import clsx from 'clsx'; import React, { Suspense, lazy, useEffect, useRef } from 'react'; import { Switch, useHistory, useLocation, Redirect } from 'react-router-dom'; import { fetchFollowRequests } from 'soapbox/actions/accounts'; import { fetchReports, fetchUsers, fetchConfig } from 'soapbox/actions/admin'; import { fetchAnnouncements } from 'soapbox/actions/announcements'; import { fetchCustomEmojis } from 'soapbox/actions/custom-emojis'; import { fetchFilters } from 'soapbox/actions/filters'; import { fetchMarker } from 'soapbox/actions/markers'; import { expandNotifications } from 'soapbox/actions/notifications'; import { register as registerPushNotifications } from 'soapbox/actions/push-notifications'; import { fetchScheduledStatuses } from 'soapbox/actions/scheduled-statuses'; import { fetchSuggestionsForTimeline } from 'soapbox/actions/suggestions'; import { expandHomeTimeline } from 'soapbox/actions/timelines'; import { useUserStream } from 'soapbox/api/hooks'; import { useSignerStream } from 'soapbox/api/hooks/nostr/useSignerStream'; import SidebarNavigation from 'soapbox/components/sidebar-navigation'; import ThumbNavigation from 'soapbox/components/thumb-navigation'; import { Layout } from 'soapbox/components/ui'; import { useAppDispatch, useAppSelector, useOwnAccount, useSoapboxConfig, useFeatures, useDraggedFiles, useInstance, useLoggedIn } from 'soapbox/hooks'; import AdminPage from 'soapbox/pages/admin-page'; import ChatsPage from 'soapbox/pages/chats-page'; import DefaultPage from 'soapbox/pages/default-page'; import EmptyPage from 'soapbox/pages/empty-page'; import EventPage from 'soapbox/pages/event-page'; import EventsPage from 'soapbox/pages/events-page'; import GroupPage from 'soapbox/pages/group-page'; import GroupsPage from 'soapbox/pages/groups-page'; import GroupsPendingPage from 'soapbox/pages/groups-pending-page'; import HomePage from 'soapbox/pages/home-page'; import LandingPage from 'soapbox/pages/landing-page'; import ManageGroupsPage from 'soapbox/pages/manage-groups-page'; import ProfilePage from 'soapbox/pages/profile-page'; import RemoteInstancePage from 'soapbox/pages/remote-instance-page'; import SearchPage from 'soapbox/pages/search-page'; import StatusPage from 'soapbox/pages/status-page'; import { getVapidKey } from 'soapbox/utils/auth'; import { isStandalone } from 'soapbox/utils/state'; import BackgroundShapes from './components/background-shapes'; import FloatingActionButton from './components/floating-action-button'; import Navbar from './components/navbar'; import { Status, CommunityTimeline, PublicTimeline, RemoteTimeline, AccountTimeline, AccountGallery, HomeTimeline, Followers, Following, DirectTimeline, Conversations, HashtagTimeline, Notifications, FollowRequests, GenericNotFound, FavouritedStatuses, Blocks, DomainBlocks, Mutes, Filters, EditFilter, PinnedStatuses, Search, ListTimeline, Lists, Bookmarks, Settings, EditProfile, EditEmail, EditPassword, EmailConfirmation, DeleteAccount, SoapboxConfig, ExportData, ImportData, Backups, MfaForm, ChatIndex, ChatWidget, ServerInfo, Dashboard, ModerationLog, CryptoDonate, ScheduledStatuses, UserIndex, FederationRestrictions, Aliases, Migration, FollowRecommendations, Directory, SidebarMenu, ProfileHoverCard, StatusHoverCard, Share, NewStatus, IntentionalError, Developers, CreateApp, SettingsStore, TestTimeline, LogoutPage, AuthTokenList, ThemeEditor, Quotes, ServiceWorkerInfo, EventInformation, EventDiscussion, Events, GroupGallery, Groups, GroupsDiscover, GroupsPopular, GroupsSuggested, GroupsTag, GroupsTags, PendingGroupRequests, GroupMembers, GroupTags, GroupTagTimeline, GroupTimeline, ManageGroup, GroupBlockedMembers, GroupMembershipRequests, Announcements, EditGroup, FollowedTags, AboutPage, RegistrationPage, LoginPage, PasswordReset, PasswordResetConfirm, RegisterInvite, ExternalLogin, LandingTimeline, BookmarkFolders, } 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 'soapbox/components/status'; interface ISwitchingColumnsArea { children: React.ReactNode; } const SwitchingColumnsArea: React.FC = ({ children }) => { const instance = useInstance(); const features = useFeatures(); const { search } = useLocation(); const { isLoggedIn } = useLoggedIn(); const standalone = useAppSelector(isStandalone); const { authenticatedProfile, cryptoAddresses } = useSoapboxConfig(); const hasCrypto = cryptoAddresses.size > 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 ? ( ) : ( )} {/* NOTE: we cannot nest routes in a fragment https://stackoverflow.com/a/68637108 */} {features.federating && } {features.federating && } {features.federating && } {features.conversations && } {features.directTimeline && } {(features.conversations && !features.directTimeline) && ( )} {/* Mastodon web routes */} {/* Pleroma FE web routes */} {/* Gab */} {/* Mastodon rendered pages */} {/* Pleroma hard-coded email URLs */} {/* Soapbox Legacy redirects */} {features.lists && } {features.lists && } {features.bookmarks && } {features.bookmarks && } {features.suggestions && } {features.profileDirectory && } {features.events && } {features.chats && } {features.chats && } {features.chats && } {features.chats && } {features.federating && } {(features.filters || features.filtersV2) && } {(features.filters || features.filtersV2) && } {(features.filters || features.filtersV2) && } {(features.followedHashtagsList) && } {features.events && } {features.events && } {features.groups && } {features.groupsDiscovery && } {features.groupsDiscovery && } {features.groupsDiscovery && } {features.groupsDiscovery && } {features.groupsDiscovery && } {features.groupsPending && } {features.groupsTags && } {features.groupsTags && } {features.groups && } {features.groups && } {features.groups && } {features.groups && } {features.groups && } {features.groups && } {features.groups && } {features.groups && } {features.scheduledStatuses && } {features.exportData && } {features.importData && } {features.accountAliases && } {features.accountMoving && } {features.backups && } 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 = ({ 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 dropdownMenuIsOpen = useAppSelector(state => state.dropdown_menu.isOpen); 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; dispatch(expandHomeTimeline({}, () => { dispatch(fetchSuggestionsForTimeline()); })); dispatch(expandNotifications()) // @ts-ignore .then(() => dispatch(fetchMarker(['notifications']))) .catch(console.error); dispatch(fetchAnnouncements()); if (account.staff) { dispatch(fetchReports({ resolved: false })); dispatch(fetchUsers(['local', 'need_approval'])); } if (account.admin) { dispatch(fetchConfig()); } setTimeout(() => dispatch(fetchFilters()), 500); if (account.locked) { setTimeout(() => dispatch(fetchFollowRequests()), 700); } setTimeout(() => dispatch(fetchScheduledStatuses()), 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(); useSignerStream(); // The user has logged in useEffect(() => { loadAccountData(); dispatch(fetchCustomEmojis()); }, [!!account]); useEffect(() => { dispatch(registerPushNotifications()); }, [vapidKey]); const shouldHideFAB = (): boolean => { const path = location.pathname; return Boolean(path.match(/^\/posts\/|^\/search|^\/getting-started|^\/chats/)); }; // Wait for login to succeed or fail if (me === null) return null; const style: React.CSSProperties = { pointerEvents: dropdownMenuIsOpen ? 'none' : undefined, }; return (
{!standalone && } {children} {(me && !shouldHideFAB()) && (
)} {me && ( )} {me && features.chats && (
}>
)}
); }; export default UI;