import { ChangeEvent, createContext, Dispatch, ReactNode, SetStateAction, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react'; import { messages } from '../../shared/gettext'; import { getDownloadUrl } from '../../shared/version'; import { useAppContext } from '../context'; import useActions from '../lib/actionsHook'; import { useHistory } from '../lib/history'; import { useSelector } from '../redux/store'; import support from '../redux/support/actions'; import * as AppButton from './AppButton'; import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from './AriaGroup'; import ImageView from './ImageView'; import { BackAction } from './KeyboardNavigation'; import { Footer, Layout, SettingsContainer } from './Layout'; import { ModalAlert, ModalAlertType } from './Modal'; import { NavigationBar, NavigationItems, TitleBarItem } from './NavigationBar'; import { StyledContent, StyledContentContainer, StyledEmail, StyledEmailInput, StyledForm, StyledFormEmailRow, StyledFormMessageRow, StyledMessageInput, StyledSendStatus, StyledSentMessage, StyledStatusIcon, StyledThanks, } from './ProblemReportStyles'; import SettingsHeader, { HeaderSubTitle, HeaderTitle } from './SettingsHeader'; enum SendState { initial, confirm, sending, success, failed, } export default function ProblemReport() { return ( ); } function ProblemReportComponent() { const history = useHistory(); return ( { // TRANSLATORS: Title label in navigation bar messages.pgettext('support-view', 'Report a problem') }
); } function Header() { const { sendState } = useProblemReportContext(); return ( {messages.pgettext('support-view', 'Report a problem')} {(sendState === SendState.initial || sendState === SendState.confirm) && ( {messages.pgettext( 'support-view', 'To help you more effectively, your app’s log file will be attached to this message. Your data will remain secure and private, as it is anonymised before being sent over an encrypted channel.', )} )} ); } function Content() { const { sendState } = useProblemReportContext(); switch (sendState) { case SendState.initial: case SendState.confirm: return
; case SendState.sending: return ; case SendState.success: return ; case SendState.failed: return ; default: return null; } } function Form() { const { viewLog } = useAppContext(); const { email, setEmail, message, setMessage, onSend } = useProblemReportContext(); const { collectLog } = useCollectLog(); const [disableActions, setDisableActions] = useState(false); const onViewLog = useCallback(async () => { setDisableActions(true); try { const reportId = await collectLog(); await viewLog(reportId); } catch { // TODO: handle error } finally { setDisableActions(false); } }, []); const onChangeEmail = useCallback((event: ChangeEvent) => { setEmail(event.target.value); }, []); const onChangeDescription = useCallback((event: ChangeEvent) => { setMessage(event.target.value); }, []); const validate = () => message.trim().length > 0; return (
{messages.pgettext('support-view', 'View app logs')} {messages.pgettext('support-view', 'Send')}
); } function Sending() { return ( {messages.pgettext('support-view', 'Sending...')} ); } function Sent() { const { email } = useProblemReportContext(); const reachBackMessage: ReactNode[] = // TRANSLATORS: The message displayed to the user after submitting the problem report, given that the user left his or her email for us to reach back. // TRANSLATORS: Available placeholders: // TRANSLATORS: %(email)s messages .pgettext('support-view', 'If needed we will contact you at %(email)s') .split('%(email)s', 2); reachBackMessage.splice(1, 0, {email}); return ( {messages.pgettext('support-view', 'Sent')} {messages.pgettext('support-view', 'Thanks!')} {messages.pgettext('support-view', 'We will look into this.')} {email.trim().length > 0 ? {reachBackMessage} : null} ); } function Failed() { const { setSendState, onSend } = useProblemReportContext(); const handleEditMessage = useCallback(() => { setSendState(SendState.initial); }, []); return ( {messages.pgettext('support-view', 'Failed to send')} {messages.pgettext( 'support-view', 'If you exit the form and try again later, the information you already entered will still be here.', )}
{messages.pgettext('support-view', 'Edit message')} {messages.pgettext('support-view', 'Try again')}
); } function NoEmailDialog() { const { sendState, setSendState, onSend } = useProblemReportContext(); const message = messages.pgettext( 'support-view', 'You are about to send the problem report without a way for us to get back to you. If you want an answer to your report you will have to enter an email address.', ); const onCancelNoEmailDialog = useCallback(() => { setSendState(SendState.initial); }, []); return ( {messages.pgettext('support-view', 'Send anyway')} , {messages.gettext('Back')} , ]} close={onCancelNoEmailDialog} /> ); } function OutdatedVersionWarningDialog() { const history = useHistory(); const { openUrl } = useAppContext(); const isOffline = useSelector((state) => state.connection.isBlocked); const suggestedIsBeta = useSelector((state) => state.version.suggestedIsBeta ?? false); const outdatedVersion = useSelector((state) => !!state.version.suggestedUpgrade); const [showOutdatedVersionWarning, setShowOutdatedVersionWarning] = useState(outdatedVersion); const acknowledgeOutdatedVersion = useCallback(() => { setShowOutdatedVersionWarning(false); }, []); const openDownloadLink = useCallback(async () => { await openUrl(getDownloadUrl(suggestedIsBeta)); }, [suggestedIsBeta]); const onClose = useCallback(() => history.pop(), [history.pop]); const outdatedVersionCancel = useCallback(() => { acknowledgeOutdatedVersion(); onClose(); }, [onClose]); const message = messages.pgettext( 'support-view', 'You are using an old version of the app. Please upgrade and see if the problem still exists before sending a report.', ); return ( {messages.pgettext('support-view', 'Upgrade app')} , {messages.pgettext('support-view', 'Continue anyway')} , {messages.gettext('Cancel')} , ]} close={onClose} /> ); } const useCollectLog = () => { const { collectProblemReport } = useAppContext(); const accountHistory = useSelector((state) => state.account.accountHistory); const collectLogPromise = useRef>(); const collectLog = useCallback(async (): Promise => { if (collectLogPromise.current) { return collectLogPromise.current; } else { const collectPromise = collectProblemReport(accountHistory); // save promise to prevent subsequent requests collectLogPromise.current = collectPromise; try { const reportId = await collectPromise; return reportId; } catch (error) { collectLogPromise.current = undefined; throw error; } } }, [collectLogPromise]); return { collectLog }; }; type ProblemReportContextType = { sendState: SendState; setSendState: Dispatch>; email: string; setEmail: Dispatch>; message: string; setMessage: Dispatch>; onSend: () => Promise; }; const ProblemReportContext = createContext(undefined); const ProblemReportContextProvider = ({ children }: { children: ReactNode }) => { const { sendProblemReport } = useAppContext(); const { clearReportForm, saveReportForm } = useActions(support); const { email: defaultEmail, message: defaultMessage } = useSelector((state) => state.support); const { collectLog } = useCollectLog(); const [sendState, setSendState] = useState(SendState.initial); const [email, setEmail] = useState(defaultEmail); const [message, setMessage] = useState(defaultMessage); const sendReport = useCallback(async () => { try { const reportId = await collectLog(); await sendProblemReport(email, message, reportId); clearReportForm(); setSendState(SendState.success); } catch { setSendState(SendState.failed); } }, [email, message]); const onSend = useCallback(async () => { if (sendState === SendState.initial && email.length === 0) { setSendState(SendState.confirm); } else if ( sendState === SendState.initial || sendState === SendState.confirm || sendState === SendState.failed ) { try { setSendState(SendState.sending); await sendReport(); } catch { // No-op } } }, [email, sendReport, sendState]); /** * Save the form whenever email or message gets updated */ useEffect(() => { saveReportForm({ email, message }); }, [email, message]); const value: ProblemReportContextType = useMemo( () => ({ sendState, setSendState, email, setEmail, message, setMessage, onSend }), [sendState, setSendState, email, setEmail, message, setMessage, onSend], ); return {children}; }; const useProblemReportContext = () => { const context = useContext(ProblemReportContext); if (!context) { throw new Error('useProblemReportContext must be used within a ProblemReportContextProvider'); } return context; };