import { lazy, Suspense, useEffect } from 'react'; import { useShallow } from 'zustand/react/shallow'; import { Navigate, Outlet, Route, Routes, useLocation, useParams } from 'react-router-dom'; import { useAccount, useAccountComment, useCommunity } from '@bitsocial/bitsocial-react-hooks'; import { initSnow, removeSnow, shouldShowSnow } from './lib/snow'; import { isAllView, isCatalogView, isModView, isSubscriptionsView } from './lib/utils/view-utils'; import { preloadReplyModal, preloadThemeAssets } from './lib/utils/preload-utils'; import { hasModQueueAccessRole } from './lib/utils/mod-access'; import useReplyModalStore from './stores/use-reply-modal-store'; import useCreateBoardModalStore from './stores/use-create-board-modal-store'; import useSpecialThemeStore from './stores/use-special-theme-store'; import useIsMobile from './hooks/use-is-mobile'; import { useAccountCommunityAddresses } from './hooks/use-account-community-addresses'; import useTheme from './hooks/use-theme'; import { useDirectories } from './hooks/use-directories'; import { useBrowserPureP2PAccountUpgrade } from './hooks/use-browser-pure-p2p-account-upgrade'; import { useCommunityIdentifier } from './hooks/use-community-identifiers'; import { useResolvedCommunityAddress, useResolvedDirectoryBoardPath } from './hooks/use-resolved-community-address'; import useSuspendOffscreenMediaPlayback from './hooks/use-suspend-offscreen-media-playback'; import { normalizeAccountCommentIndex } from './lib/utils/account-comment-index-utils'; import { getCommentCommunityAddress } from './lib/utils/comment-utils'; import { getBoardPath, isBoardModRoute, isDirectoryBoard, isArchiveRoute, isDirectoryListRoute, isLegacyBoardModQueueRoute, isPostRoute, isPendingPostRoute, isModQueueRoute, isValidBoardModRoute, isValidModRoute, isFlashBoardRoute, } from './lib/utils/route-utils'; import styles from './app.module.css'; import { DesktopBoardButtons, MobileAllFeedFilter, MobileBoardButtons } from './components/board-buttons/board-buttons'; import Blotter from './views/blotter/blotter'; import Faq from './views/faq/faq'; import Home from './views/home/home'; import Archive from './views/archive/archive'; import Directory from './views/directory/directory'; import ModQueueView from './views/mod-queue/mod-queue'; import NotAllowed from './views/not-allowed/not-allowed'; import NotFound from './views/not-found/not-found'; import Pass from './views/pass/pass'; import PendingPost from './views/pending-post/pending-post'; import Post from './views/post/post'; import Rules from './views/rules/rules'; import BoardHeader from './components/board-header/board-header'; import FeedCacheContainer from './components/feed-cache-container/feed-cache-container'; import PostForm from './components/post-form/post-form'; import BoardBlotter from './components/board-blotter/board-blotter'; import BoardsBar from './components/boards-bar/boards-bar'; import ExternalQuoteStatus from './components/external-quote-status/external-quote-status'; import ModEmptyState from './components/mod-empty-state/mod-empty-state'; const AccountDataEditor = lazy(() => import('./views/account-data-editor')); const BoardsBarEditModal = lazy(() => import('./components/boards-bar-edit-modal')); const CreateBoardModal = lazy(() => import('./components/create-board-modal')); const ChallengeModal = lazy(() => import('./components/challenge-modal')); const DirectoryModal = lazy(() => import('./components/directory-modal')); const DisclaimerModal = lazy(() => import('./components/disclaimer-modal')); const ReplyModal = lazy(() => import('./components/reply-modal')); const SettingsModal = lazy(() => import('./components/settings-modal')); const SettingsUpgradeModal = lazy(() => import('./components/settings-upgrade-modal')); // Preload all theme assets (buttons, backgrounds) immediately on app load // to prevent visible loading delays when switching themes preloadThemeAssets(); preloadReplyModal(); const getPostFormRouteKeyPath = (pathname: string) => pathname.replace(/\/settings$/, '').replace(/\/$/, ''); const getPageOneCanonicalPath = (boardIdentifier: string, pathname: string) => `/${boardIdentifier}${pathname.endsWith('/settings') ? '/settings' : ''}`; const BoardLayout = () => { const params = useParams(); const { accountCommentIndex, boardIdentifier, pageNumber } = params; const { pathname, search, hash } = useLocation(); const isMobile = useIsMobile(); const isInAllView = isAllView(pathname); const isInSubscriptionsView = isSubscriptionsView(pathname, useParams()); const isInModView = isModView(pathname); const directories = useDirectories(); const communityAddress = useResolvedCommunityAddress(boardIdentifier); const { boardPath: resolvedDirectoryBoardPath, isDirectoryCandidate } = useResolvedDirectoryBoardPath(boardIdentifier); const pendingPost = useAccountComment({ commentIndex: normalizeAccountCommentIndex(accountCommentIndex) }); const pendingPostCommunityAddress = getCommentCommunityAddress(pendingPost); const { closeCreateBoardModal } = useCreateBoardModalStore(); const isOnPostRoute = isPostRoute(pathname); const isOnPendingPostRoute = isPendingPostRoute(pathname); const isOnModQueueRoute = isModQueueRoute(pathname); const isOnArchiveRoute = isArchiveRoute(pathname); const isOnDirectoryRoute = isDirectoryListRoute(pathname); const shouldRenderOutlet = isOnPostRoute || isOnPendingPostRoute || isOnModQueueRoute || isOnArchiveRoute || isOnDirectoryRoute; const shouldRenderBoardBlotter = !isOnArchiveRoute && !isOnDirectoryRoute && !isOnModQueueRoute; const isInCatalogView = isCatalogView(pathname, params); // Christmas snow const { isEnabled: isSpecialEnabled } = useSpecialThemeStore(); useEffect(() => { if (isSpecialEnabled && shouldShowSnow() && !isMobile) { initSnow({ flakeCount: 150 }); } return () => { removeSnow(); }; }, [isSpecialEnabled, isMobile]); // Close create board modal when navigating to a different page useEffect(() => { closeCreateBoardModal(); }, [pathname, closeCreateBoardModal]); // force rerender of post form when navigating between pages, except when opening settings modal in current view const key = `${communityAddress}-${getPostFormRouteKeyPath(pathname)}`; if (pageNumber === '1' && boardIdentifier) { return ; } if (isCatalogView(pathname, params) && isFlashBoardRoute(boardIdentifier, directories)) { return ; } // Invalid /mod/ paths (e.g. /mod/modqueue, /mod/asdoijasd) -> not-found if (pathname.startsWith('/mod/') && !isValidModRoute(pathname)) { return ; } if (isLegacyBoardModQueueRoute(pathname)) { return ; } // Invalid board-scoped mod paths (e.g. /biz/mod, /biz/mod/asdoijasd) -> not-found if (isBoardModRoute(pathname) && !isValidBoardModRoute(pathname)) { return ; } // Normalize address URLs to directory codes: /anime-and-manga.eth/thread/xxx -> /a/thread/xxx if (boardIdentifier && !isDirectoryBoard(boardIdentifier, directories)) { const canonicalBoardIdentifier = resolvedDirectoryBoardPath ?? (isDirectoryCandidate ? boardIdentifier : getBoardPath(boardIdentifier, directories)); if (canonicalBoardIdentifier !== boardIdentifier) { const canonicalPath = pathname.replace(`/${boardIdentifier}`, `/${canonicalBoardIdentifier}`); return ; } } return (
{isMobile ? (communityAddress || isInAllView || isInModView || isInSubscriptionsView || pendingPostCommunityAddress || isOnModQueueRoute) && !isOnArchiveRoute && !isOnDirectoryRoute && (isInCatalogView ? ( <> ) : ( <> {isInAllView && } )) : (communityAddress || isInAllView || isInModView || isInSubscriptionsView || pendingPostCommunityAddress || isOnModQueueRoute) && !isOnArchiveRoute && !isOnDirectoryRoute && ( <> {shouldRenderBoardBlotter ? : null} )} {!isOnModQueueRoute && } {shouldRenderOutlet && }
); }; const GlobalLayout = () => { useTheme(); useSuspendOffscreenMediaPlayback(); const { activeCid, parentNumber, threadNumber, threadCid, communityAddress: activeCommunityAddress, closeModal, showReplyModal, scrollY, } = useReplyModalStore( useShallow((state) => ({ activeCid: state.activeCid, parentNumber: state.parentNumber, threadNumber: state.threadNumber, threadCid: state.threadCid, communityAddress: state.communityAddress, closeModal: state.closeModal, showReplyModal: state.showReplyModal, scrollY: state.scrollY, })), ); const { pathname } = useLocation(); const isInSettingsView = pathname.endsWith('/settings'); return ( <> {activeCid && threadCid && activeCommunityAddress && ( )} {isInSettingsView && ( )} ); }; const ModQueueRoute = () => { const { boardIdentifier } = useParams(); const account = useAccount(); const accountAddress = account?.author?.address; const communityAddress = useResolvedCommunityAddress(); const communityIdentifier = useCommunityIdentifier(communityAddress); const community = useCommunity(communityIdentifier ? { community: communityIdentifier } : undefined); const accountCommunityAddresses = useAccountCommunityAddresses(); if (!account) { return null; } if (!accountAddress) { return ; } if (!boardIdentifier) { return accountCommunityAddresses.length > 0 ? : ; } // Wait for board role metadata before enforcing access to avoid false redirects during initial load. const boardState = community?.state; const isBoardLoading = !community || !boardState || (boardState !== 'succeeded' && boardState !== 'failed'); if (isBoardLoading) { return null; } const accountRole = community?.roles?.[accountAddress]?.role; return hasModQueueAccessRole(accountRole) ? : ; }; const App = () => { useBrowserPureP2PAccountUpgrade(); // Feed routes are always rendered by FeedCacheContainer (Virtuoso used for all modes) const boardFeedElement = null; const catalogFeedElement = null; return (
}> } /> } /> } /> } /> } /> } /> } /> }> {/* Canonical multiboard routes (time filter lives in ?t=) */} } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> {/* Invalid subpaths: old URLs and unknown paths -> not-found */} } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } />
); }; export default App;