// Copyright 2015 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { MouseEvent } from 'react'; import React, { useEffect, useState } from 'react'; import copyText from 'copy-text-to-clipboard'; import type { ExecuteMenuRoleType } from './TitleBarContainer'; import type { LocalizerType } from '../types/Util'; import * as Errors from '../types/errors'; import * as log from '../logging/log'; import { Button, ButtonVariant } from './Button'; import { Spinner } from './Spinner'; import { TitleBarContainer } from './TitleBarContainer'; import { ToastDebugLogError } from './ToastDebugLogError'; import { ToastLinkCopied } from './ToastLinkCopied'; import { ToastLoadingFullLogs } from './ToastLoadingFullLogs'; import { createSupportUrl } from '../util/createSupportUrl'; import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser'; import { useEscapeHandling } from '../hooks/useEscapeHandling'; import { useTheme } from '../hooks/useTheme'; enum LoadState { NotStarted, Started, Loaded, Submitting, } export type PropsType = { closeWindow: () => unknown; downloadLog: (text: string) => unknown; i18n: LocalizerType; fetchLogs: () => Promise<string>; uploadLogs: (logs: string) => Promise<string>; hasCustomTitleBar: boolean; executeMenuRole: ExecuteMenuRoleType; }; enum ToastType { Copied, Error, Loading, } export function DebugLogWindow({ closeWindow, downloadLog, i18n, fetchLogs, uploadLogs, hasCustomTitleBar, executeMenuRole, }: PropsType): JSX.Element { const [loadState, setLoadState] = useState<LoadState>(LoadState.NotStarted); const [logText, setLogText] = useState<string | undefined>(); const [publicLogURL, setPublicLogURL] = useState<string | undefined>(); const [textAreaValue, setTextAreaValue] = useState<string>( i18n('icu:loading') ); const [toastType, setToastType] = useState<ToastType | undefined>(); const theme = useTheme(); useEscapeHandling(closeWindow); useEffect(() => { setLoadState(LoadState.Started); let shouldCancel = false; async function doFetchLogs() { const fetchedLogText = await fetchLogs(); if (shouldCancel) { return; } setToastType(ToastType.Loading); setLogText(fetchedLogText); setLoadState(LoadState.Loaded); // This number is somewhat arbitrary; we want to show enough that it's // clear that we need to scroll, but not so many that things get slow. const linesToShow = Math.ceil(Math.min(window.innerHeight, 2000) / 5); const value = fetchedLogText.split(/\n/g, linesToShow).join('\n'); setTextAreaValue(`${value}\n\n\n${i18n('icu:debugLogLogIsIncomplete')}`); setToastType(undefined); } void doFetchLogs(); return () => { shouldCancel = true; }; }, [fetchLogs, i18n]); const handleSubmit = async (ev: MouseEvent) => { ev.preventDefault(); const text = logText; if (!text || text.length === 0) { return; } setLoadState(LoadState.Submitting); try { const publishedLogURL = await uploadLogs(text); setPublicLogURL(publishedLogURL); } catch (error) { log.error('DebugLogWindow error:', Errors.toLogFormat(error)); setLoadState(LoadState.Loaded); setToastType(ToastType.Error); } }; function closeToast() { setToastType(undefined); } let toastElement: JSX.Element | undefined; if (toastType === ToastType.Loading) { toastElement = <ToastLoadingFullLogs i18n={i18n} onClose={closeToast} />; } else if (toastType === ToastType.Copied) { toastElement = <ToastLinkCopied i18n={i18n} onClose={closeToast} />; } else if (toastType === ToastType.Error) { toastElement = <ToastDebugLogError i18n={i18n} onClose={closeToast} />; } if (publicLogURL) { const copyLog = (ev: MouseEvent) => { ev.preventDefault(); copyText(publicLogURL); setToastType(ToastType.Copied); }; const supportURL = createSupportUrl({ locale: window.SignalContext.getI18nLocale(), query: { debugLog: publicLogURL, }, }); return ( <TitleBarContainer hasCustomTitleBar={hasCustomTitleBar} theme={theme} executeMenuRole={executeMenuRole} > <div className="DebugLogWindow"> <div> <div className="DebugLogWindow__title"> {i18n('icu:debugLogSuccess')} </div> <p className="DebugLogWindow__subtitle"> {i18n('icu:debugLogSuccessNextSteps')} </p> </div> <div className="DebugLogWindow__container"> <input className="DebugLogWindow__link" readOnly type="text" dir="auto" value={publicLogURL} /> </div> <div className="DebugLogWindow__footer"> <Button onClick={() => openLinkInWebBrowser(supportURL)} variant={ButtonVariant.Secondary} > {i18n('icu:reportIssue')} </Button> <Button onClick={copyLog}>{i18n('icu:debugLogCopy')}</Button> </div> {toastElement} </div> </TitleBarContainer> ); } const canSubmit = Boolean(logText) && loadState !== LoadState.Submitting; const canSave = Boolean(logText); const isLoading = loadState === LoadState.Started || loadState === LoadState.Submitting; return ( <TitleBarContainer hasCustomTitleBar={hasCustomTitleBar} theme={theme} executeMenuRole={executeMenuRole} > <div className="DebugLogWindow"> <div> <div className="DebugLogWindow__title"> {i18n('icu:submitDebugLog')} </div> <p className="DebugLogWindow__subtitle"> {i18n('icu:debugLogExplanation')} </p> </div> {isLoading ? ( <div className="DebugLogWindow__container"> <Spinner svgSize="normal" /> </div> ) : ( <div className="DebugLogWindow__scroll_area"> <pre className="DebugLogWindow__scroll_area__text"> {textAreaValue} </pre> </div> )} <div className="DebugLogWindow__footer"> <Button disabled={!canSave} onClick={() => { if (logText) { downloadLog(logText); } }} variant={ButtonVariant.Secondary} > {i18n('icu:debugLogSave')} </Button> <Button disabled={!canSubmit} onClick={handleSubmit}> {i18n('icu:submit')} </Button> </div> {toastElement} </div> </TitleBarContainer> ); }