Merge branch 'network-error' into 'main'

Restore Network Error column

Closes #1591

See merge request soapbox-pub/soapbox!2857
environments/review-main-yi2y9f/deployments/4267
Alex Gleason 10 months ago
commit f56abb22a1

@ -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 = () => {
</Stack>
<HStack space={2} alignItems='center' className='flex-none'>
{soapbox.gdprUrl && (
<a href={soapbox.gdprUrl} tabIndex={-1} className='inline-flex'>
{gdprUrl && (
<a href={gdprUrl} tabIndex={-1} className='inline-flex'>
<Button theme='secondary'>
<FormattedMessage id='gdpr.learn_more' defaultMessage='Learn more' />
</Button>

@ -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<IErrorColumn> = ({ error, onRetry = () => location.reload() }) => {
const intl = useIntl();
const handleRetry = () => {
onRetry?.();
};
if (!isNetworkError(error)) {
throw error;
}
return (
<Column label={intl.formatMessage(messages.title)}>
<Stack space={4} alignItems='center' justifyContent='center' className='min-h-[160px] rounded-lg p-10'>
<IconButton
iconClassName='h-10 w-10'
title={intl.formatMessage(messages.retry)}
src={require('@tabler/icons/refresh.svg')}
onClick={handleRetry}
/>
<Text align='center' theme='muted'>{intl.formatMessage(messages.body)}</Text>
</Stack>
</Column>
);
};
export default ErrorColumn;

@ -329,7 +329,7 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
<WrappedRoute path='/developers/timeline' developerOnly page={DefaultPage} component={TestTimeline} content={children} />
<WrappedRoute path='/developers/sw' developerOnly page={DefaultPage} component={ServiceWorkerInfo} content={children} />
<WrappedRoute path='/developers' page={DefaultPage} component={Developers} content={children} />
<WrappedRoute path='/error/network' developerOnly page={EmptyPage} component={lazy(() => Promise.reject())} content={children} />
<WrappedRoute path='/error/network' developerOnly page={EmptyPage} component={lazy(() => Promise.reject(new TypeError('Failed to fetch dynamically imported module: TEST')))} content={children} />
<WrappedRoute path='/error' developerOnly page={EmptyPage} component={IntentionalError} content={children} />
{hasCrypto && <WrappedRoute path='/donate/crypto' publicRoute page={DefaultPage} component={CryptoDonate} content={children} />}

@ -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<IWrappedRoute> = ({
const renderComponent = ({ match }: RouteComponentProps) => {
if (Page) {
return (
<Suspense fallback={renderLoading()}>
<Page params={match.params} layout={layout} {...componentParams}>
<Component params={match.params} {...componentParams}>
{content}
</Component>
</Page>
</Suspense>
<ErrorBoundary FallbackComponent={FallbackError}>
<Suspense fallback={<FallbackLoading />}>
<Page params={match.params} layout={layout} {...componentParams}>
<Component params={match.params} {...componentParams}>
{content}
</Component>
</Page>
</Suspense>
</ErrorBoundary>
);
}
return (
<Suspense fallback={renderLoading()}>
<ColumnsArea layout={layout}>
<Component params={match.params} {...componentParams}>
{content}
</Component>
</ColumnsArea>
</Suspense>
<ErrorBoundary FallbackComponent={FallbackError}>
<Suspense fallback={<FallbackLoading />}>
<ColumnsArea layout={layout}>
<Component params={match.params} {...componentParams}>
{content}
</Component>
</ColumnsArea>
</Suspense>
</ErrorBoundary>
);
};
const renderWithLayout = (children: JSX.Element) => (
<>
<Layout.Main>
{children}
</Layout.Main>
<Layout.Aside />
</>
);
const renderLoading = () => renderWithLayout(<ColumnLoading />);
const renderForbidden = () => renderWithLayout(<ColumnForbidden />);
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<IWrappedRoute> = ({
if (!account) {
return loginRedirect();
} else {
return renderForbidden();
return <FallbackForbidden />;
}
}
return <Route {...rest} render={renderComponent} />;
};
interface IFallbackLayout {
children: JSX.Element;
}
const FallbackLayout: React.FC<IFallbackLayout> = ({ children }) => (
<>
<Layout.Main>
{children}
</Layout.Main>
<Layout.Aside />
</>
);
const FallbackLoading: React.FC = () => (
<FallbackLayout>
<ColumnLoading />
</FallbackLayout>
);
const FallbackForbidden: React.FC = () => (
<FallbackLayout>
<ColumnForbidden />
</FallbackLayout>
);
const FallbackError: React.FC<FallbackProps> = ({ error, resetErrorBoundary }) => {
const location = useLocation();
const firstUpdate = useRef(true);
useEffect(() => {
if (firstUpdate.current) {
firstUpdate.current = false;
} else {
resetErrorBoundary();
}
}, [location]);
return (
<FallbackLayout>
<ErrorColumn error={error} onRetry={resetErrorBoundary} />
</FallbackLayout>
);
};
export {
WrappedRoute,
};

@ -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 = () => {
<CompatRouter>
<ScrollContext shouldUpdateScroll={shouldUpdateScroll}>
<Switch>
{(!me && redirectRootNoLogin) && (
{(!isLoggedIn && redirectRootNoLogin) && (
<Redirect exact from='/' to={redirectRootNoLogin} />
)}
@ -73,9 +74,11 @@ const SoapboxMount = () => {
<ModalContainer />
</Suspense>
<Suspense>
<GdprBanner />
</Suspense>
{(gdpr && !isLoggedIn) && (
<Suspense>
<GdprBanner />
</Suspense>
)}
<div id='toaster'>
<Toaster

@ -198,6 +198,9 @@
"birthdays_modal.empty": "None of your friends have birthday today.",
"boost_modal.combo": "You can press {combo} to skip this next time",
"boost_modal.title": "Repost?",
"bundle_column_error.body": "Something went wrong while loading this page.",
"bundle_column_error.retry": "Try again",
"bundle_column_error.title": "Network error",
"card.back.label": "Back",
"chat.actions.send": "Send",
"chat.failed_to_send": "Message failed to send.",

@ -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 };

Loading…
Cancel
Save