63
packages/pl-fe/src/init/soapbox-head.tsx
Normal file
63
packages/pl-fe/src/init/soapbox-head.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import {
|
||||
useSettings,
|
||||
useSoapboxConfig,
|
||||
useTheme,
|
||||
useLocale,
|
||||
useAppSelector,
|
||||
} from 'soapbox/hooks';
|
||||
import { userTouching } from 'soapbox/is-mobile';
|
||||
import { normalizeSoapboxConfig } from 'soapbox/normalizers';
|
||||
import { startSentry } from 'soapbox/sentry';
|
||||
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 { demo, reduceMotion, underlineLinks, demetricator, systemFont } = useSettings();
|
||||
const soapboxConfig = useSoapboxConfig();
|
||||
const theme = useTheme();
|
||||
|
||||
const withModals = useAppSelector((state) => !state.modals.isEmpty() || (state.dropdown_menu.isOpen && userTouching.matches));
|
||||
|
||||
const themeCss = generateThemeCss(demo ? normalizeSoapboxConfig({ brandColor: '#0482d8' }) : soapboxConfig);
|
||||
const dsn = soapboxConfig.sentryDsn;
|
||||
|
||||
const bodyClass = clsx('h-full bg-white text-base antialiased black:bg-black dark:bg-gray-800', {
|
||||
'no-reduce-motion': !reduceMotion,
|
||||
'underline-links': underlineLinks,
|
||||
'demetricator': demetricator,
|
||||
'system-font': systemFont,
|
||||
'overflow-hidden': withModals,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (dsn) {
|
||||
startSentry(dsn).catch(console.error);
|
||||
}
|
||||
}, [dsn]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<html lang={locale} className={clsx('h-full', { 'dark': theme === 'dark', 'dark black': theme === 'black' })} />
|
||||
<body className={bodyClass} dir={direction} />
|
||||
{themeCss && <style id='theme' type='text/css'>{`:root{${themeCss}}`}</style>}
|
||||
{['dark', 'black'].includes(theme) && <style type='text/css'>{':root { color-scheme: dark; }'}</style>}
|
||||
<meta name='theme-color' content={soapboxConfig.brandColor} />
|
||||
</Helmet>
|
||||
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { SoapboxHead as default };
|
||||
85
packages/pl-fe/src/init/soapbox-load.tsx
Normal file
85
packages/pl-fe/src/init/soapbox-load.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import { fetchInstance } 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(fetchInstance());
|
||||
// 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 { SoapboxLoad as default };
|
||||
95
packages/pl-fe/src/init/soapbox-mount.tsx
Normal file
95
packages/pl-fe/src/init/soapbox-mount.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
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 SiteErrorBoundary from 'soapbox/components/site-error-boundary';
|
||||
import { ModalRoot, OnboardingWizard } from 'soapbox/features/ui/util/async-components';
|
||||
import {
|
||||
useAppSelector,
|
||||
useLoggedIn,
|
||||
useOwnAccount,
|
||||
useSoapboxConfig,
|
||||
} from 'soapbox/hooks';
|
||||
import { useCachedLocationHandler } from 'soapbox/utils/redirect';
|
||||
|
||||
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 { isLoggedIn } = useLoggedIn();
|
||||
const { account } = useOwnAccount();
|
||||
const soapboxConfig = useSoapboxConfig();
|
||||
|
||||
const needsOnboarding = useAppSelector(state => state.onboarding.needsOnboarding);
|
||||
const showOnboarding = account && needsOnboarding;
|
||||
const { redirectRootNoLogin, gdpr } = soapboxConfig;
|
||||
|
||||
// @ts-ignore: I don't actually know what these should be, lol
|
||||
const shouldUpdateScroll = (prevRouterProps, { location }) =>
|
||||
!(location.state?.soapboxModalKey && location.state?.soapboxModalKey !== prevRouterProps?.location?.state?.soapboxModalKey)
|
||||
&& !(location.state?.soapboxDropdownKey && location.state?.soapboxDropdownKey !== prevRouterProps?.location?.state?.soapboxDropdownKey);
|
||||
|
||||
return (
|
||||
<SiteErrorBoundary>
|
||||
<BrowserRouter basename={BuildConfig.FE_SUBDIRECTORY}>
|
||||
<CompatRouter>
|
||||
<ScrollContext shouldUpdateScroll={shouldUpdateScroll}>
|
||||
<Switch>
|
||||
{(!isLoggedIn && redirectRootNoLogin) && (
|
||||
<Redirect exact from='/' to={redirectRootNoLogin} />
|
||||
)}
|
||||
|
||||
<Route
|
||||
path='/embed/:statusId'
|
||||
render={(props) => (
|
||||
<Suspense>
|
||||
<EmbeddedStatus params={props.match.params} />
|
||||
</Suspense>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Redirect from='/@:username/:statusId/embed' to='/embed/:statusId' />
|
||||
|
||||
<Route>
|
||||
<Suspense fallback={<LoadingScreen />}>
|
||||
{showOnboarding
|
||||
? <OnboardingWizard />
|
||||
: <UI />
|
||||
}
|
||||
</Suspense>
|
||||
|
||||
<Suspense>
|
||||
<ModalRoot />
|
||||
</Suspense>
|
||||
|
||||
{(gdpr && !isLoggedIn) && (
|
||||
<Suspense>
|
||||
<GdprBanner />
|
||||
</Suspense>
|
||||
)}
|
||||
|
||||
<div id='toaster'>
|
||||
<Toaster
|
||||
position='top-right'
|
||||
containerClassName='top-4'
|
||||
/>
|
||||
</div>
|
||||
</Route>
|
||||
</Switch>
|
||||
</ScrollContext>
|
||||
</CompatRouter>
|
||||
</BrowserRouter>
|
||||
</SiteErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
export { SoapboxMount as default };
|
||||
41
packages/pl-fe/src/init/soapbox.tsx
Normal file
41
packages/pl-fe/src/init/soapbox.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
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';
|
||||
|
||||
import SoapboxHead from './soapbox-head';
|
||||
import SoapboxLoad from './soapbox-load';
|
||||
import SoapboxMount from './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 = () => (
|
||||
<Provider store={store}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<StatProvider>
|
||||
<SoapboxHead>
|
||||
<SoapboxLoad>
|
||||
<SoapboxMount />
|
||||
</SoapboxLoad>
|
||||
</SoapboxHead>
|
||||
</StatProvider>
|
||||
</QueryClientProvider>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
export { Soapbox as default };
|
||||
Reference in New Issue
Block a user