diff --git a/src/components/gdpr-banner.tsx b/src/components/gdpr-banner.tsx index 73d90684e..bcd4ed303 100644 --- a/src/components/gdpr-banner.tsx +++ b/src/components/gdpr-banner.tsx @@ -3,7 +3,7 @@ import React, { useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { Banner, Button, HStack, Stack, Text } from 'soapbox/components/ui'; -import { useAppSelector, useInstance, useSoapboxConfig } from 'soapbox/hooks'; +import { useInstance, useSoapboxConfig } from 'soapbox/hooks'; const acceptedGdpr = !!localStorage.getItem('soapbox:gdpr'); @@ -14,8 +14,7 @@ const GdprBanner: React.FC = () => { const [slideout, setSlideout] = useState(false); const instance = useInstance(); - const soapbox = useSoapboxConfig(); - const isLoggedIn = useAppSelector(state => !!state.me); + const { gdprUrl } = useSoapboxConfig(); const handleAccept = () => { localStorage.setItem('soapbox:gdpr', 'true'); @@ -23,9 +22,7 @@ const GdprBanner: React.FC = () => { setTimeout(() => setShown(true), 200); }; - const showBanner = soapbox.gdpr && !isLoggedIn && !shown; - - if (!showBanner) { + if (!shown) { return null; } @@ -47,8 +44,8 @@ const GdprBanner: React.FC = () => { - {soapbox.gdprUrl && ( - + {gdprUrl && ( + diff --git a/src/features/ui/components/error-column.tsx b/src/features/ui/components/error-column.tsx new file mode 100644 index 000000000..39964cca8 --- /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/index.tsx b/src/features/ui/index.tsx index 59857d52f..20f25c4a8 100644 --- a/src/features/ui/index.tsx +++ b/src/features/ui/index.tsx @@ -329,7 +329,7 @@ const SwitchingColumnsArea: React.FC = ({ children }) => - Promise.reject())} content={children} /> + Promise.reject(new TypeError('Failed to fetch dynamically imported module: TEST')))} content={children} /> {hasCrypto && } diff --git a/src/features/ui/util/react-router-helpers.tsx b/src/features/ui/util/react-router-helpers.tsx index ba1895ae4..0487ca825 100644 --- a/src/features/ui/util/react-router-helpers.tsx +++ b/src/features/ui/util/react-router-helpers.tsx @@ -1,5 +1,6 @@ -import React, { Suspense } from 'react'; -import { Redirect, Route, useHistory, RouteProps, RouteComponentProps, match as MatchType } from 'react-router-dom'; +import React, { Suspense, useEffect, useRef } from 'react'; +import { ErrorBoundary, type FallbackProps } from 'react-error-boundary'; +import { Redirect, Route, useHistory, RouteProps, RouteComponentProps, match as MatchType, useLocation } from 'react-router-dom'; import { Layout } from 'soapbox/components/ui'; import { useOwnAccount, useSettings } from 'soapbox/hooks'; @@ -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,40 +48,31 @@ const WrappedRoute: React.FC = ({ const renderComponent = ({ match }: RouteComponentProps) => { if (Page) { return ( - - - - {content} - - - + + }> + + + {content} + + + + ); } return ( - - - - {content} - - - + + }> + + + {content} + + + + ); }; - const renderWithLayout = (children: JSX.Element) => ( - <> - - {children} - - - - - ); - - const renderLoading = () => renderWithLayout(); - const renderForbidden = () => renderWithLayout(); - const loginRedirect = () => { const actualUrl = encodeURIComponent(`${history.location.pathname}${history.location.search}`); localStorage.setItem('soapbox:redirect_uri', actualUrl); @@ -97,13 +90,58 @@ const WrappedRoute: React.FC = ({ if (!account) { return loginRedirect(); } else { - return renderForbidden(); + return ; } } return ; }; +interface IFallbackLayout { + children: JSX.Element; +} + +const FallbackLayout: React.FC = ({ children }) => ( + <> + + {children} + + + + +); + +const FallbackLoading: React.FC = () => ( + + + +); + +const FallbackForbidden: React.FC = () => ( + + + +); + +const FallbackError: React.FC = ({ error, resetErrorBoundary }) => { + const location = useLocation(); + const firstUpdate = useRef(true); + + useEffect(() => { + if (firstUpdate.current) { + firstUpdate.current = false; + } else { + resetErrorBoundary(); + } + }, [location]); + + return ( + + + + ); +}; + export { WrappedRoute, }; diff --git a/src/init/soapbox-mount.tsx b/src/init/soapbox-mount.tsx index 06d394293..213c50494 100644 --- a/src/init/soapbox-mount.tsx +++ b/src/init/soapbox-mount.tsx @@ -14,6 +14,7 @@ import { } from 'soapbox/features/ui/util/async-components'; import { useAppSelector, + useLoggedIn, useOwnAccount, useSoapboxConfig, } from 'soapbox/hooks'; @@ -27,13 +28,13 @@ const UI = React.lazy(() => import('soapbox/features/ui')); const SoapboxMount = () => { useCachedLocationHandler(); - const me = useAppSelector(state => state.me); + const { isLoggedIn } = useLoggedIn(); const { account } = useOwnAccount(); const soapboxConfig = useSoapboxConfig(); const needsOnboarding = useAppSelector(state => state.onboarding.needsOnboarding); const showOnboarding = account && needsOnboarding; - const { redirectRootNoLogin } = soapboxConfig; + const { redirectRootNoLogin, gdpr } = soapboxConfig; // @ts-ignore: I don't actually know what these should be, lol const shouldUpdateScroll = (prevRouterProps, { location }) => { @@ -46,7 +47,7 @@ const SoapboxMount = () => { - {(!me && redirectRootNoLogin) && ( + {(!isLoggedIn && redirectRootNoLogin) && ( )} @@ -73,9 +74,11 @@ const SoapboxMount = () => { - - - + {(gdpr && !isLoggedIn) && ( + + + + )}