import clsx from 'clsx'; import React, { Suspense, lazy, useEffect, useRef } from 'react'; import { Redirect, Switch, useHistory, useLocation } from 'react-router-dom'; import { fetchConfig } from 'pl-fe/actions/admin'; import { fetchDraftStatuses } from 'pl-fe/actions/draft-statuses'; import { fetchFilters } from 'pl-fe/actions/filters'; import { fetchMarker } from 'pl-fe/actions/markers'; import { expandNotifications } from 'pl-fe/actions/notifications'; import { register as registerPushNotifications } from 'pl-fe/actions/push-notifications/registerer'; import { connectShoutbox } from 'pl-fe/actions/shoutbox'; import { fetchHomeTimeline } from 'pl-fe/actions/timelines'; import { useUserStream } from 'pl-fe/api/hooks/streaming/use-user-stream'; import { WITH_LANDING_PAGE } from 'pl-fe/build-config'; import SidebarNavigation from 'pl-fe/components/sidebar-navigation'; import ThumbNavigation from 'pl-fe/components/thumb-navigation'; import Layout from 'pl-fe/components/ui/layout'; import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; import { useClient } from 'pl-fe/hooks/use-client'; import { useDraggedFiles } from 'pl-fe/hooks/use-dragged-files'; import { useFeatures } from 'pl-fe/hooks/use-features'; import { useInstance } from 'pl-fe/hooks/use-instance'; import { useLoggedIn } from 'pl-fe/hooks/use-logged-in'; import { useOwnAccount } from 'pl-fe/hooks/use-own-account'; import { usePlFeConfig } from 'pl-fe/hooks/use-pl-fe-config'; import AdminLayout from 'pl-fe/layouts/admin-layout'; import ChatsLayout from 'pl-fe/layouts/chats-layout'; import DefaultLayout from 'pl-fe/layouts/default-layout'; import EmptyLayout from 'pl-fe/layouts/empty-layout'; import EventLayout from 'pl-fe/layouts/event-layout'; import EventsLayout from 'pl-fe/layouts/events-layout'; import ExternalLoginLayout from 'pl-fe/layouts/external-login-layout'; import GroupLayout from 'pl-fe/layouts/group-layout'; import GroupsLayout from 'pl-fe/layouts/groups-layout'; import HomeLayout from 'pl-fe/layouts/home-layout'; import LandingLayout from 'pl-fe/layouts/landing-layout'; import ManageGroupsLayout from 'pl-fe/layouts/manage-groups-layout'; import ProfileLayout from 'pl-fe/layouts/profile-layout'; import RemoteInstanceLayout from 'pl-fe/layouts/remote-instance-layout'; import SearchLayout from 'pl-fe/layouts/search-layout'; import StatusLayout from 'pl-fe/layouts/status-layout'; import { prefetchFollowRequests } from 'pl-fe/queries/accounts/use-follow-requests'; import { pendingUsersQuery } from 'pl-fe/queries/admin/use-accounts'; import { pendingReportsQuery } from 'pl-fe/queries/admin/use-reports'; import { queryClient } from 'pl-fe/queries/client'; import { prefetchCustomEmojis } from 'pl-fe/queries/instance/use-custom-emojis'; import { scheduledStatusesQueryOptions } from 'pl-fe/queries/statuses/scheduled-statuses'; import { useUiStore } from 'pl-fe/stores/ui'; import { getVapidKey } from 'pl-fe/utils/auth'; import { isStandalone } from 'pl-fe/utils/state'; import BackgroundShapes from './components/background-shapes'; import { AboutPage, AccountGallery, AccountHoverCard, AccountTimeline, AdminAccount, Aliases, Announcements, AuthTokenList, Backups, Blocks, BookmarkFolders, Bookmarks, BubbleTimeline, ChatIndex, ChatWidget, Circle, Circles, CommunityTimeline, ComposeEvent, Conversations, CreateApp, CryptoDonate, Dashboard, DeleteAccount, Developers, Directory, DomainBlocks, Domains, DraftStatuses, EditEmail, EditFilter, EditGroup, EditPassword, EditProfile, EventDiscussion, EventInformation, Events, ExportData, ExternalLogin, FavouritedStatuses, FederationRestrictions, Filters, FollowRecommendations, FollowRequests, FollowedTags, Followers, Following, GenericNotFound, GroupBlockedMembers, GroupGallery, GroupMembers, GroupMembershipRequests, GroupTimeline, Groups, HashtagTimeline, HomeTimeline, ImportData, IntentionalError, InteractionPolicies, InteractionRequests, LandingPage, LandingTimeline, LinkTimeline, ListTimeline, Lists, LoginPage, LogoutPage, ManageGroup, MfaForm, Migration, ModerationLog, Mutes, NewStatus, Notifications, OutgoingFollowRequests, PasswordReset, PinnedStatuses, PlFeConfig, PublicTimeline, Quotes, RegisterInvite, RegistrationPage, Relays, RemoteTimeline, Report, Rules, ScheduledStatuses, Search, ServerInfo, ServiceWorkerInfo, Settings, SettingsStore, Share, SidebarMenu, Status, StatusHoverCard, TestTimeline, ThemeEditor, UrlPrivacy, UserIndex, } from './util/async-components'; import GlobalHotkeys from './util/global-hotkeys'; import { WrappedRoute } from './util/react-router-helpers'; // Dummy import, to make sure that ends up in the application bundle. // Without this it ends up in ~8 very commonly used bundles. import 'pl-fe/components/status'; interface ISwitchingColumnsArea { children: React.ReactNode; } const SwitchingColumnsArea: React.FC = React.memo(({ children }) => { const instance = useInstance(); const features = useFeatures(); const { search } = useLocation(); const { isLoggedIn } = useLoggedIn(); const standalone = useAppSelector(isStandalone); const { authenticatedProfile, cryptoAddresses } = usePlFeConfig(); const hasCrypto = cryptoAddresses.length > 0; // NOTE: Mastodon and Pleroma route some basenames to the backend. // When adding new routes, use a basename that does NOT conflict // with a known backend route, but DO redirect the backend route // to the corresponding component as a fallback. // Ex: use /login instead of /auth, but redirect /auth to /login return ( {standalone && !isLoggedIn && (WITH_LANDING_PAGE ? : )} {isLoggedIn ? ( ) : ( )} {/* NOTE: we cannot nest routes in a fragment https://stackoverflow.com/a/68637108 */} {features.federating && } {features.federating && } {features.bubbleTimeline && } {features.federating && } {features.conversations && } {features.conversations && } {/* Mastodon web routes */} {/* Pleroma FE web routes */} {/* Iceshrimp.NET web routes */} {/* Mastodon rendered pages */} {/* Pleroma hard-coded email URLs */} {features.lists && } {features.lists && } {features.circles && } {features.bookmarks && } {features.bookmarks && } {features.suggestions && } {features.profileDirectory && } {features.events && } {features.events && } {features.chats && } {features.chats && } {features.chats && } {features.shoutbox && } {features.chats && } {features.outgoingFollowRequests && } {features.federating && } {(features.filters || features.filtersV2) && } {(features.filters || features.filtersV2) && } {(features.filters || features.filtersV2) && } {(features.followedHashtagsList) && } {features.interactionRequests && } {features.events && } {features.events && } {features.events && } {features.groups && } {features.groups && } {features.groups && } {features.groups && } {features.groups && } {features.groups && } {features.groups && } {features.groups && } {features.groups && } {features.scheduledStatuses && } {(features.importBlocks || features.importFollows || features.importMutes) && } {features.manageAccountAliases && } {features.accountMoving && } {features.accountBackups && } {features.interactionRequests && } {features.pleromaAdminAnnouncements && } {features.domains && } {features.adminRules && } Promise.reject(new TypeError('Failed to fetch dynamically imported module: TEST')))} content={children} /> {hasCrypto && } {features.federating && } {(features.accountCreation && instance.registrations.enabled) && ( )} ); }); interface IUI { children?: React.ReactNode; } const UI: React.FC = React.memo(({ children }) => { const history = useHistory(); const dispatch = useAppDispatch(); const node = useRef(null); const me = useAppSelector(state => state.me); const { account } = useOwnAccount(); const features = useFeatures(); const vapidKey = useAppSelector(state => getVapidKey(state)); const client = useClient(); const instance = useInstance(); const { isDropdownMenuOpen } = useUiStore(); const standalone = useAppSelector(isStandalone); const { isDragging } = useDraggedFiles(node); const handleServiceWorkerPostMessage = ({ data }: MessageEvent) => { if (data.type === 'navigate') { history.push(data.path); } else { console.warn('Unknown message type:', data.type); } }; const handleDragEnter = (e: DragEvent) => e.preventDefault(); const handleDragLeave = (e: DragEvent) => e.preventDefault(); const handleDragOver = (e: DragEvent) => e.preventDefault(); const handleDrop = (e: DragEvent) => e.preventDefault(); /** Load initial data when a user is logged in */ const loadAccountData = () => { if (!account) return; prefetchCustomEmojis(client); dispatch(fetchDraftStatuses()); dispatch(fetchHomeTimeline()); dispatch(expandNotifications()) // @ts-ignore .then(() => dispatch(fetchMarker(['notifications']))) .catch(console.error); if (account.is_admin || account.is_moderator) { queryClient.prefetchInfiniteQuery(pendingReportsQuery); queryClient.prefetchInfiniteQuery(pendingUsersQuery); } if (account.is_admin && features.pleromaAdminAccounts) { dispatch(fetchConfig()); } if (features.filters || features.filtersV2) { setTimeout(() => dispatch(fetchFilters()), 500); } if (account.locked) { setTimeout(() => prefetchFollowRequests(client), 700); } if (features.shoutbox) { dispatch(connectShoutbox()); } if (features.scheduledStatuses) { setTimeout(() => { queryClient.prefetchInfiniteQuery(scheduledStatusesQueryOptions); }, 900); } }; useEffect(() => { if ('serviceWorker' in navigator) { navigator.serviceWorker.addEventListener('message', handleServiceWorkerPostMessage); } if (window.Notification?.permission === 'default') { window.setTimeout(() => Notification.requestPermission(), 120 * 1000); } }, []); useEffect(() => { document.addEventListener('dragenter', handleDragEnter); document.addEventListener('dragleave', handleDragLeave); document.addEventListener('dragover', handleDragOver); document.addEventListener('drop', handleDrop); return () => { document.removeEventListener('dragenter', handleDragEnter); document.removeEventListener('dragleave', handleDragLeave); document.removeEventListener('dragover', handleDragOver); document.removeEventListener('drop', handleDrop); }; }, []); useUserStream(); // The user has logged in useEffect(() => { if (instance.fetched) loadAccountData(); }, [!!account, instance.fetched]); useEffect(() => { dispatch(registerPushNotifications()); }, [vapidKey]); // Wait for login to succeed or fail if (me === null) return null; const style: React.CSSProperties = { pointerEvents: isDropdownMenuOpen ? 'none' : undefined, }; // to be used with the deck const fullWidth = false; return (
{!(standalone && !me) && } {children} {me && features.chats && (
}>
)}
); }); export { UI as default };