summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorHank <hank@mullvad.net>2022-10-20 16:36:36 +0200
committerHank <hank@mullvad.net>2022-10-20 16:36:36 +0200
commitca1bbade3ea6e0528f984c08e6118da7ef849eca (patch)
tree488504146ca25602c2245907b6ec2add994b30ff
parent8df7475bc989276c2841d0fa8c290700b6bb3d91 (diff)
parent349e47e9786dc34e3a6c49027fb02de6b13b3610 (diff)
downloadmullvadvpn-ca1bbade3ea6e0528f984c08e6118da7ef849eca.tar.xz
mullvadvpn-ca1bbade3ea6e0528f984c08e6118da7ef849eca.zip
Merge branch 'problem-report-component'
-rw-r--r--gui/src/renderer/components/AppRouter.tsx4
-rw-r--r--gui/src/renderer/components/ProblemReport.tsx780
-rw-r--r--gui/src/renderer/containers/ProblemReportPage.tsx39
3 files changed, 400 insertions, 423 deletions
diff --git a/gui/src/renderer/components/AppRouter.tsx b/gui/src/renderer/components/AppRouter.tsx
index 1f0d36fc24..4dad09a773 100644
--- a/gui/src/renderer/components/AppRouter.tsx
+++ b/gui/src/renderer/components/AppRouter.tsx
@@ -2,7 +2,6 @@ import { createRef, useCallback, useEffect, useState } from 'react';
import { Route, Switch } from 'react-router';
import LoginPage from '../containers/LoginPage';
-import ProblemReportPage from '../containers/ProblemReportPage';
import SelectLocationPage from '../containers/SelectLocationPage';
import { useAppContext } from '../context';
import { ITransitionSpecification, transitions, useHistory } from '../lib/history';
@@ -20,6 +19,7 @@ import Focus, { IFocusHandle } from './Focus';
import Launch from './Launch';
import MainView from './MainView';
import OpenVpnSettings from './OpenVpnSettings';
+import ProblemReport from './ProblemReport';
import SelectLanguage from './SelectLanguage';
import Settings from './Settings';
import SplitTunnelingSettings from './SplitTunnelingSettings';
@@ -78,7 +78,7 @@ export default function AppRouter() {
<Route exact path={RoutePath.openVpnSettings} component={OpenVpnSettings} />
<Route exact path={RoutePath.splitTunneling} component={SplitTunnelingSettings} />
<Route exact path={RoutePath.support} component={Support} />
- <Route exact path={RoutePath.problemReport} component={ProblemReportPage} />
+ <Route exact path={RoutePath.problemReport} component={ProblemReport} />
<Route exact path={RoutePath.selectLocation} component={SelectLocationPage} />
<Route exact path={RoutePath.filter} component={Filter} />
</Switch>
diff --git a/gui/src/renderer/components/ProblemReport.tsx b/gui/src/renderer/components/ProblemReport.tsx
index aa89365fd5..9d826200d3 100644
--- a/gui/src/renderer/components/ProblemReport.tsx
+++ b/gui/src/renderer/components/ProblemReport.tsx
@@ -1,9 +1,24 @@
-import * as React from 'react';
+import {
+ ChangeEvent,
+ createContext,
+ Dispatch,
+ ReactNode,
+ SetStateAction,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
import { links } from '../../config.json';
-import { AccountToken } from '../../shared/daemon-rpc-types';
import { messages } from '../../shared/gettext';
-import { IProblemReportForm } from '../redux/support/actions';
+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';
@@ -35,426 +50,427 @@ enum SendState {
failed,
}
-interface IProblemReportState {
- email: string;
- message: string;
- savedReportId?: string;
- sendState: SendState;
- disableActions: boolean;
- showOutdatedVersionWarning: boolean;
+export default function ProblemReport() {
+ return (
+ <ProblemReportContextProvider>
+ <ProblemReportComponent />
+ </ProblemReportContextProvider>
+ );
}
-interface IProblemReportProps {
- defaultEmail: string;
- defaultMessage: string;
- accountHistory?: AccountToken;
- isOffline: boolean;
- onClose: () => void;
- viewLog: (path: string) => void;
- saveReportForm: (form: IProblemReportForm) => void;
- clearReportForm: () => void;
- collectProblemReport: (accountToRedact?: string) => Promise<string>;
- sendProblemReport: (email: string, message: string, savedReportId: string) => Promise<void>;
- outdatedVersion: boolean;
- suggestedIsBeta: boolean;
- onExternalLink: (url: string) => void;
-}
+function ProblemReportComponent() {
+ const history = useHistory();
-export default class ProblemReport extends React.Component<
- IProblemReportProps,
- IProblemReportState
-> {
- public state = {
- email: '',
- message: '',
- savedReportId: undefined,
- sendState: SendState.initial,
- disableActions: false,
- showOutdatedVersionWarning: false,
- };
+ return (
+ <BackAction action={history.pop}>
+ <Layout>
+ <SettingsContainer>
+ <NavigationBar>
+ <NavigationItems>
+ <TitleBarItem>
+ {
+ // TRANSLATORS: Title label in navigation bar
+ messages.pgettext('support-view', 'Report a problem')
+ }
+ </TitleBarItem>
+ </NavigationItems>
+ </NavigationBar>
+ <StyledContentContainer>
+ <Header />
+ <Content />
+ </StyledContentContainer>
- private collectLogPromise?: Promise<string>;
+ <NoEmailDialog />
+ <OutdatedVersionWarningDialog />
+ </SettingsContainer>
+ </Layout>
+ </BackAction>
+ );
+}
- constructor(props: IProblemReportProps) {
- super(props);
+function Header() {
+ const { sendState } = useProblemReportContext();
- // seed initial data from props
- this.state.email = props.defaultEmail;
- this.state.message = props.defaultMessage;
- this.state.showOutdatedVersionWarning = props.outdatedVersion;
- }
+ return (
+ <SettingsHeader>
+ <HeaderTitle>{messages.pgettext('support-view', 'Report a problem')}</HeaderTitle>
+ {(sendState === SendState.initial || sendState === SendState.confirm) && (
+ <HeaderSubTitle>
+ {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.',
+ )}
+ </HeaderSubTitle>
+ )}
+ </SettingsHeader>
+ );
+}
+
+function Content() {
+ const { sendState } = useProblemReportContext();
- public validate() {
- return this.state.message.trim().length > 0;
+ switch (sendState) {
+ case SendState.initial:
+ case SendState.confirm:
+ return <Form />;
+ case SendState.sending:
+ return <Sending />;
+ case SendState.success:
+ return <Sent />;
+ case SendState.failed:
+ return <Failed />;
+ default:
+ return null;
}
+}
- public onChangeEmail = (event: React.ChangeEvent<HTMLInputElement>) => {
- this.setState({ email: event.target.value }, () => {
- this.saveFormData();
- });
- };
+function Form() {
+ const { viewLog } = useAppContext();
+ const { email, setEmail, message, setMessage, onSend } = useProblemReportContext();
+ const { collectLog } = useCollectLog();
- public onChangeDescription = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
- this.setState({ message: event.target.value }, () => {
- this.saveFormData();
- });
- };
+ const [disableActions, setDisableActions] = useState(false);
- public onViewLog = () => {
- this.performWithActionsDisabled(async () => {
- try {
- const reportId = await this.collectLog();
- this.props.viewLog(reportId);
- } catch (error) {
- // TODO: handle error
- }
- });
- };
+ const onViewLog = useCallback(async () => {
+ setDisableActions(true);
- public onSend = async (): Promise<void> => {
- const sendState = this.state.sendState;
- if (sendState === SendState.initial && this.state.email.length === 0) {
- this.setState({ sendState: SendState.confirm });
- } else if (
- sendState === SendState.initial ||
- sendState === SendState.confirm ||
- sendState === SendState.failed
- ) {
- try {
- await this.sendReport();
- } catch (error) {
- // No-op
- }
+ try {
+ const reportId = await collectLog();
+ await viewLog(reportId);
+ } catch (error) {
+ // TODO: handle error
+ } finally {
+ setDisableActions(false);
}
- };
+ }, []);
+
+ const onChangeEmail = useCallback((event: ChangeEvent<HTMLInputElement>) => {
+ setEmail(event.target.value);
+ }, []);
- public onCancelNoEmailDialog = () => {
- this.setState({ sendState: SendState.initial });
- };
+ const onChangeDescription = useCallback((event: ChangeEvent<HTMLTextAreaElement>) => {
+ setMessage(event.target.value);
+ }, []);
- public render() {
- const { sendState } = this.state;
- const header = (
- <SettingsHeader>
- <HeaderTitle>{messages.pgettext('support-view', 'Report a problem')}</HeaderTitle>
- {(sendState === SendState.initial || sendState === SendState.confirm) && (
- <HeaderSubTitle>
- {messages.pgettext(
+ const validate = () => message.trim().length > 0;
+
+ return (
+ <StyledContent>
+ <StyledForm>
+ <StyledFormEmailRow>
+ <StyledEmailInput
+ placeholder={messages.pgettext('support-view', 'Your email (optional)')}
+ defaultValue={email}
+ onChange={onChangeEmail}
+ />
+ </StyledFormEmailRow>
+ <StyledFormMessageRow>
+ <StyledMessageInput
+ placeholder={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.',
+ 'Please describe your problem in English or Swedish.',
)}
- </HeaderSubTitle>
- )}
- </SettingsHeader>
- );
+ defaultValue={message}
+ onChange={onChangeDescription}
+ />
+ </StyledFormMessageRow>
+ </StyledForm>
+ <Footer>
+ <AriaDescriptionGroup>
+ <AriaDescribed>
+ <AppButton.ButtonGroup>
+ <AppButton.BlueButton onClick={onViewLog} disabled={disableActions}>
+ <AppButton.Label>
+ {messages.pgettext('support-view', 'View app logs')}
+ </AppButton.Label>
+ <AriaDescription>
+ <AppButton.Icon
+ source="icon-extLink"
+ height={16}
+ width={16}
+ aria-label={messages.pgettext('accessibility', 'Opens externally')}
+ />
+ </AriaDescription>
+ </AppButton.BlueButton>
+ </AppButton.ButtonGroup>
+ </AriaDescribed>
+ </AriaDescriptionGroup>
+ <AppButton.GreenButton disabled={!validate() || disableActions} onClick={onSend}>
+ {messages.pgettext('support-view', 'Send')}
+ </AppButton.GreenButton>
+ </Footer>
+ </StyledContent>
+ );
+}
- const content = this.renderContent();
+function Sending() {
+ return (
+ <StyledContent>
+ <StyledForm>
+ <StyledStatusIcon>
+ <ImageView source="icon-spinner" height={60} width={60} />
+ </StyledStatusIcon>
+ <StyledSendStatus>{messages.pgettext('support-view', 'Sending...')}</StyledSendStatus>
+ </StyledForm>
+ </StyledContent>
+ );
+}
- return (
- <BackAction action={this.props.onClose}>
- <Layout>
- <SettingsContainer>
- <NavigationBar>
- <NavigationItems>
- <TitleBarItem>
- {
- // TRANSLATORS: Title label in navigation bar
- messages.pgettext('support-view', 'Report a problem')
- }
- </TitleBarItem>
- </NavigationItems>
- </NavigationBar>
- <StyledContentContainer>
- {header}
- {content}
- </StyledContentContainer>
+function Sent() {
+ const { email } = useProblemReportContext();
- {this.renderNoEmailDialog()}
- {this.renderOutdateVersionWarningDialog()}
- </SettingsContainer>
- </Layout>
- </BackAction>
- );
- }
+ 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, <StyledEmail key="email">{email}</StyledEmail>);
- private saveFormData() {
- this.props.saveReportForm({
- email: this.state.email,
- message: this.state.message,
- });
- }
+ return (
+ <StyledContent>
+ <StyledForm>
+ <StyledStatusIcon>
+ <ImageView source="icon-success" height={60} width={60} />
+ </StyledStatusIcon>
+ <StyledSendStatus>{messages.pgettext('support-view', 'Sent')}</StyledSendStatus>
- private async collectLog(): Promise<string> {
- if (this.collectLogPromise) {
- return this.collectLogPromise;
- } else {
- const collectPromise = this.props.collectProblemReport(this.props.accountHistory);
+ <StyledSentMessage>
+ <StyledThanks>{messages.pgettext('support-view', 'Thanks!')} </StyledThanks>
+ {messages.pgettext('support-view', 'We will look into this.')}
+ </StyledSentMessage>
+ {email.trim().length > 0 ? <StyledSentMessage>{reachBackMessage}</StyledSentMessage> : null}
+ </StyledForm>
+ </StyledContent>
+ );
+}
- // save promise to prevent subsequent requests
- this.collectLogPromise = collectPromise;
+function Failed() {
+ const { setSendState, onSend } = useProblemReportContext();
- try {
- const reportId = await collectPromise;
- return new Promise((resolve) => {
- this.setState({ savedReportId: reportId }, () => resolve(reportId));
- });
- } catch (error) {
- this.collectLogPromise = undefined;
+ const handleEditMessage = useCallback(() => {
+ setSendState(SendState.initial);
+ }, []);
- throw error;
- }
- }
- }
+ return (
+ <StyledContent>
+ <StyledForm>
+ <StyledStatusIcon>
+ <ImageView source="icon-fail" height={60} width={60} />
+ </StyledStatusIcon>
+ <StyledSendStatus>{messages.pgettext('support-view', 'Failed to send')}</StyledSendStatus>
+ <StyledSentMessage>
+ {messages.pgettext(
+ 'support-view',
+ 'If you exit the form and try again later, the information you already entered will still be here.',
+ )}
+ </StyledSentMessage>
+ </StyledForm>
+ <Footer>
+ <AppButton.ButtonGroup>
+ <AppButton.BlueButton onClick={handleEditMessage}>
+ {messages.pgettext('support-view', 'Edit message')}
+ </AppButton.BlueButton>
+ <AppButton.GreenButton onClick={onSend}>
+ {messages.pgettext('support-view', 'Try again')}
+ </AppButton.GreenButton>
+ </AppButton.ButtonGroup>
+ </Footer>
+ </StyledContent>
+ );
+}
- private sendReport(): Promise<void> {
- return new Promise((resolve, reject) => {
- this.setState({ sendState: SendState.sending }, async () => {
- try {
- const { email, message } = this.state;
- const reportId = await this.collectLog();
- await this.props.sendProblemReport(email, message, reportId);
- this.props.clearReportForm();
- this.setState({ sendState: SendState.success }, () => {
- resolve();
- });
- } catch (error) {
- this.setState({ sendState: SendState.failed }, () => {
- reject(error);
- });
- }
- });
- });
- }
+function NoEmailDialog() {
+ const { sendState, setSendState, onSend } = useProblemReportContext();
- private renderContent() {
- switch (this.state.sendState) {
- case SendState.initial:
- case SendState.confirm:
- return this.renderForm();
- case SendState.sending:
- return this.renderSending();
- case SendState.success:
- return this.renderSent();
- case SendState.failed:
- return this.renderFailed();
- default:
- return null;
- }
- }
+ 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.',
+ );
- private renderNoEmailDialog() {
- 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.',
- );
- return (
- <ModalAlert
- isOpen={this.state.sendState === SendState.confirm}
- type={ModalAlertType.warning}
- message={message}
- buttons={[
- <AppButton.RedButton key="proceed" onClick={this.onSend}>
- {messages.pgettext('support-view', 'Send anyway')}
- </AppButton.RedButton>,
- <AppButton.BlueButton key="cancel" onClick={this.onCancelNoEmailDialog}>
- {messages.gettext('Back')}
- </AppButton.BlueButton>,
- ]}
- close={this.onCancelNoEmailDialog}
- />
- );
- }
+ const onCancelNoEmailDialog = useCallback(() => {
+ setSendState(SendState.initial);
+ }, []);
- private acknowledgeOutdateVersion = () => {
- this.setState({ showOutdatedVersionWarning: false });
- };
+ return (
+ <ModalAlert
+ isOpen={sendState === SendState.confirm}
+ type={ModalAlertType.warning}
+ message={message}
+ buttons={[
+ <AppButton.RedButton key="proceed" onClick={onSend}>
+ {messages.pgettext('support-view', 'Send anyway')}
+ </AppButton.RedButton>,
+ <AppButton.BlueButton key="cancel" onClick={onCancelNoEmailDialog}>
+ {messages.gettext('Back')}
+ </AppButton.BlueButton>,
+ ]}
+ close={onCancelNoEmailDialog}
+ />
+ );
+}
- private openDownloadLink = () =>
- this.props.onExternalLink(this.props.suggestedIsBeta ? links.betaDownload : links.download);
+function OutdatedVersionWarningDialog() {
+ const history = useHistory();
+ const { openUrl } = useAppContext();
- private renderOutdateVersionWarningDialog() {
- 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 (
- <ModalAlert
- isOpen={this.state.showOutdatedVersionWarning}
- type={ModalAlertType.warning}
- message={message}
- buttons={[
- <AriaDescriptionGroup key="upgrade">
- <AriaDescribed>
- <AppButton.GreenButton
- disabled={this.props.isOffline}
- onClick={this.openDownloadLink}>
- <AppButton.Label>
- {messages.pgettext('support-view', 'Upgrade app')}
- </AppButton.Label>
- <AriaDescription>
- <AppButton.Icon
- height={16}
- width={16}
- source="icon-extLink"
- aria-label={messages.pgettext('accessibility', 'Opens externally')}
- />
- </AriaDescription>
- </AppButton.GreenButton>
- </AriaDescribed>
- </AriaDescriptionGroup>,
- <AppButton.RedButton key="proceed" onClick={this.acknowledgeOutdateVersion}>
- {messages.pgettext('support-view', 'Continue anyway')}
- </AppButton.RedButton>,
- <AppButton.BlueButton key="cancel" onClick={this.outdatedVersionCancel}>
- {messages.gettext('Cancel')}
- </AppButton.BlueButton>,
- ]}
- close={this.props.onClose}
- />
- );
- }
+ const isOffline = useSelector((state) => state.connection.isBlocked);
+ const suggestedIsBeta = useSelector((state) => state.version.suggestedIsBeta ?? false);
+ const outdatedVersion = useSelector((state) => !!state.version.suggestedUpgrade);
- private outdatedVersionCancel = () => {
- this.acknowledgeOutdateVersion();
- this.props.onClose();
- };
+ const [showOutdatedVersionWarning, setShowOutdatedVersionWarning] = useState(outdatedVersion);
- private renderForm() {
- return (
- <StyledContent>
- <StyledForm>
- <StyledFormEmailRow>
- <StyledEmailInput
- placeholder={messages.pgettext('support-view', 'Your email (optional)')}
- defaultValue={this.state.email}
- onChange={this.onChangeEmail}
- />
- </StyledFormEmailRow>
- <StyledFormMessageRow>
- <StyledMessageInput
- placeholder={messages.pgettext(
- 'support-view',
- 'Please describe your problem in English or Swedish.',
- )}
- defaultValue={this.state.message}
- onChange={this.onChangeDescription}
- />
- </StyledFormMessageRow>
- </StyledForm>
- <Footer>
- <AriaDescriptionGroup>
- <AriaDescribed>
- <AppButton.ButtonGroup>
- <AppButton.BlueButton onClick={this.onViewLog} disabled={this.state.disableActions}>
- <AppButton.Label>
- {messages.pgettext('support-view', 'View app logs')}
- </AppButton.Label>
- <AriaDescription>
- <AppButton.Icon
- source="icon-extLink"
- height={16}
- width={16}
- aria-label={messages.pgettext('accessibility', 'Opens externally')}
- />
- </AriaDescription>
- </AppButton.BlueButton>
- </AppButton.ButtonGroup>
- </AriaDescribed>
- </AriaDescriptionGroup>
- <AppButton.GreenButton
- disabled={!this.validate() || this.state.disableActions}
- onClick={this.onSend}>
- {messages.pgettext('support-view', 'Send')}
- </AppButton.GreenButton>
- </Footer>
- </StyledContent>
- );
- }
+ const acknowledgeOutdatedVersion = useCallback(() => {
+ setShowOutdatedVersionWarning(false);
+ }, []);
- private renderSending() {
- return (
- <StyledContent>
- <StyledForm>
- <StyledStatusIcon>
- <ImageView source="icon-spinner" height={60} width={60} />
- </StyledStatusIcon>
- <StyledSendStatus>{messages.pgettext('support-view', 'Sending...')}</StyledSendStatus>
- </StyledForm>
- </StyledContent>
- );
- }
+ const openDownloadLink = useCallback(async () => {
+ await openUrl(suggestedIsBeta ? links.betaDownload : links.download);
+ }, [suggestedIsBeta]);
- private renderSent() {
- const reachBackMessage: React.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, <StyledEmail key="email">{this.state.email}</StyledEmail>);
+ const onClose = useCallback(() => history.pop(), [history.pop]);
- return (
- <StyledContent>
- <StyledForm>
- <StyledStatusIcon>
- <ImageView source="icon-success" height={60} width={60} />
- </StyledStatusIcon>
- <StyledSendStatus>{messages.pgettext('support-view', 'Sent')}</StyledSendStatus>
+ const outdatedVersionCancel = useCallback(() => {
+ acknowledgeOutdatedVersion();
+ onClose();
+ }, [onClose]);
- <StyledSentMessage>
- <StyledThanks>{messages.pgettext('support-view', 'Thanks!')} </StyledThanks>
- {messages.pgettext('support-view', 'We will look into this.')}
- </StyledSentMessage>
- {this.state.email.trim().length > 0 ? (
- <StyledSentMessage>{reachBackMessage}</StyledSentMessage>
- ) : null}
- </StyledForm>
- </StyledContent>
- );
- }
+ 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.',
+ );
- private renderFailed() {
- return (
- <StyledContent>
- <StyledForm>
- <StyledStatusIcon>
- <ImageView source="icon-fail" height={60} width={60} />
- </StyledStatusIcon>
- <StyledSendStatus>{messages.pgettext('support-view', 'Failed to send')}</StyledSendStatus>
- <StyledSentMessage>
- {messages.pgettext(
- 'support-view',
- 'If you exit the form and try again later, the information you already entered will still be here.',
- )}
- </StyledSentMessage>
- </StyledForm>
- <Footer>
- <AppButton.ButtonGroup>
- <AppButton.BlueButton onClick={this.handleEditMessage}>
- {messages.pgettext('support-view', 'Edit message')}
- </AppButton.BlueButton>
- <AppButton.GreenButton onClick={this.onSend}>
- {messages.pgettext('support-view', 'Try again')}
+ return (
+ <ModalAlert
+ isOpen={showOutdatedVersionWarning}
+ type={ModalAlertType.warning}
+ message={message}
+ buttons={[
+ <AriaDescriptionGroup key="upgrade">
+ <AriaDescribed>
+ <AppButton.GreenButton disabled={isOffline} onClick={openDownloadLink}>
+ <AppButton.Label>{messages.pgettext('support-view', 'Upgrade app')}</AppButton.Label>
+ <AriaDescription>
+ <AppButton.Icon
+ height={16}
+ width={16}
+ source="icon-extLink"
+ aria-label={messages.pgettext('accessibility', 'Opens externally')}
+ />
+ </AriaDescription>
</AppButton.GreenButton>
- </AppButton.ButtonGroup>
- </Footer>
- </StyledContent>
- );
- }
+ </AriaDescribed>
+ </AriaDescriptionGroup>,
+ <AppButton.RedButton key="proceed" onClick={acknowledgeOutdatedVersion}>
+ {messages.pgettext('support-view', 'Continue anyway')}
+ </AppButton.RedButton>,
+ <AppButton.BlueButton key="cancel" onClick={outdatedVersionCancel}>
+ {messages.gettext('Cancel')}
+ </AppButton.BlueButton>,
+ ]}
+ close={onClose}
+ />
+ );
+}
+
+const useCollectLog = () => {
+ const { collectProblemReport } = useAppContext();
+ const accountHistory = useSelector((state) => state.account.accountHistory);
+
+ const collectLogPromise = useRef<Promise<string>>();
+
+ const collectLog = useCallback(async (): Promise<string> => {
+ 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<SetStateAction<SendState>>;
+ email: string;
+ setEmail: Dispatch<SetStateAction<string>>;
+ message: string;
+ setMessage: Dispatch<SetStateAction<string>>;
+ onSend: () => Promise<void>;
+};
+
+const ProblemReportContext = createContext<ProblemReportContextType | undefined>(undefined);
+
+const ProblemReportContextProvider = ({ children }: { children: ReactNode }) => {
+ const { sendProblemReport } = useAppContext();
+ const { clearReportForm, saveReportForm } = useActions(support);
- private handleEditMessage = () => {
- this.setState({ sendState: SendState.initial });
- };
+ const { email: defaultEmail, message: defaultMessage } = useSelector((state) => state.support);
- private performWithActionsDisabled(work: () => Promise<void>) {
- this.setState({ disableActions: true }, async () => {
+ 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 (error) {
+ 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 {
- await work();
- } catch {
- // TODO: handle error
+ setSendState(SendState.sending);
+ await sendReport();
+ } catch (error) {
+ // No-op
}
- this.setState({ disableActions: false });
- });
+ }
+ }, [email]);
+
+ /**
+ * 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 <ProblemReportContext.Provider value={value}>{children}</ProblemReportContext.Provider>;
+};
+
+const useProblemReportContext = () => {
+ const context = useContext(ProblemReportContext);
+ if (!context) {
+ throw new Error('useProblemReportContext must be used within a ProblemReportContextProvider');
}
-}
+ return context;
+};
diff --git a/gui/src/renderer/containers/ProblemReportPage.tsx b/gui/src/renderer/containers/ProblemReportPage.tsx
deleted file mode 100644
index 1c405134c8..0000000000
--- a/gui/src/renderer/containers/ProblemReportPage.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import { connect } from 'react-redux';
-import { bindActionCreators } from 'redux';
-
-import ProblemReport from '../components/ProblemReport';
-import withAppContext, { IAppContext } from '../context';
-import { IHistoryProps, withHistory } from '../lib/history';
-import { IReduxState, ReduxDispatch } from '../redux/store';
-import supportActions from '../redux/support/actions';
-
-const mapStateToProps = (state: IReduxState) => ({
- defaultEmail: state.support.email,
- defaultMessage: state.support.message,
- accountHistory: state.account.accountHistory,
- isOffline: state.connection.isBlocked,
- outdatedVersion: state.version.suggestedUpgrade ? true : false,
- suggestedIsBeta: state.version.suggestedIsBeta ?? false,
-});
-
-const mapDispatchToProps = (dispatch: ReduxDispatch, props: IAppContext & IHistoryProps) => {
- const { saveReportForm, clearReportForm } = bindActionCreators(supportActions, dispatch);
-
- return {
- onClose() {
- props.history.pop();
- },
- viewLog(id: string) {
- void props.app.viewLog(id);
- },
- saveReportForm,
- clearReportForm,
- collectProblemReport: props.app.collectProblemReport,
- sendProblemReport: props.app.sendProblemReport,
- onExternalLink: (url: string) => props.app.openUrl(url),
- };
-};
-
-export default withAppContext(
- withHistory(connect(mapStateToProps, mapDispatchToProps)(ProblemReport)),
-);