diff --git a/src/features/ui/components/error-column.tsx b/src/features/ui/components/error-column.tsx new file mode 100644 index 0000000000..39964cca84 --- /dev/null +++ b/src/features/ui/components/error-column.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import { Column, Stack, Text, IconButton } from 'soapbox/components/ui'; +import { isNetworkError } from 'soapbox/utils/errors'; + +const messages = defineMessages({ + title: { id: 'bundle_column_error.title', defaultMessage: 'Network error' }, + body: { id: 'bundle_column_error.body', defaultMessage: 'Something went wrong while loading this page.' }, + retry: { id: 'bundle_column_error.retry', defaultMessage: 'Try again' }, +}); + +interface IErrorColumn { + error: Error; + onRetry?: () => void; +} + +const ErrorColumn: React.FC = ({ error, onRetry = () => location.reload() }) => { + const intl = useIntl(); + + const handleRetry = () => { + onRetry?.(); + }; + + if (!isNetworkError(error)) { + throw error; + } + + return ( + + + + + {intl.formatMessage(messages.body)} + + + ); +}; + +export default ErrorColumn; diff --git a/src/features/ui/util/react-router-helpers.tsx b/src/features/ui/util/react-router-helpers.tsx index ba1895ae4a..92bbf28b43 100644 --- a/src/features/ui/util/react-router-helpers.tsx +++ b/src/features/ui/util/react-router-helpers.tsx @@ -1,4 +1,5 @@ -import React, { Suspense } from 'react'; +import React, { ComponentProps, Suspense } from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; import { Redirect, Route, useHistory, RouteProps, RouteComponentProps, match as MatchType } from 'react-router-dom'; import { Layout } from 'soapbox/components/ui'; @@ -7,6 +8,7 @@ import { useOwnAccount, useSettings } from 'soapbox/hooks'; import ColumnForbidden from '../components/column-forbidden'; import ColumnLoading from '../components/column-loading'; import ColumnsArea from '../components/columns-area'; +import ErrorColumn from '../components/error-column'; type PageProps = { params?: MatchType['params']; @@ -46,24 +48,28 @@ const WrappedRoute: React.FC = ({ const renderComponent = ({ match }: RouteComponentProps) => { if (Page) { return ( - - - - {content} - - - + + + + + {content} + + + + ); } return ( - - - - {content} - - - + + + + + {content} + + + + ); }; @@ -79,6 +85,7 @@ const WrappedRoute: React.FC = ({ const renderLoading = () => renderWithLayout(); const renderForbidden = () => renderWithLayout(); + const renderError = (props: ComponentProps) => renderWithLayout(); const loginRedirect = () => { const actualUrl = encodeURIComponent(`${history.location.pathname}${history.location.search}`); diff --git a/src/utils/errors.ts b/src/utils/errors.ts index 7c8d81bd1b..78f8db8bdb 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -200,4 +200,11 @@ const httpErrorMessages: { code: number; name: string; description: string }[] = }, ]; -export { buildErrorMessage, httpErrorMessages }; +/** Whether the error is caused by a JS chunk failing to load. */ +function isNetworkError(error: unknown): boolean { + return error instanceof Error + && error.name === 'TypeError' + && error.message.startsWith('Failed to fetch dynamically imported module: '); +} + +export { buildErrorMessage, httpErrorMessages, isNetworkError };