/* Copyright 2026 Element Creations Ltd. Copyright 2024 New Vector Ltd. Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Copyright 2019 The Matrix.org Foundation C.I.C. Copyright 2018 New Vector Ltd Copyright 2017 OpenMarket 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, type ReactNode } from "react"; import { Link, Text } from "@vector-im/compound-web"; import SdkConfig from "../../../SdkConfig"; import Modal from "../../../Modal"; import { _t } from "../../../languageHandler"; import sendBugReport, { downloadBugReport, RageshakeError } from "../../../rageshake/submit-rageshake"; import AccessibleButton from "../elements/AccessibleButton"; import QuestionDialog from "./QuestionDialog"; import BaseDialog from "./BaseDialog"; import Field from "../elements/Field"; import Spinner from "../elements/Spinner"; import DialogButtons from "../elements/DialogButtons"; import { sendSentryReport } from "../../../sentry"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; import { getBrowserSupport } from "../../../SupportedBrowser"; import { BugReportEndpointURLLocal } from "../../../IConfigOptions"; export interface BugReportDialogProps { onFinished: (success: boolean) => void; initialText?: string; label?: string; error?: unknown; } interface IState { sendLogs: boolean; busy: boolean; err: ReactNode | null; issueUrl: string; text: string; progress: string | null; downloadBusy: boolean; downloadProgress: string | null; } export default class BugReportDialog extends React.Component { private unmounted: boolean; private issueRef: React.RefObject; private readonly isLocalOnly: boolean; public constructor(props: BugReportDialogProps) { super(props); this.state = { sendLogs: true, busy: false, err: null, issueUrl: "", text: props.initialText || "", progress: null, downloadBusy: false, downloadProgress: null, }; this.unmounted = false; this.issueRef = React.createRef(); // This config is static at runtime, but may change during tests. this.isLocalOnly = SdkConfig.get().bug_report_endpoint_url === BugReportEndpointURLLocal; } public componentDidMount(): void { this.unmounted = false; this.issueRef.current?.focus(); // Get all of the extra info dumped to the console when someone is about // to send debug logs. Since this is a fire and forget action, we do // this when the bug report dialog is opened instead of when we submit // logs because we have no signal to know when all of the various // components have finished logging. Someone could potentially send logs // before we fully dump everything but it's probably unlikely. defaultDispatcher.dispatch({ action: Action.DumpDebugLogs, }); } public componentWillUnmount(): void { this.unmounted = true; } private onCancel = (): void => { this.props.onFinished(false); }; private getErrorText(error: Error | RageshakeError): ReactNode { if (error instanceof RageshakeError) { let errorText; switch (error.errorcode) { case "DISALLOWED_APP": errorText = _t("bug_reporting|failed_send_logs_causes|disallowed_app"); break; case "REJECTED_BAD_VERSION": errorText = _t("bug_reporting|failed_send_logs_causes|rejected_version"); break; case "REJECTED_UNEXPECTED_RECOVERY_KEY": errorText = _t("bug_reporting|failed_send_logs_causes|rejected_recovery_key"); break; default: if (error.errorcode?.startsWith("REJECTED")) { errorText = _t("bug_reporting|failed_send_logs_causes|rejected_generic"); } else { errorText = _t("bug_reporting|failed_send_logs_causes|server_unknown_error"); } break; } return ( <>

{errorText}

{error.policyURL && ( {_t("action|learn_more")} )} ); } else { return

{_t("bug_reporting|failed_send_logs_causes|unknown_error")}

; } } private onSubmit = (): void => { if ((!this.state.text || !this.state.text.trim()) && (!this.state.issueUrl || !this.state.issueUrl.trim())) { this.setState({ err: _t("bug_reporting|error_empty"), }); return; } const userText = (this.state.text.length > 0 ? this.state.text + "\n\n" : "") + "Issue: " + (this.state.issueUrl.length > 0 ? this.state.issueUrl : "No issue link given"); this.setState({ busy: true, progress: null, err: null }); this.sendProgressCallback(_t("bug_reporting|preparing_logs")); if (this.isLocalOnly) { // Shouldn't reach here, but throw in case we do. this.setState({ err: _t("bug_reporting|failed_send_logs_causes|unknown_error"), }); return; } sendBugReport(SdkConfig.get().bug_report_endpoint_url, { userText, sendLogs: true, progressCallback: this.sendProgressCallback, labels: this.props.label ? [this.props.label] : [], }).then( () => { if (!this.unmounted) { this.props.onFinished(false); Modal.createDialog(QuestionDialog, { title: _t("bug_reporting|logs_sent"), description: _t("bug_reporting|thank_you"), hasCancelButton: false, }); } }, (err) => { if (!this.unmounted) { this.setState({ busy: false, progress: null, err: this.getErrorText(err), }); } }, ); sendSentryReport(this.state.text, this.state.issueUrl, this.props.error); }; private onDownload = async (): Promise => { this.setState({ downloadBusy: true }); this.downloadProgressCallback(_t("bug_reporting|preparing_download")); try { await downloadBugReport({ sendLogs: true, progressCallback: this.downloadProgressCallback, labels: this.props.label ? [this.props.label] : [], }); this.setState({ downloadBusy: false, downloadProgress: null, }); } catch (err) { if (!this.unmounted) { this.setState({ downloadBusy: false, downloadProgress: _t("bug_reporting|failed_download_logs") + `${err instanceof Error ? err.message : ""}`, }); } } }; private onTextChange = (ev: React.FormEvent): void => { this.setState({ text: ev.currentTarget.value }); }; private onIssueUrlChange = (ev: React.FormEvent): void => { this.setState({ issueUrl: ev.currentTarget.value }); }; private sendProgressCallback = (progress: string): void => { if (this.unmounted) { return; } this.setState({ progress }); }; private downloadProgressCallback = (downloadProgress: string): void => { if (this.unmounted) { return; } this.setState({ downloadProgress }); }; public render(): React.ReactNode { let error: JSX.Element | undefined; if (this.state.err) { error =
{this.state.err}
; } let progress: JSX.Element | undefined; if (this.state.busy) { progress = (
{this.state.progress} ...
); } let warning: JSX.Element | undefined; if ( (window.Modernizr && Object.values(window.Modernizr).some((support) => support === false)) || !getBrowserSupport() ) { warning = {_t("bug_reporting|unsupported_browser")}; } return (
{warning} {_t("bug_reporting|description")} {this.isLocalOnly ? ( <>{this.state.downloadProgress && {this.state.downloadProgress} ...} ) : ( <> {_t( "bug_reporting|before_submitting", {}, { a: (sub) => ( {sub} ), }, )}
{_t("bug_reporting|download_logs")} {this.state.downloadProgress && {this.state.downloadProgress} ...}
{progress} {error} )}
); } }