Merge branch 'chunk' into 'main'
Improve chunking, reduce entrypoint to 200kb See merge request soapbox-pub/soapbox!2780
This commit is contained in:
commit
690c678e80
8 changed files with 290 additions and 261 deletions
|
@ -1,258 +0,0 @@
|
|||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import clsx from 'clsx';
|
||||
import React, { useState, useEffect, Suspense } from 'react';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { Provider } from 'react-redux';
|
||||
import { BrowserRouter, Switch, Redirect, Route } from 'react-router-dom';
|
||||
import { CompatRouter } from 'react-router-dom-v5-compat';
|
||||
// @ts-ignore: it doesn't have types
|
||||
import { ScrollContext } from 'react-router-scroll-4';
|
||||
|
||||
import { loadInstance } from 'soapbox/actions/instance';
|
||||
import { fetchMe } from 'soapbox/actions/me';
|
||||
import { loadSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||
import * as BuildConfig from 'soapbox/build-config';
|
||||
import GdprBanner from 'soapbox/components/gdpr-banner';
|
||||
import Helmet from 'soapbox/components/helmet';
|
||||
import LoadingScreen from 'soapbox/components/loading-screen';
|
||||
import { StatProvider } from 'soapbox/contexts/stat-context';
|
||||
import EmbeddedStatus from 'soapbox/features/embedded-status';
|
||||
import {
|
||||
ModalContainer,
|
||||
OnboardingWizard,
|
||||
} from 'soapbox/features/ui/util/async-components';
|
||||
import { createGlobals } from 'soapbox/globals';
|
||||
import {
|
||||
useAppSelector,
|
||||
useAppDispatch,
|
||||
useOwnAccount,
|
||||
useSentry,
|
||||
useSettings,
|
||||
useSoapboxConfig,
|
||||
useTheme,
|
||||
useLocale,
|
||||
} from 'soapbox/hooks';
|
||||
import MESSAGES from 'soapbox/messages';
|
||||
import { normalizeSoapboxConfig } from 'soapbox/normalizers';
|
||||
import { queryClient } from 'soapbox/queries/client';
|
||||
import { useCachedLocationHandler } from 'soapbox/utils/redirect';
|
||||
import { generateThemeCss } from 'soapbox/utils/theme';
|
||||
|
||||
import { checkOnboardingStatus } from '../actions/onboarding';
|
||||
import { preload } from '../actions/preload';
|
||||
import ErrorBoundary from '../components/error-boundary';
|
||||
import UI from '../features/ui';
|
||||
import { store } from '../store';
|
||||
|
||||
// Configure global functions for developers
|
||||
createGlobals(store);
|
||||
|
||||
// Preload happens synchronously
|
||||
store.dispatch(preload() as any);
|
||||
|
||||
// This happens synchronously
|
||||
store.dispatch(checkOnboardingStatus() as any);
|
||||
|
||||
/** Load initial data from the backend */
|
||||
const loadInitial = () => {
|
||||
// @ts-ignore
|
||||
return async(dispatch, getState) => {
|
||||
// Await for authenticated fetch
|
||||
await dispatch(fetchMe());
|
||||
// Await for feature detection
|
||||
await dispatch(loadInstance());
|
||||
// Await for configuration
|
||||
await dispatch(loadSoapboxConfig());
|
||||
};
|
||||
};
|
||||
|
||||
/** Highest level node with the Redux store. */
|
||||
const SoapboxMount = () => {
|
||||
useCachedLocationHandler();
|
||||
|
||||
const me = useAppSelector(state => state.me);
|
||||
const { account } = useOwnAccount();
|
||||
const soapboxConfig = useSoapboxConfig();
|
||||
|
||||
const needsOnboarding = useAppSelector(state => state.onboarding.needsOnboarding);
|
||||
const showOnboarding = account && needsOnboarding;
|
||||
const { redirectRootNoLogin } = soapboxConfig;
|
||||
|
||||
// @ts-ignore: I don't actually know what these should be, lol
|
||||
const shouldUpdateScroll = (prevRouterProps, { location }) => {
|
||||
return !(location.state?.soapboxModalKey && location.state?.soapboxModalKey !== prevRouterProps?.location?.state?.soapboxModalKey);
|
||||
};
|
||||
|
||||
/** Render the onboarding flow. */
|
||||
const renderOnboarding = () => (
|
||||
<Suspense fallback={<LoadingScreen />}>
|
||||
<OnboardingWizard />
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
/** Render the auth layout or UI. */
|
||||
const renderSwitch = () => (
|
||||
<Switch>
|
||||
{(!me && redirectRootNoLogin) && (
|
||||
<Redirect exact from='/' to={redirectRootNoLogin} />
|
||||
)}
|
||||
|
||||
<Route path='/' component={UI} />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
/** Render the onboarding flow or UI. */
|
||||
const renderBody = () => {
|
||||
if (showOnboarding) {
|
||||
return renderOnboarding();
|
||||
} else {
|
||||
return renderSwitch();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<BrowserRouter basename={BuildConfig.FE_SUBDIRECTORY}>
|
||||
<CompatRouter>
|
||||
<ScrollContext shouldUpdateScroll={shouldUpdateScroll}>
|
||||
<Switch>
|
||||
<Route
|
||||
path='/embed/:statusId'
|
||||
render={(props) => <EmbeddedStatus params={props.match.params} />}
|
||||
/>
|
||||
<Redirect from='/@:username/:statusId/embed' to='/embed/:statusId' />
|
||||
|
||||
<Route>
|
||||
{renderBody()}
|
||||
|
||||
<ModalContainer />
|
||||
<GdprBanner />
|
||||
|
||||
<div id='toaster'>
|
||||
<Toaster
|
||||
position='top-right'
|
||||
containerClassName='top-10'
|
||||
containerStyle={{ top: 75 }}
|
||||
/>
|
||||
</div>
|
||||
</Route>
|
||||
</Switch>
|
||||
</ScrollContext>
|
||||
</CompatRouter>
|
||||
</BrowserRouter>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
interface ISoapboxLoad {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/** Initial data loader. */
|
||||
const SoapboxLoad: React.FC<ISoapboxLoad> = ({ children }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const me = useAppSelector(state => state.me);
|
||||
const { account } = useOwnAccount();
|
||||
const swUpdating = useAppSelector(state => state.meta.swUpdating);
|
||||
const { locale } = useLocale();
|
||||
|
||||
const [messages, setMessages] = useState<Record<string, string>>({});
|
||||
const [localeLoading, setLocaleLoading] = useState(true);
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
|
||||
/** Whether to display a loading indicator. */
|
||||
const showLoading = [
|
||||
me === null,
|
||||
me && !account,
|
||||
!isLoaded,
|
||||
localeLoading,
|
||||
swUpdating,
|
||||
].some(Boolean);
|
||||
|
||||
// Load the user's locale
|
||||
useEffect(() => {
|
||||
MESSAGES[locale]().then(messages => {
|
||||
setMessages(messages);
|
||||
setLocaleLoading(false);
|
||||
}).catch(() => { });
|
||||
}, [locale]);
|
||||
|
||||
// Load initial data from the API
|
||||
useEffect(() => {
|
||||
dispatch(loadInitial()).then(() => {
|
||||
setIsLoaded(true);
|
||||
}).catch(() => {
|
||||
setIsLoaded(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// intl is part of loading.
|
||||
// It's important nothing in here depends on intl.
|
||||
if (showLoading) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
return (
|
||||
<IntlProvider locale={locale} messages={messages}>
|
||||
{children}
|
||||
</IntlProvider>
|
||||
);
|
||||
};
|
||||
|
||||
interface ISoapboxHead {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/** Injects metadata into site head with Helmet. */
|
||||
const SoapboxHead: React.FC<ISoapboxHead> = ({ children }) => {
|
||||
const { locale, direction } = useLocale();
|
||||
const settings = useSettings();
|
||||
const soapboxConfig = useSoapboxConfig();
|
||||
|
||||
const demo = !!settings.get('demo');
|
||||
const darkMode = useTheme() === 'dark';
|
||||
const themeCss = generateThemeCss(demo ? normalizeSoapboxConfig({ brandColor: '#0482d8' }) : soapboxConfig);
|
||||
|
||||
const bodyClass = clsx('h-full bg-white text-base dark:bg-gray-800', {
|
||||
'no-reduce-motion': !settings.get('reduceMotion'),
|
||||
'underline-links': settings.get('underlineLinks'),
|
||||
'demetricator': settings.get('demetricator'),
|
||||
});
|
||||
|
||||
useSentry(soapboxConfig.sentryDsn);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<html lang={locale} className={clsx('h-full', { dark: darkMode })} />
|
||||
<body className={bodyClass} dir={direction} />
|
||||
{themeCss && <style id='theme' type='text/css'>{`:root{${themeCss}}`}</style>}
|
||||
{darkMode && <style type='text/css'>{':root { color-scheme: dark; }'}</style>}
|
||||
<meta name='theme-color' content={soapboxConfig.brandColor} />
|
||||
</Helmet>
|
||||
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/** The root React node of the application. */
|
||||
const Soapbox: React.FC = () => {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<StatProvider>
|
||||
<SoapboxHead>
|
||||
<SoapboxLoad>
|
||||
<SoapboxMount />
|
||||
</SoapboxLoad>
|
||||
</SoapboxHead>
|
||||
</StatProvider>
|
||||
</QueryClientProvider>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default Soapbox;
|
|
@ -1,7 +1,7 @@
|
|||
import { useMutation } from '@tanstack/react-query';
|
||||
import clsx from 'clsx';
|
||||
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
||||
import { escape } from 'lodash';
|
||||
import escape from 'lodash/escape';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||
import { debounce } from 'lodash';
|
||||
import debounce from 'lodash/debounce';
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
|
53
src/init/soapbox-head.tsx
Normal file
53
src/init/soapbox-head.tsx
Normal file
|
@ -0,0 +1,53 @@
|
|||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
useSentry,
|
||||
useSettings,
|
||||
useSoapboxConfig,
|
||||
useTheme,
|
||||
useLocale,
|
||||
} from 'soapbox/hooks';
|
||||
import { normalizeSoapboxConfig } from 'soapbox/normalizers';
|
||||
import { generateThemeCss } from 'soapbox/utils/theme';
|
||||
|
||||
const Helmet = React.lazy(() => import('soapbox/components/helmet'));
|
||||
|
||||
interface ISoapboxHead {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/** Injects metadata into site head with Helmet. */
|
||||
const SoapboxHead: React.FC<ISoapboxHead> = ({ children }) => {
|
||||
const { locale, direction } = useLocale();
|
||||
const settings = useSettings();
|
||||
const soapboxConfig = useSoapboxConfig();
|
||||
|
||||
const demo = !!settings.get('demo');
|
||||
const darkMode = useTheme() === 'dark';
|
||||
const themeCss = generateThemeCss(demo ? normalizeSoapboxConfig({ brandColor: '#0482d8' }) : soapboxConfig);
|
||||
|
||||
const bodyClass = clsx('h-full bg-white text-base dark:bg-gray-800', {
|
||||
'no-reduce-motion': !settings.get('reduceMotion'),
|
||||
'underline-links': settings.get('underlineLinks'),
|
||||
'demetricator': settings.get('demetricator'),
|
||||
});
|
||||
|
||||
useSentry(soapboxConfig.sentryDsn);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<html lang={locale} className={clsx('h-full', { dark: darkMode })} />
|
||||
<body className={bodyClass} dir={direction} />
|
||||
{themeCss && <style id='theme' type='text/css'>{`:root{${themeCss}}`}</style>}
|
||||
{darkMode && <style type='text/css'>{':root { color-scheme: dark; }'}</style>}
|
||||
<meta name='theme-color' content={soapboxConfig.brandColor} />
|
||||
</Helmet>
|
||||
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SoapboxHead;
|
85
src/init/soapbox-load.tsx
Normal file
85
src/init/soapbox-load.tsx
Normal file
|
@ -0,0 +1,85 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import { loadInstance } from 'soapbox/actions/instance';
|
||||
import { fetchMe } from 'soapbox/actions/me';
|
||||
import { loadSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||
import LoadingScreen from 'soapbox/components/loading-screen';
|
||||
import {
|
||||
useAppSelector,
|
||||
useAppDispatch,
|
||||
useOwnAccount,
|
||||
useLocale,
|
||||
} from 'soapbox/hooks';
|
||||
import MESSAGES from 'soapbox/messages';
|
||||
|
||||
/** Load initial data from the backend */
|
||||
const loadInitial = () => {
|
||||
// @ts-ignore
|
||||
return async(dispatch, getState) => {
|
||||
// Await for authenticated fetch
|
||||
await dispatch(fetchMe());
|
||||
// Await for feature detection
|
||||
await dispatch(loadInstance());
|
||||
// Await for configuration
|
||||
await dispatch(loadSoapboxConfig());
|
||||
};
|
||||
};
|
||||
|
||||
interface ISoapboxLoad {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/** Initial data loader. */
|
||||
const SoapboxLoad: React.FC<ISoapboxLoad> = ({ children }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const me = useAppSelector(state => state.me);
|
||||
const { account } = useOwnAccount();
|
||||
const swUpdating = useAppSelector(state => state.meta.swUpdating);
|
||||
const { locale } = useLocale();
|
||||
|
||||
const [messages, setMessages] = useState<Record<string, string>>({});
|
||||
const [localeLoading, setLocaleLoading] = useState(true);
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
|
||||
/** Whether to display a loading indicator. */
|
||||
const showLoading = [
|
||||
me === null,
|
||||
me && !account,
|
||||
!isLoaded,
|
||||
localeLoading,
|
||||
swUpdating,
|
||||
].some(Boolean);
|
||||
|
||||
// Load the user's locale
|
||||
useEffect(() => {
|
||||
MESSAGES[locale]().then(messages => {
|
||||
setMessages(messages);
|
||||
setLocaleLoading(false);
|
||||
}).catch(() => { });
|
||||
}, [locale]);
|
||||
|
||||
// Load initial data from the API
|
||||
useEffect(() => {
|
||||
dispatch(loadInitial()).then(() => {
|
||||
setIsLoaded(true);
|
||||
}).catch(() => {
|
||||
setIsLoaded(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// intl is part of loading.
|
||||
// It's important nothing in here depends on intl.
|
||||
if (showLoading) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
return (
|
||||
<IntlProvider locale={locale} messages={messages}>
|
||||
{children}
|
||||
</IntlProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default SoapboxLoad;
|
105
src/init/soapbox-mount.tsx
Normal file
105
src/init/soapbox-mount.tsx
Normal file
|
@ -0,0 +1,105 @@
|
|||
import React, { Suspense } from 'react';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import { BrowserRouter, Switch, Redirect, Route } from 'react-router-dom';
|
||||
import { CompatRouter } from 'react-router-dom-v5-compat';
|
||||
// @ts-ignore: it doesn't have types
|
||||
import { ScrollContext } from 'react-router-scroll-4';
|
||||
|
||||
import * as BuildConfig from 'soapbox/build-config';
|
||||
import LoadingScreen from 'soapbox/components/loading-screen';
|
||||
import {
|
||||
ModalContainer,
|
||||
OnboardingWizard,
|
||||
} from 'soapbox/features/ui/util/async-components';
|
||||
import {
|
||||
useAppSelector,
|
||||
useOwnAccount,
|
||||
useSoapboxConfig,
|
||||
} from 'soapbox/hooks';
|
||||
import { useCachedLocationHandler } from 'soapbox/utils/redirect';
|
||||
|
||||
import ErrorBoundary from '../components/error-boundary';
|
||||
|
||||
const GdprBanner = React.lazy(() => import('soapbox/components/gdpr-banner'));
|
||||
const EmbeddedStatus = React.lazy(() => import('soapbox/features/embedded-status'));
|
||||
const UI = React.lazy(() => import('soapbox/features/ui'));
|
||||
|
||||
/** Highest level node with the Redux store. */
|
||||
const SoapboxMount = () => {
|
||||
useCachedLocationHandler();
|
||||
|
||||
const me = useAppSelector(state => state.me);
|
||||
const { account } = useOwnAccount();
|
||||
const soapboxConfig = useSoapboxConfig();
|
||||
|
||||
const needsOnboarding = useAppSelector(state => state.onboarding.needsOnboarding);
|
||||
const showOnboarding = account && needsOnboarding;
|
||||
const { redirectRootNoLogin } = soapboxConfig;
|
||||
|
||||
// @ts-ignore: I don't actually know what these should be, lol
|
||||
const shouldUpdateScroll = (prevRouterProps, { location }) => {
|
||||
return !(location.state?.soapboxModalKey && location.state?.soapboxModalKey !== prevRouterProps?.location?.state?.soapboxModalKey);
|
||||
};
|
||||
|
||||
/** Render the onboarding flow. */
|
||||
const renderOnboarding = () => (
|
||||
<Suspense fallback={<LoadingScreen />}>
|
||||
<OnboardingWizard />
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
/** Render the auth layout or UI. */
|
||||
const renderSwitch = () => (
|
||||
<Switch>
|
||||
{(!me && redirectRootNoLogin) && (
|
||||
<Redirect exact from='/' to={redirectRootNoLogin} />
|
||||
)}
|
||||
|
||||
<Route path='/' component={UI} />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
/** Render the onboarding flow or UI. */
|
||||
const renderBody = () => {
|
||||
if (showOnboarding) {
|
||||
return renderOnboarding();
|
||||
} else {
|
||||
return renderSwitch();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<BrowserRouter basename={BuildConfig.FE_SUBDIRECTORY}>
|
||||
<CompatRouter>
|
||||
<ScrollContext shouldUpdateScroll={shouldUpdateScroll}>
|
||||
<Switch>
|
||||
<Route
|
||||
path='/embed/:statusId'
|
||||
render={(props) => <EmbeddedStatus params={props.match.params} />}
|
||||
/>
|
||||
<Redirect from='/@:username/:statusId/embed' to='/embed/:statusId' />
|
||||
|
||||
<Route>
|
||||
{renderBody()}
|
||||
|
||||
<ModalContainer />
|
||||
<GdprBanner />
|
||||
|
||||
<div id='toaster'>
|
||||
<Toaster
|
||||
position='top-right'
|
||||
containerClassName='top-10'
|
||||
containerStyle={{ top: 75 }}
|
||||
/>
|
||||
</div>
|
||||
</Route>
|
||||
</Switch>
|
||||
</ScrollContext>
|
||||
</CompatRouter>
|
||||
</BrowserRouter>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
export default SoapboxMount;
|
43
src/init/soapbox.tsx
Normal file
43
src/init/soapbox.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { StatProvider } from 'soapbox/contexts/stat-context';
|
||||
import { createGlobals } from 'soapbox/globals';
|
||||
import { queryClient } from 'soapbox/queries/client';
|
||||
|
||||
import { checkOnboardingStatus } from '../actions/onboarding';
|
||||
import { preload } from '../actions/preload';
|
||||
import { store } from '../store';
|
||||
|
||||
const SoapboxHead = React.lazy(() => import('./soapbox-head'));
|
||||
const SoapboxLoad = React.lazy(() => import('./soapbox-load'));
|
||||
const SoapboxMount = React.lazy(() => import('./soapbox-mount'));
|
||||
|
||||
// Configure global functions for developers
|
||||
createGlobals(store);
|
||||
|
||||
// Preload happens synchronously
|
||||
store.dispatch(preload() as any);
|
||||
|
||||
// This happens synchronously
|
||||
store.dispatch(checkOnboardingStatus() as any);
|
||||
|
||||
/** The root React node of the application. */
|
||||
const Soapbox: React.FC = () => {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<StatProvider>
|
||||
<SoapboxHead>
|
||||
<SoapboxLoad>
|
||||
<SoapboxMount />
|
||||
</SoapboxLoad>
|
||||
</SoapboxHead>
|
||||
</StatProvider>
|
||||
</QueryClientProvider>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default Soapbox;
|
|
@ -22,10 +22,11 @@ import './styles/application.scss';
|
|||
import './styles/tailwind.css';
|
||||
|
||||
import './precheck';
|
||||
import { default as Soapbox } from './containers/soapbox';
|
||||
import ready from './ready';
|
||||
import { registerSW } from './utils/sw';
|
||||
|
||||
const Soapbox = React.lazy(() => import('./init/soapbox'));
|
||||
|
||||
if (BuildConfig.NODE_ENV === 'production') {
|
||||
printConsoleWarning();
|
||||
registerSW('/sw.js');
|
||||
|
|
Loading…
Reference in a new issue