diff --git a/src/containers/soapbox.tsx b/src/containers/soapbox.tsx
deleted file mode 100644
index d60f8eec0..000000000
--- a/src/containers/soapbox.tsx
+++ /dev/null
@@ -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 = () => (
- }>
-
-
- );
-
- /** Render the auth layout or UI. */
- const renderSwitch = () => (
-
- {(!me && redirectRootNoLogin) && (
-
- )}
-
-
-
- );
-
- /** Render the onboarding flow or UI. */
- const renderBody = () => {
- if (showOnboarding) {
- return renderOnboarding();
- } else {
- return renderSwitch();
- }
- };
-
- return (
-
-
-
-
-
- }
- />
-
-
-
- {renderBody()}
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-interface ISoapboxLoad {
- children: React.ReactNode;
-}
-
-/** Initial data loader. */
-const SoapboxLoad: React.FC = ({ 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>({});
- 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 ;
- }
-
- return (
-
- {children}
-
- );
-};
-
-interface ISoapboxHead {
- children: React.ReactNode;
-}
-
-/** Injects metadata into site head with Helmet. */
-const SoapboxHead: React.FC = ({ 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 (
- <>
-
-
-
- {themeCss && }
- {darkMode && }
-
-
-
- {children}
- >
- );
-};
-
-/** The root React node of the application. */
-const Soapbox: React.FC = () => {
- return (
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default Soapbox;
diff --git a/src/features/chats/components/chat-message.tsx b/src/features/chats/components/chat-message.tsx
index 9b65eabf9..c4134a48b 100644
--- a/src/features/chats/components/chat-message.tsx
+++ b/src/features/chats/components/chat-message.tsx
@@ -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';
diff --git a/src/features/quotes/index.tsx b/src/features/quotes/index.tsx
index 6ba38fe10..7b0d057fe 100644
--- a/src/features/quotes/index.tsx
+++ b/src/features/quotes/index.tsx
@@ -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';
diff --git a/src/init/soapbox-head.tsx b/src/init/soapbox-head.tsx
new file mode 100644
index 000000000..0039404e0
--- /dev/null
+++ b/src/init/soapbox-head.tsx
@@ -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 = ({ 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 (
+ <>
+
+
+
+ {themeCss && }
+ {darkMode && }
+
+
+
+ {children}
+ >
+ );
+};
+
+export default SoapboxHead;
diff --git a/src/init/soapbox-load.tsx b/src/init/soapbox-load.tsx
new file mode 100644
index 000000000..4c88f2a7e
--- /dev/null
+++ b/src/init/soapbox-load.tsx
@@ -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 = ({ 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>({});
+ 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 ;
+ }
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default SoapboxLoad;
diff --git a/src/init/soapbox-mount.tsx b/src/init/soapbox-mount.tsx
new file mode 100644
index 000000000..bbc911691
--- /dev/null
+++ b/src/init/soapbox-mount.tsx
@@ -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 = () => (
+ }>
+
+
+ );
+
+ /** Render the auth layout or UI. */
+ const renderSwitch = () => (
+
+ {(!me && redirectRootNoLogin) && (
+
+ )}
+
+
+
+ );
+
+ /** Render the onboarding flow or UI. */
+ const renderBody = () => {
+ if (showOnboarding) {
+ return renderOnboarding();
+ } else {
+ return renderSwitch();
+ }
+ };
+
+ return (
+
+
+
+
+
+ }
+ />
+
+
+
+ {renderBody()}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default SoapboxMount;
diff --git a/src/init/soapbox.tsx b/src/init/soapbox.tsx
new file mode 100644
index 000000000..b63e42f3b
--- /dev/null
+++ b/src/init/soapbox.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Soapbox;
diff --git a/src/main.tsx b/src/main.tsx
index 33b325dab..9073b6bf6 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -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');