diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 308b0586f..910da28f6 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -2377,6 +2377,14 @@ "message": "No conversations found", "description": "Label shown when there are no conversations to compose to" }, + "Toast--error": { + "message": "An error has occurred", + "description": "Toast for general errors" + }, + "Toast--error--action": { + "message": "Submit log", + "description": "Label for the error toast button" + }, "Toast--failed-to-fetch-username": { "message": "Failed to fetch username. Check your connection and try again.", "description": "Shown if request to Signal servers to find username fails" diff --git a/ts/components/ErrorBoundary.tsx b/ts/components/ErrorBoundary.tsx new file mode 100644 index 000000000..2a15f2910 --- /dev/null +++ b/ts/components/ErrorBoundary.tsx @@ -0,0 +1,47 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { ReactNode } from 'react'; +import React from 'react'; + +import * as Errors from '../types/errors'; +import * as log from '../logging/log'; +import { ToastType } from '../state/ducks/toast'; + +export type Props = { + children: ReactNode; +}; + +export type State = { + error?: Error; +}; + +export class ErrorBoundary extends React.PureComponent { + constructor(props: Props) { + super(props); + + this.state = { error: undefined }; + } + + public static getDerivedStateFromError(error: Error): State { + log.error( + 'ErrorBoundary: captured rendering error', + Errors.toLogFormat(error) + ); + if (window.reduxActions) { + window.reduxActions.toast.showToast(ToastType.Error); + } + return { error }; + } + + public override render(): ReactNode { + const { error } = this.state; + const { children } = this.props; + + if (error) { + return null; + } + + return children; + } +} diff --git a/ts/components/ToastManager.tsx b/ts/components/ToastManager.tsx index 554087c8a..551ee9ac4 100644 --- a/ts/components/ToastManager.tsx +++ b/ts/components/ToastManager.tsx @@ -20,6 +20,21 @@ export const ToastManager = ({ i18n, toastType, }: PropsType): JSX.Element | null => { + if (toastType === ToastType.Error) { + return ( + window.showDebugLog(), + }} + > + {i18n('Toast--error')} + + ); + } + if (toastType === ToastType.MessageBodyTooLong) { return ; } diff --git a/ts/state/ducks/toast.ts b/ts/state/ducks/toast.ts index 86ef093b9..719223cc0 100644 --- a/ts/state/ducks/toast.ts +++ b/ts/state/ducks/toast.ts @@ -4,6 +4,7 @@ import { useBoundActions } from '../../hooks/useBoundActions'; export enum ToastType { + Error = 'Error', MessageBodyTooLong = 'MessageBodyTooLong', StoryReact = 'StoryReact', StoryReply = 'StoryReply', diff --git a/ts/state/smart/App.tsx b/ts/state/smart/App.tsx index 0b08d99d1..638a7b23b 100644 --- a/ts/state/smart/App.tsx +++ b/ts/state/smart/App.tsx @@ -33,13 +33,16 @@ import { getConversationsStoppingSend } from '../selectors/conversations'; import { getIsCustomizingPreferredReactions } from '../selectors/preferredReactions'; import { mapDispatchToProps } from '../actions'; import type { SafetyNumberProps } from '../../components/SafetyNumberChangeDialog'; +import { ErrorBoundary } from '../../components/ErrorBoundary'; const mapStateToProps = (state: StateType) => { + const i18n = getIntl(state); + return { ...state.app, conversationsStoppingSend: getConversationsStoppingSend(state), getPreferredBadge: getPreferredBadgeSelector(state), - i18n: getIntl(state), + i18n, localeMessages: getLocaleMessages(state), isCustomizingPreferredReactions: getIsCustomizingPreferredReactions(state), isMaximized: getIsMainWindowMaximized(state), @@ -57,9 +60,17 @@ const mapStateToProps = (state: StateType) => { ), isShowingStoriesView: shouldShowStoriesView(state), - renderStories: () => , + renderStories: () => ( + + + + ), selectedStoryData: getSelectedStoryData(state), - renderStoryViewer: () => , + renderStoryViewer: () => ( + + + + ), requestVerification: ( type: 'sms' | 'voice', number: string,