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');