import Sidebar from '@/components/Sidebar' import { Button } from '@/components/ui/button' import { cn } from '@/lib/utils' import logger from '@/lib/logger' import { ChevronLeft } from 'lucide-react' import { NavigationService } from '@/services/navigation.service' import NoteListPage from '@/pages/primary/NoteListPage' import SecondaryNoteListPage from '@/pages/secondary/NoteListPage' // Page imports needed for primary note view import SettingsPage from '@/pages/secondary/SettingsPage' import RelaySettingsPage from '@/pages/secondary/RelaySettingsPage' import WalletPage from '@/pages/secondary/WalletPage' import PostSettingsPage from '@/pages/secondary/PostSettingsPage' import GeneralSettingsPage from '@/pages/secondary/GeneralSettingsPage' import TranslationPage from '@/pages/secondary/TranslationPage' import RssFeedSettingsPage from '@/pages/secondary/RssFeedSettingsPage' import NoteDrawer from '@/components/NoteDrawer' import SecondaryProfilePage from '@/pages/secondary/ProfilePage' import storage from '@/services/local-storage.service' import { Sheet, SheetContent } from '@/components/ui/sheet' import FollowingListPage from '@/pages/secondary/FollowingListPage' import MuteListPage from '@/pages/secondary/MuteListPage' import OthersRelaySettingsPage from '@/pages/secondary/OthersRelaySettingsPage' import SecondaryRelayPage from '@/pages/secondary/RelayPage' import { CurrentRelaysProvider } from '@/providers/CurrentRelaysProvider' import { NotificationProvider } from '@/providers/NotificationProvider' // DEPRECATED: useUserPreferences removed - double-panel functionality disabled import { TPageRef } from '@/types' import { cloneElement, createContext, createRef, ReactNode, RefObject, useCallback, useContext, useEffect, useRef, useState } from 'react' import BottomNavigationBar from './components/BottomNavigationBar' import TooManyRelaysAlertDialog from './components/TooManyRelaysAlertDialog' import { normalizeUrl } from './lib/url' import ExplorePage from './pages/primary/ExplorePage' import MePage from './pages/primary/MePage' import NotificationListPage from './pages/primary/NotificationListPage' import ProfilePage from './pages/primary/ProfilePage' import RelayPage from './pages/primary/RelayPage' import SearchPage from './pages/primary/SearchPage' import DiscussionsPage from './pages/primary/DiscussionsPage' import { useScreenSize } from './providers/ScreenSizeProvider' import { routes } from './routes' import modalManager from './services/modal-manager.service' import CreateWalletGuideToast from './components/CreateWalletGuideToast' type TPrimaryPageContext = { navigate: (page: TPrimaryPageName, props?: object) => void current: TPrimaryPageName | null display: boolean } type TSecondaryPageContext = { push: (url: string) => void pop: () => void currentIndex: number navigateToPrimaryPage: (page: TPrimaryPageName, props?: object) => void } type TStackItem = { index: number url: string component: React.ReactElement | null ref: RefObject | null } const PRIMARY_PAGE_REF_MAP = { home: createRef(), explore: createRef(), notifications: createRef(), me: createRef(), profile: createRef(), relay: createRef(), search: createRef(), discussions: createRef() } // Lazy function to create PRIMARY_PAGE_MAP to avoid circular dependency // This is only evaluated when called, not at module load time const getPrimaryPageMap = () => ({ home: , explore: , notifications: , me: , profile: , relay: , search: , discussions: }) // Type for primary page names - use the return type of getPrimaryPageMap export type TPrimaryPageName = keyof ReturnType export const PrimaryPageContext = createContext(undefined) const SecondaryPageContext = createContext(undefined) const PrimaryNoteViewContext = createContext<{ setPrimaryNoteView: (view: ReactNode | null, type?: 'note' | 'settings' | 'settings-sub' | 'profile' | 'hashtag' | 'relay' | 'following' | 'mute' | 'others-relay-settings') => void primaryViewType: 'note' | 'settings' | 'settings-sub' | 'profile' | 'hashtag' | 'relay' | 'following' | 'mute' | 'others-relay-settings' | null getNavigationCounter: () => number } | undefined>(undefined) const NoteDrawerContext = createContext<{ openDrawer: (noteId: string) => void closeDrawer: () => void isDrawerOpen: boolean drawerNoteId: string | null } | undefined>(undefined) export function usePrimaryPage() { const context = useContext(PrimaryPageContext) if (!context) { throw new Error('usePrimaryPage must be used within a PrimaryPageContext.Provider') } return context } export function useSecondaryPage() { const context = useContext(SecondaryPageContext) if (!context) { throw new Error('useSecondaryPage must be used within a SecondaryPageContext.Provider') } return context } export function usePrimaryNoteView() { const context = useContext(PrimaryNoteViewContext) if (!context) { throw new Error('usePrimaryNoteView must be used within a PrimaryNoteViewContext.Provider') } return context } export function useNoteDrawer() { const context = useContext(NoteDrawerContext) if (!context) { throw new Error('useNoteDrawer must be used within a NoteDrawerContext.Provider') } return context } // Helper function to build contextual note URL function buildNoteUrl(noteId: string, currentPage: TPrimaryPageName | null): string { // Pages that should preserve context in the URL const contextualPages: TPrimaryPageName[] = ['discussions', 'search', 'profile', 'explore', 'notifications'] if (currentPage && contextualPages.includes(currentPage) && currentPage !== 'home') { return `/${currentPage}/notes/${noteId}` } return `/notes/${noteId}` } // Helper function to build contextual relay URL function buildRelayUrl(relayUrl: string, currentPage: TPrimaryPageName | null): string { const encodedRelayUrl = encodeURIComponent(relayUrl) // Only preserve context for explore page (where you discover relays) if (currentPage === 'explore') { return `/explore/relays/${encodedRelayUrl}` } return `/relays/${encodedRelayUrl}` } // Helper function to extract noteId and context from URL function parseNoteUrl(url: string): { noteId: string; context?: string } { // Match patterns like /discussions/notes/{noteId} or /notes/{noteId} const contextualMatch = url.match(/\/(discussions|search|profile|explore|notifications)\/notes\/(.+)$/) if (contextualMatch) { return { noteId: contextualMatch[2], context: contextualMatch[1] } } // Match standard pattern /notes/{noteId} const standardMatch = url.match(/\/notes\/(.+)$/) if (standardMatch) { return { noteId: standardMatch[1] } } // Fallback: extract from any /notes/ pattern const fallbackMatch = url.replace(/.*\/notes\//, '') return { noteId: fallbackMatch || url } } // Fixed: Note navigation uses drawer on mobile/single-pane, secondary panel on double-pane desktop export function useSmartNoteNavigation() { const { push: pushSecondaryPage } = useSecondaryPage() const { openDrawer, isDrawerOpen } = useNoteDrawer() const { isSmallScreen } = useScreenSize() const { current: currentPrimaryPage } = usePrimaryPage() const navigateToNote = (url: string) => { // Extract noteId from URL (handles both /notes/{id} and /{context}/notes/{id}) const { noteId } = parseNoteUrl(url) // Build contextual URL based on current page const contextualUrl = buildNoteUrl(noteId, currentPrimaryPage) if (isSmallScreen) { // Mobile: always push to secondary stack AND update drawer // This ensures back button works when clicking embedded events pushSecondaryPage(contextualUrl) openDrawer(noteId) } else { // Desktop: check panel mode const currentPanelMode = storage.getPanelMode() if (currentPanelMode === 'single') { // Single-pane: if drawer is already open, push to stack AND update drawer // Otherwise, just open drawer if (isDrawerOpen) { // Navigating from within drawer - push to stack for back button support pushSecondaryPage(contextualUrl) openDrawer(noteId) } else { // Opening drawer for first time window.history.pushState(null, '', contextualUrl) openDrawer(noteId) } } else { // Double-pane: use secondary panel pushSecondaryPage(contextualUrl) } } } return { navigateToNote } } // Fixed: Relay navigation now uses primary note view on mobile, secondary routing (drawer in single-pane, side panel in double-pane) on desktop export function useSmartRelayNavigation() { const { setPrimaryNoteView } = usePrimaryNoteView() const { push: pushSecondaryPage } = useSecondaryPage() const { isSmallScreen } = useScreenSize() const { current: currentPrimaryPage } = usePrimaryPage() const navigateToRelay = (url: string) => { // Extract relay URL from path (handles both /relays/{url} and /{context}/relays/{url}) const relayUrlMatch = url.match(/\/(discussions|search|profile|explore|notifications)\/relays\/(.+)$/) || url.match(/\/relays\/(.+)$/) const relayUrl = relayUrlMatch ? decodeURIComponent(relayUrlMatch[relayUrlMatch.length - 1]) : decodeURIComponent(url.replace(/.*\/relays\//, '')) // Build contextual URL based on current page const contextualUrl = buildRelayUrl(relayUrl, currentPrimaryPage) if (isSmallScreen) { // Use primary note view on mobile window.history.pushState(null, '', contextualUrl) setPrimaryNoteView(, 'relay') } else { // Desktop: always use secondary routing (will be rendered in drawer in single-pane, side panel in double-pane) pushSecondaryPage(contextualUrl) } } return { navigateToRelay } } // Fixed: Profile navigation now uses primary note view on mobile, secondary routing on desktop export function useSmartProfileNavigation() { const { setPrimaryNoteView } = usePrimaryNoteView() const { push: pushSecondaryPage } = useSecondaryPage() const { isSmallScreen } = useScreenSize() const { closeDrawer, isDrawerOpen } = useNoteDrawer() const navigateToProfile = (url: string) => { // Close drawer if open (profiles aren't shown in drawers) // Navigate after drawer closes to avoid URL being restored by drawer's onOpenChange if (isDrawerOpen) { closeDrawer() // Wait for drawer to close (350ms animation) before navigating setTimeout(() => { if (isSmallScreen) { // Use primary note view on mobile const profileId = url.replace('/users/', '') window.history.pushState(null, '', url) setPrimaryNoteView(, 'profile') } else { // Use secondary routing on desktop pushSecondaryPage(url) } }, 400) // Slightly longer than drawer close animation (350ms) } else { // No drawer open, navigate immediately if (isSmallScreen) { // Use primary note view on mobile const profileId = url.replace('/users/', '') window.history.pushState(null, '', url) setPrimaryNoteView(, 'profile') } else { // Use secondary routing on desktop pushSecondaryPage(url) } } } return { navigateToProfile } } // Fixed: Hashtag navigation now uses primary note view since secondary panel is disabled export function useSmartHashtagNavigation() { const { setPrimaryNoteView, getNavigationCounter } = usePrimaryNoteView() const navigateToHashtag = (url: string) => { // Use primary note view to show hashtag feed since secondary panel is disabled // Update URL first - do this synchronously before setting the view const parsedUrl = url.startsWith('/') ? url : `/${url}` window.history.pushState(null, '', parsedUrl) // Extract hashtag from URL for the key to ensure unique keys for different hashtags const searchParams = new URLSearchParams(parsedUrl.includes('?') ? parsedUrl.split('?')[1] : '') const hashtag = searchParams.get('t') || '' // Get the current navigation counter and use next value for the key // This ensures unique keys that force remounting - setPrimaryNoteView will increment it const counter = getNavigationCounter() const key = `hashtag-${hashtag}-${counter + 1}` // Use a key based on the hashtag and navigation counter to force remounting when hashtag changes // This ensures the component reads the new URL parameters when it mounts // setPrimaryNoteView will increment the counter, so we use counter + 1 for the key setPrimaryNoteView(, 'hashtag') // Dispatch custom event as a fallback for components that might be reused window.dispatchEvent(new CustomEvent('hashtag-navigation', { detail: { url: parsedUrl } })) } return { navigateToHashtag } } // Fixed: Following list navigation now uses primary note view on mobile, secondary routing on desktop export function useSmartFollowingListNavigation() { const { setPrimaryNoteView } = usePrimaryNoteView() const { push: pushSecondaryPage } = useSecondaryPage() const { isSmallScreen } = useScreenSize() const navigateToFollowingList = (url: string) => { if (isSmallScreen) { // Use primary note view on mobile const profileId = url.replace('/users/', '').replace('/following', '') window.history.pushState(null, '', url) setPrimaryNoteView(, 'following') } else { // Use secondary routing on desktop pushSecondaryPage(url) } } return { navigateToFollowingList } } // Fixed: Mute list navigation now uses primary note view on mobile, secondary routing on desktop export function useSmartMuteListNavigation() { const { setPrimaryNoteView } = usePrimaryNoteView() const { push: pushSecondaryPage } = useSecondaryPage() const { isSmallScreen } = useScreenSize() const navigateToMuteList = (url: string) => { if (isSmallScreen) { // Use primary note view on mobile window.history.pushState(null, '', url) setPrimaryNoteView(, 'mute') } else { // Use secondary routing on desktop pushSecondaryPage(url) } } return { navigateToMuteList } } // Fixed: Others relay settings navigation now uses primary note view on mobile, secondary routing on desktop export function useSmartOthersRelaySettingsNavigation() { const { setPrimaryNoteView } = usePrimaryNoteView() const { push: pushSecondaryPage } = useSecondaryPage() const { isSmallScreen } = useScreenSize() const navigateToOthersRelaySettings = (url: string) => { if (isSmallScreen) { // Use primary note view on mobile const profileId = url.replace('/users/', '').replace('/relays', '') window.history.pushState(null, '', url) setPrimaryNoteView(, 'others-relay-settings') } else { // Use secondary routing on desktop pushSecondaryPage(url) } } return { navigateToOthersRelaySettings } } // Fixed: Settings navigation now uses primary note view since secondary panel is disabled export function useSmartSettingsNavigation() { const { setPrimaryNoteView } = usePrimaryNoteView() const navigateToSettings = (url: string) => { // Use primary note view to show settings since secondary panel is disabled if (url === '/settings') { window.history.pushState(null, '', url) setPrimaryNoteView(, 'settings') } else if (url.startsWith('/settings/relays')) { window.history.pushState(null, '', url) setPrimaryNoteView(, 'settings-sub') } else if (url === '/settings/wallet') { window.history.pushState(null, '', url) setPrimaryNoteView(, 'settings-sub') } else if (url === '/settings/posts') { window.history.pushState(null, '', url) setPrimaryNoteView(, 'settings-sub') } else if (url === '/settings/general') { window.history.pushState(null, '', url) setPrimaryNoteView(, 'settings-sub') } else if (url === '/settings/translation') { window.history.pushState(null, '', url) setPrimaryNoteView(, 'settings-sub') } else if (url === '/settings/rss-feeds') { window.history.pushState(null, '', url) setPrimaryNoteView(, 'settings-sub') } } return { navigateToSettings } } // DEPRECATED: ConditionalHomePage removed - double-panel functionality disabled // Helper function to get page title based on view type and URL function getPageTitle(viewType: 'note' | 'settings' | 'settings-sub' | 'profile' | 'hashtag' | 'relay' | 'following' | 'mute' | 'others-relay-settings' | null, pathname: string): string { // Create a temporary navigation service instance to use the getPageTitle method const tempService = new NavigationService({ setPrimaryNoteView: () => {} }) return tempService.getPageTitle(viewType, pathname) } // DEPRECATED: Double-panel functionality removed - simplified to single column layout function MainContentArea({ primaryPages, currentPrimaryPage, primaryNoteView, primaryViewType, goBack }: { primaryPages: { name: TPrimaryPageName; element: ReactNode; props?: any }[] currentPrimaryPage: TPrimaryPageName primaryNoteView: ReactNode | null primaryViewType: 'note' | 'settings' | 'settings-sub' | 'profile' | 'hashtag' | 'relay' | 'following' | 'mute' | 'others-relay-settings' | null goBack: () => void }) { const [, forceUpdate] = useState(0) // Listen for note page title updates useEffect(() => { const handleTitleUpdate = () => { forceUpdate(n => n + 1) } window.addEventListener('notePageTitleUpdated', handleTitleUpdate) return () => { window.removeEventListener('notePageTitleUpdated', handleTitleUpdate) } }, []) logger.debug('MainContentArea rendering:', { currentPrimaryPage, primaryPages: primaryPages.map(p => p.name), primaryNoteView: !!primaryNoteView }) // Always use single column layout since double-panel is disabled return (
{primaryNoteView ? ( // Show note view with back button
Imwald
{getPageTitle(primaryViewType, window.location.pathname)}
{primaryNoteView}
) : ( // Show normal primary pages primaryPages.map(({ name, element, props }) => { const isCurrentPage = currentPrimaryPage === name logger.debug(`Primary page ${name}:`, { isCurrentPage, currentPrimaryPage }) return (
{(() => { try { logger.debug(`Rendering ${name} component`) return props ? cloneElement(element as React.ReactElement, props) : element } catch (error) { logger.error(`Error rendering ${name} component:`, error) return
Error rendering {name}: {error instanceof Error ? error.message : String(error)}
} })()}
) }) )}
{/* DEPRECATED: Secondary panel removed - double-panel functionality disabled */}
) } export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { const { isSmallScreen } = useScreenSize() // DEPRECATED: showRecommendedRelaysPanel removed - double-panel functionality disabled const [currentPrimaryPage, setCurrentPrimaryPage] = useState('home') const [primaryPages, setPrimaryPages] = useState< { name: TPrimaryPageName; element: ReactNode; props?: any }[] >([ { name: 'home', element: getPrimaryPageMap().home } ]) const [secondaryStack, setSecondaryStack] = useState([]) const [primaryNoteView, setPrimaryNoteViewState] = useState(null) const [primaryViewType, setPrimaryViewType] = useState<'note' | 'settings' | 'settings-sub' | 'profile' | 'hashtag' | 'relay' | 'following' | 'mute' | 'others-relay-settings' | null>(null) const [savedPrimaryPage, setSavedPrimaryPage] = useState(null) const [drawerOpen, setDrawerOpen] = useState(false) const [drawerNoteId, setDrawerNoteId] = useState(null) const [panelMode, setPanelMode] = useState<'single' | 'double'>(() => storage.getPanelMode()) const navigationCounterRef = useRef(0) const savedFeedStateRef = useRef>(new Map()) const currentTabStateRef = useRef>(new Map()) // Track current tab state for each page const setPrimaryNoteView = (view: ReactNode | null, type?: 'note' | 'settings' | 'settings-sub' | 'profile' | 'hashtag' | 'relay' | 'following' | 'mute' | 'others-relay-settings') => { if (view && !primaryNoteView) { // Saving current primary page before showing overlay setSavedPrimaryPage(currentPrimaryPage) // Get current tab state from ref (updated by components via events) const currentTab = currentTabStateRef.current.get(currentPrimaryPage) // Get Discussions state if on discussions page let discussionsState: { selectedTopic: string, timeSpan: '30days' | '90days' | 'all' } | undefined = undefined if (currentPrimaryPage === 'discussions') { // Request discussions state from component const stateEvent = new CustomEvent('requestDiscussionsState') let receivedState: { selectedTopic: string, timeSpan: '30days' | '90days' | 'all' } | null = null const handler = ((e: CustomEvent) => { receivedState = e.detail }) as EventListener window.addEventListener('discussionsStateResponse', handler) window.dispatchEvent(stateEvent) setTimeout(() => { window.removeEventListener('discussionsStateResponse', handler) if (receivedState) { discussionsState = receivedState } }, 10) } // Get trending tab if on search page const trendingTab = currentTabStateRef.current.get('search') as 'nostr' | 'relays' | 'hashtags' | undefined // Save state (tab, discussions, trending) if any exists if (currentTab || discussionsState || trendingTab) { logger.info('PageManager: Saving page state', { page: currentPrimaryPage, tab: currentTab, discussionsState, trendingTab }) savedFeedStateRef.current.set(currentPrimaryPage, { tab: currentTab, discussionsState, trendingTab }) } } // Increment navigation counter when setting a new view to ensure unique keys // This forces React to remount components even when navigating between items of the same type if (view) { navigationCounterRef.current += 1 } // Always update the view state - even if the type is the same, the component might be different // This ensures that navigation works even when navigating between items of the same type (e.g., different hashtags) setPrimaryNoteViewState(view) setPrimaryViewType(type || null) // If clearing the view, restore to the saved primary page if (!view && savedPrimaryPage) { const newUrl = savedPrimaryPage === 'home' ? '/' : `/${savedPrimaryPage}` window.history.replaceState(null, '', newUrl) const savedFeedState = savedFeedStateRef.current.get(savedPrimaryPage) // Restore tab state first if (savedFeedState?.tab) { logger.info('PageManager: Restoring tab state', { page: savedPrimaryPage, tab: savedFeedState.tab }) window.dispatchEvent(new CustomEvent('restorePageTab', { detail: { page: savedPrimaryPage, tab: savedFeedState.tab } })) currentTabStateRef.current.set(savedPrimaryPage, savedFeedState.tab) } // Restore Discussions state if (savedFeedState?.discussionsState && savedPrimaryPage === 'discussions') { logger.info('PageManager: Restoring Discussions state', { page: savedPrimaryPage, discussionsState: savedFeedState.discussionsState }) window.dispatchEvent(new CustomEvent('restoreDiscussionsState', { detail: { page: savedPrimaryPage, discussionsState: savedFeedState.discussionsState } })) } // Restore trending tab for search page if (savedFeedState?.trendingTab && savedPrimaryPage === 'search') { logger.info('PageManager: Restoring trending tab', { page: savedPrimaryPage, trendingTab: savedFeedState.trendingTab }) window.dispatchEvent(new CustomEvent('restorePageTab', { detail: { page: 'search', tab: savedFeedState.trendingTab } })) currentTabStateRef.current.set('search', savedFeedState.trendingTab) } } } const goBack = () => { // Special handling for settings sub-pages - go back to main settings page if (primaryViewType === 'settings-sub') { window.history.pushState(null, '', '/settings') setPrimaryNoteView(, 'settings') } else if (primaryViewType === 'following' || primaryViewType === 'mute' || primaryViewType === 'others-relay-settings') { // Special handling for profile sub-pages - go back to main profile page const currentPath = window.location.pathname const profileId = currentPath.replace('/users/', '').replace('/following', '').replace('/muted', '').replace('/relays', '') const profileUrl = `/users/${profileId}` window.history.pushState(null, '', profileUrl) setPrimaryNoteView(, 'profile') } else { // Use browser's back functionality for other pages window.history.back() } } // Drawer handlers const openDrawer = useCallback((noteId: string) => { setDrawerNoteId(noteId) setDrawerOpen(true) }, []) const closeDrawer = useCallback(() => { if (!drawerOpen) return // Already closed setDrawerOpen(false) // Don't clear noteId here - let onOpenChange handle it when animation completes }, [drawerOpen]) const ignorePopStateRef = useRef(false) // Handle browser back button for primary note view useEffect(() => { const handlePopState = () => { if (ignorePopStateRef.current) { ignorePopStateRef.current = false return } // If we have a primary note view open (and drawer is not open), close it if (primaryNoteView && !drawerOpen) { setPrimaryNoteView(null) } } window.addEventListener('popstate', handlePopState) return () => window.removeEventListener('popstate', handlePopState) }, [primaryNoteView, drawerOpen]) useEffect(() => { if (['/npub1', '/nprofile1'].some((prefix) => window.location.pathname.startsWith(prefix))) { window.history.replaceState( null, '', '/users' + window.location.pathname + window.location.search + window.location.hash ) } else if ( ['/note1', '/nevent1', '/naddr1'].some((prefix) => window.location.pathname.startsWith(prefix) ) ) { window.history.replaceState( null, '', '/notes' + window.location.pathname + window.location.search + window.location.hash ) } window.history.pushState(null, '', window.location.href) if (window.location.pathname !== '/') { const url = window.location.pathname + window.location.search + window.location.hash const pathname = window.location.pathname // Check if this is a note URL - handle both /notes/{id} and /{context}/notes/{id} const contextualNoteMatch = pathname.match(/\/(discussions|search|profile|explore|notifications)\/notes\/(.+)$/) const standardNoteMatch = pathname.match(/\/notes\/(.+)$/) const noteUrlMatch = contextualNoteMatch || standardNoteMatch if (noteUrlMatch) { const noteId = noteUrlMatch[noteUrlMatch.length - 1].split('?')[0].split('#')[0] if (noteId) { // If this is a contextual note URL, set the primary page first if (contextualNoteMatch) { const pageContext = contextualNoteMatch[1] as TPrimaryPageName if (pageContext in getPrimaryPageMap()) { // Open drawer immediately, then load background page asynchronously // This prevents the background page loading from blocking the drawer if (isSmallScreen || panelMode === 'single') { // Single-pane mode or mobile: open drawer first openDrawer(noteId) // Load background page asynchronously after drawer opens setTimeout(() => { setCurrentPrimaryPage(pageContext) setPrimaryPages((prev) => { const exists = prev.find((p) => p.name === pageContext) if (!exists) { return [...prev, { name: pageContext, element: getPrimaryPageMap()[pageContext] }] } return prev }) setSavedPrimaryPage(pageContext) }, 0) return } else { // Double-pane mode: set page immediately (no drawer) setCurrentPrimaryPage(pageContext) setPrimaryPages((prev) => { const exists = prev.find((p) => p.name === pageContext) if (!exists) { return [...prev, { name: pageContext, element: getPrimaryPageMap()[pageContext] }] } return prev }) setSavedPrimaryPage(pageContext) } } } // Build contextual URL based on current page (for both single and double-pane) const contextualUrl = buildNoteUrl(noteId, currentPrimaryPage) // Check pane mode to determine how to open the note if (isSmallScreen || panelMode === 'single') { // Single-pane mode or mobile: open in drawer openDrawer(noteId) // Update URL to contextual URL if different if (url !== contextualUrl) { window.history.replaceState(null, '', contextualUrl) } return } else { // Double-pane mode: push to secondary stack with contextual URL setSecondaryStack((prevStack) => { if (isCurrentPage(prevStack, contextualUrl)) return prevStack const { newStack, newItem } = pushNewPageToStack( prevStack, contextualUrl, maxStackSize, window.history.state?.index ) if (newItem) { window.history.replaceState({ index: newItem.index, url: contextualUrl }, '', contextualUrl) } return newStack }) return } } } // Check if this is a primary page URL - don't push primary pages to secondary stack const pathnameOnly = pathname.split('?')[0].split('#')[0] const isPrimaryPageUrl = pathnameOnly === '/' || pathnameOnly === '/home' || (pathnameOnly.startsWith('/') && pathnameOnly.slice(1).split('/')[0] in getPrimaryPageMap() && !pathnameOnly.match(/^\/(notes|users|relays|settings|profile-editor|mutes|follow-packs)/)) if (isPrimaryPageUrl) { // This is a primary page - just navigate to it, don't push to secondary stack const pageName = pathnameOnly === '/' || pathnameOnly === '/home' ? 'home' : pathnameOnly.slice(1).split('/')[0] as TPrimaryPageName if (pageName in getPrimaryPageMap()) { navigatePrimaryPage(pageName) } return } // For relay URLs and other non-note URLs, push to secondary stack // (will be rendered in drawer in single-pane mode, side panel in double-pane mode) setSecondaryStack((prevStack) => { if (isCurrentPage(prevStack, url)) return prevStack const { newStack, newItem } = pushNewPageToStack( prevStack, url, maxStackSize, window.history.state?.index ) if (newItem) { window.history.replaceState({ index: newItem.index, url }, '', url) } return newStack }) } else { // Check for relay URL in query params (legacy support) const searchParams = new URLSearchParams(window.location.search) const r = searchParams.get('r') if (r) { const url = normalizeUrl(r) if (url) { navigatePrimaryPage('relay', { url }) return } } // Parse pathname to determine primary page const pathname: string = window.location.pathname // Handle dedicated paths for primary pages if (pathname === '/' || pathname === '/home') { navigatePrimaryPage('home') } else { // Check if pathname matches a primary page name // First, check if it's a contextual note URL (e.g., /discussions/notes/...) const contextualNoteMatch = pathname.match(/^\/(discussions|search|profile|explore|notifications)\/notes\//) if (contextualNoteMatch) { // Extract the page context from the URL const pageContext = contextualNoteMatch[1] as TPrimaryPageName if (pageContext in getPrimaryPageMap()) { navigatePrimaryPage(pageContext) // The note URL will be handled by the note URL parsing above } return } // Check if it's a standard primary page path const pageName: string = pathname.slice(1).split('/')[0] // Get first segment after slash if (pageName && pageName in getPrimaryPageMap()) { // For relay page, check if there's a URL prop if (pageName === 'relay') { // Relay URLs are handled via secondary routing, not primary pages // This should be caught earlier in the URL parsing } else { navigatePrimaryPage(pageName as TPrimaryPageName) } } // If pathname doesn't match a primary page, it might be a secondary route // which is handled elsewhere } } const onPopState = (e: PopStateEvent) => { if (ignorePopStateRef.current) { ignorePopStateRef.current = false return } const closeModal = modalManager.pop() if (closeModal) { ignorePopStateRef.current = true window.history.forward() return } let state = e.state as { index: number; url: string } | null // Use state.url if available, otherwise fall back to current pathname const urlToCheck = state?.url || window.location.pathname // Check if it's a note URL (we'll update drawer after stack is synced) const noteUrlMatch = urlToCheck.match(/\/(discussions|search|profile|explore|notifications)\/notes\/(.+)$/) || urlToCheck.match(/\/notes\/(.+)$/) const noteIdToShow = noteUrlMatch ? noteUrlMatch[noteUrlMatch.length - 1].split('?')[0].split('#')[0] : null // If not a note URL and drawer is open - close the drawer immediately // Only in single-pane mode or mobile if (!noteIdToShow && drawerOpen && (isSmallScreen || panelMode === 'single')) { setDrawerOpen(false) setTimeout(() => { setDrawerNoteId(null) // Restore URL to current primary page const pageUrl = currentPrimaryPage === 'home' ? '/' : `/${currentPrimaryPage}` window.history.replaceState(null, '', pageUrl) }, 350) } setSecondaryStack((pre) => { const currentItem = pre[pre.length - 1] as TStackItem | undefined const currentIndex = currentItem?.index if (!state) { if (window.location.pathname + window.location.search + window.location.hash !== '/') { // Just change the URL return pre } else { // Back to root state = { index: -1, url: '/' } } } // Go forward if (currentIndex === undefined || state.index > currentIndex) { const { newStack } = pushNewPageToStack(pre, state.url, maxStackSize) return newStack } if (state.index === currentIndex) { return pre } // Go back const newStack = pre.filter((item) => item.index <= state!.index) const topItem = newStack[newStack.length - 1] as TStackItem | undefined if (!topItem) { // Stack is empty - check if this is a primary page URL or a secondary route const pathname = state.url.split('?')[0].split('#')[0] const isPrimaryPage = pathname === '/' || pathname === '/home' || (pathname.startsWith('/') && pathname.slice(1).split('/')[0] in getPrimaryPageMap() && !pathname.match(/^\/(notes|users|relays|settings|profile-editor|mutes|follow-packs)/)) // If it's a primary page URL, return empty stack (right panel will close) if (isPrimaryPage) { // On mobile or single-pane: if drawer is open, close it if (drawerOpen && (isSmallScreen || panelMode === 'single')) { setDrawerOpen(false) setTimeout(() => { setDrawerNoteId(null) // Ensure URL matches the primary page const pageUrl = pathname === '/' || pathname === '/home' ? '/' : pathname window.history.replaceState(null, '', pageUrl) }, 350) } return [] } // Check if navigating to a note URL (supports both /notes/{id} and /{context}/notes/{id}) const noteUrlMatch = state.url.match(/\/(discussions|search|profile|explore|notifications)\/notes\/(.+)$/) || state.url.match(/\/notes\/(.+)$/) if (noteUrlMatch) { const noteId = noteUrlMatch[noteUrlMatch.length - 1].split('?')[0].split('#')[0] if (noteId) { if (isSmallScreen || panelMode === 'single') { // Single-pane mode or mobile: open in drawer openDrawer(noteId) return pre } // Double-pane mode: continue with stack creation } } // Create a new stack item if it's a secondary route (e.g., /follow-packs, /mutes) const { component, ref } = findAndCreateComponent(state.url, state.index) if (component) { newStack.push({ index: state.index, url: state.url, component, ref }) } else { // No component found - likely a primary page, return empty stack // On mobile or single-pane: if drawer is open, close it if (drawerOpen && (isSmallScreen || panelMode === 'single')) { closeDrawer() } return [] } } else if (!topItem.component) { // Load the component if it's not cached const { component, ref } = findAndCreateComponent(topItem.url, state.index) if (component) { topItem.component = component topItem.ref = ref } } if (newStack.length === 0) { // On mobile or single-pane: if drawer is open, close it if (drawerOpen && (isSmallScreen || panelMode === 'single')) { closeDrawer() } // DO NOT update URL when closing panel - closing should NEVER affect the main page } else if (newStack.length > 0) { // Stack still has items - update drawer to show the top item's note (for mobile/single-pane) // Only update drawer if drawer is currently open (not in the process of closing) if ((isSmallScreen || panelMode === 'single') && drawerOpen && drawerNoteId) { // Extract noteId from top item's URL or from state.url const topItemUrl = newStack[newStack.length - 1]?.url || state?.url if (topItemUrl) { const topNoteUrlMatch = topItemUrl.match(/\/(discussions|search|profile|explore|notifications)\/notes\/(.+)$/) || topItemUrl.match(/\/notes\/(.+)$/) if (topNoteUrlMatch) { const topNoteId = topNoteUrlMatch[topNoteUrlMatch.length - 1].split('?')[0].split('#')[0] if (topNoteId && topNoteId !== drawerNoteId) { // Use setTimeout to ensure drawer update happens after stack state is committed setTimeout(() => { // Double-check drawer is still open before updating if (drawerOpen) { openDrawer(topNoteId) } }, 0) } } } } } // If newStack.length === 0, we're closing - don't reopen the drawer return newStack }) } window.addEventListener('popstate', onPopState) return () => { window.removeEventListener('popstate', onPopState) } }, [isSmallScreen, openDrawer, closeDrawer, panelMode, drawerOpen]) // Listen for tab state changes from components useEffect(() => { const handleTabChange = (e: CustomEvent<{ page: TPrimaryPageName, tab: string }>) => { currentTabStateRef.current.set(e.detail.page, e.detail.tab) logger.debug('PageManager: Tab state updated', { page: e.detail.page, tab: e.detail.tab }) } window.addEventListener('pageTabChanged', handleTabChange as EventListener) return () => { window.removeEventListener('pageTabChanged', handleTabChange as EventListener) } }, []) // Listen for panel mode changes from toggle useEffect(() => { const handlePanelModeChange = (e: CustomEvent<{ mode: 'single' | 'double' }>) => { setPanelMode(e.detail.mode) logger.debug('PageManager: Panel mode changed', { mode: e.detail.mode }) } window.addEventListener('panelModeChanged', handlePanelModeChange as EventListener) return () => { window.removeEventListener('panelModeChanged', handlePanelModeChange as EventListener) } }, []) // Restore tab state when returning to primary page from browser back button useEffect(() => { if (secondaryStack.length === 0 && currentPrimaryPage) { const savedFeedState = savedFeedStateRef.current.get(currentPrimaryPage) // Restore tab state first if (savedFeedState?.tab) { logger.info('PageManager: Browser back - Restoring tab state', { page: currentPrimaryPage, tab: savedFeedState.tab }) window.dispatchEvent(new CustomEvent('restorePageTab', { detail: { page: currentPrimaryPage, tab: savedFeedState.tab } })) // Update ref immediately currentTabStateRef.current.set(currentPrimaryPage, savedFeedState.tab) } // Restore Discussions state if (savedFeedState?.discussionsState && currentPrimaryPage === 'discussions') { logger.info('PageManager: Browser back - Restoring Discussions state', { page: currentPrimaryPage, discussionsState: savedFeedState.discussionsState }) window.dispatchEvent(new CustomEvent('restoreDiscussionsState', { detail: { page: currentPrimaryPage, discussionsState: savedFeedState.discussionsState } })) } // Restore trending tab for search page if (savedFeedState?.trendingTab && currentPrimaryPage === 'search') { logger.info('PageManager: Browser back - Restoring trending tab', { page: currentPrimaryPage, trendingTab: savedFeedState.trendingTab }) window.dispatchEvent(new CustomEvent('restorePageTab', { detail: { page: 'search', tab: savedFeedState.trendingTab } })) currentTabStateRef.current.set('search', savedFeedState.trendingTab) } } }, [secondaryStack.length, currentPrimaryPage]) const navigatePrimaryPage = (page: TPrimaryPageName, props?: any) => { // Clear any primary note view when navigating to a new primary page // This ensures menu clicks always take you to the primary page, not stuck on overlays setPrimaryNoteView(null) // Always clear secondary pages when navigating to a primary page via menu // This ensures clicking menu items always takes you to that page, not stuck on profile/note pages clearSecondaryPages() // Update primary pages and current page setPrimaryPages((prev) => { const exists = prev.find((p) => p.name === page) if (exists && props) { exists.props = props return [...prev] } else if (!exists) { return [...prev, { name: page, element: getPrimaryPageMap()[page], props }] } return prev }) setCurrentPrimaryPage(page) // Update URL for primary pages - use dedicated paths // Home can be either / or /home, but we'll use / for home const newUrl = page === 'home' ? '/' : `/${page}` window.history.pushState(null, '', newUrl) // NEVER scroll to top - feed should maintain scroll position at all times } const pushSecondaryPage = (url: string, index?: number) => { logger.component('PageManager', 'pushSecondaryPage called', { url }) // Save tab state before navigating const currentTab = currentTabStateRef.current.get(currentPrimaryPage) const trendingTab = currentTabStateRef.current.get('search') as 'nostr' | 'relays' | 'hashtags' | undefined if (currentPrimaryPage && (currentTab || trendingTab)) { logger.info('PageManager: Desktop - Saving page state', { page: currentPrimaryPage, tab: currentTab, trendingTab }) savedFeedStateRef.current.set(currentPrimaryPage, { tab: currentTab, trendingTab }) } setSecondaryStack((prevStack) => { logger.component('PageManager', 'Current secondary stack length', { length: prevStack.length }) // For relay pages, clear the stack and start fresh to avoid confusion if (url.startsWith('/relays/')) { logger.component('PageManager', 'Clearing stack for relay navigation') const { newStack, newItem } = pushNewPageToStack([], url, maxStackSize, 0) logger.component('PageManager', 'New stack created', { newStackLength: newStack.length, hasNewItem: !!newItem }) if (newItem) { window.history.pushState({ index: newItem.index, url }, '', url) } return newStack } if (isCurrentPage(prevStack, url)) { logger.component('PageManager', 'Page already exists, not scrolling') // NEVER scroll to top - maintain scroll position return prevStack } logger.component('PageManager', 'Creating new page for URL', { url, prevStackLength: prevStack.length }) const { newStack, newItem } = pushNewPageToStack(prevStack, url, maxStackSize, index) logger.component('PageManager', 'New page created', { newStackLength: newStack.length, prevStackLength: prevStack.length, hasNewItem: !!newItem, newItemUrl: newItem?.url, newItemIndex: newItem?.index }) if (newItem) { window.history.pushState({ index: newItem.index, url }, '', url) } else { logger.error('PageManager', 'Failed to create component for URL - component will not be displayed', { url, path: url.split('?')[0].split('#')[0] }) } return newStack }) } const popSecondaryPage = () => { // In double-pane mode, never open drawer - just pop from stack if (panelMode === 'double' && !isSmallScreen) { if (secondaryStack.length === 1) { // Just close the panel - DO NOT change the main page or URL // Closing panel should NEVER affect the main page setSecondaryStack([]) const savedFeedState = savedFeedStateRef.current.get(currentPrimaryPage) // Restore tab state first if (savedFeedState?.tab) { logger.info('PageManager: Desktop - Restoring tab state', { page: currentPrimaryPage, tab: savedFeedState.tab }) window.dispatchEvent(new CustomEvent('restorePageTab', { detail: { page: currentPrimaryPage, tab: savedFeedState.tab } })) currentTabStateRef.current.set(currentPrimaryPage, savedFeedState.tab) } // Restore Discussions state if (savedFeedState?.discussionsState && currentPrimaryPage === 'discussions') { logger.info('PageManager: Desktop - Restoring Discussions state', { page: currentPrimaryPage, discussionsState: savedFeedState.discussionsState }) window.dispatchEvent(new CustomEvent('restoreDiscussionsState', { detail: { page: currentPrimaryPage, discussionsState: savedFeedState.discussionsState } })) } // Restore trending tab for search page if (savedFeedState?.trendingTab && currentPrimaryPage === 'search') { logger.info('PageManager: Desktop - Restoring trending tab', { page: currentPrimaryPage, trendingTab: savedFeedState.trendingTab }) window.dispatchEvent(new CustomEvent('restorePageTab', { detail: { page: 'search', tab: savedFeedState.trendingTab } })) currentTabStateRef.current.set('search', savedFeedState.trendingTab) } } else if (secondaryStack.length > 1) { // Pop from stack directly instead of using history.go(-1) // This ensures the stack is updated immediately setSecondaryStack((prevStack) => { const newStack = prevStack.slice(0, -1) const topItem = newStack[newStack.length - 1] if (topItem) { // Update URL to match the top item window.history.replaceState({ index: topItem.index, url: topItem.url }, '', topItem.url) } return newStack }) } else { // Just go back in history - popstate will handle stack update window.history.go(-1) } return } // Single-pane mode or mobile: check if drawer is open and stack is empty - close drawer instead if (drawerOpen && secondaryStack.length === 0) { // Close drawer and reveal the background page setDrawerOpen(false) setTimeout(() => setDrawerNoteId(null), 350) return } // On mobile or single-pane: if stack has 1 item and drawer is open, close drawer and clear stack if ((isSmallScreen || panelMode === 'single') && secondaryStack.length === 1 && drawerOpen) { // Close drawer (this will restore the URL to the correct primary page) setDrawerOpen(false) setTimeout(() => setDrawerNoteId(null), 350) // Clear stack setSecondaryStack([]) const savedFeedState = savedFeedStateRef.current.get(currentPrimaryPage) // Restore tab state first if (savedFeedState?.tab) { logger.info('PageManager: Mobile/Single-pane - Restoring tab state', { page: currentPrimaryPage, tab: savedFeedState.tab }) window.dispatchEvent(new CustomEvent('restorePageTab', { detail: { page: currentPrimaryPage, tab: savedFeedState.tab } })) currentTabStateRef.current.set(currentPrimaryPage, savedFeedState.tab) } // Restore Discussions state if (savedFeedState?.discussionsState && currentPrimaryPage === 'discussions') { logger.info('PageManager: Mobile/Single-pane - Restoring Discussions state', { page: currentPrimaryPage, discussionsState: savedFeedState.discussionsState }) window.dispatchEvent(new CustomEvent('restoreDiscussionsState', { detail: { page: currentPrimaryPage, discussionsState: savedFeedState.discussionsState } })) } // Restore trending tab for search page if (savedFeedState?.trendingTab && currentPrimaryPage === 'search') { logger.info('PageManager: Mobile/Single-pane - Restoring trending tab', { page: currentPrimaryPage, trendingTab: savedFeedState.trendingTab }) window.dispatchEvent(new CustomEvent('restorePageTab', { detail: { page: 'search', tab: savedFeedState.trendingTab } })) currentTabStateRef.current.set('search', savedFeedState.trendingTab) } return } if (secondaryStack.length === 1) { // Just close the panel - DO NOT change the main page or URL // Closing panel should NEVER affect the main page setSecondaryStack([]) const savedFeedState = savedFeedStateRef.current.get(currentPrimaryPage) // Restore tab state first if (savedFeedState?.tab) { logger.info('PageManager: Desktop - Restoring tab state', { page: currentPrimaryPage, tab: savedFeedState.tab }) window.dispatchEvent(new CustomEvent('restorePageTab', { detail: { page: currentPrimaryPage, tab: savedFeedState.tab } })) currentTabStateRef.current.set(currentPrimaryPage, savedFeedState.tab) } // Restore Discussions state if (savedFeedState?.discussionsState && currentPrimaryPage === 'discussions') { logger.info('PageManager: Desktop - Restoring Discussions state', { page: currentPrimaryPage, discussionsState: savedFeedState.discussionsState }) window.dispatchEvent(new CustomEvent('restoreDiscussionsState', { detail: { page: currentPrimaryPage, discussionsState: savedFeedState.discussionsState } })) } // Restore trending tab for search page if (savedFeedState?.trendingTab && currentPrimaryPage === 'search') { logger.info('PageManager: Desktop - Restoring trending tab', { page: currentPrimaryPage, trendingTab: savedFeedState.trendingTab }) window.dispatchEvent(new CustomEvent('restorePageTab', { detail: { page: 'search', tab: savedFeedState.trendingTab } })) currentTabStateRef.current.set('search', savedFeedState.trendingTab) } } else { window.history.go(-1) } } const clearSecondaryPages = () => { if (secondaryStack.length === 0) return // Capture the length before clearing const stackLength = secondaryStack.length // Clear the state immediately for instant navigation setSecondaryStack([]) // Also update browser history to keep it in sync window.history.go(-stackLength) } if (isSmallScreen) { return ( navigationCounterRef.current }}> {primaryNoteView ? ( // Show primary note view with back button on mobile
Imwald
{primaryNoteView}
) : ( <> {!!secondaryStack.length && secondaryStack.map((item, index) => { const isLast = index === secondaryStack.length - 1 logger.component('PageManager', 'Rendering secondary stack item', { index, isLast, url: item.url, hasComponent: !!item.component, display: isLast ? 'block' : 'none' }) return (
{item.component}
) })} {primaryPages.map(({ name, element, props }) => (
{props ? cloneElement(element as React.ReactElement, props) : element}
))} )} {drawerNoteId && ( { setDrawerOpen(open) // Only clear noteId when Sheet is fully closed (after animation completes) // Use 350ms to ensure animation is fully done (animation is 300ms) if (!open) { // Restore URL to current primary page const pageUrl = currentPrimaryPage === 'home' ? '/' : `/${currentPrimaryPage}` window.history.replaceState(null, '', pageUrl) setTimeout(() => setDrawerNoteId(null), 350) } }} noteId={drawerNoteId} /> )}
) } return ( navigationCounterRef.current }}>
{(() => { if (panelMode === 'double') { // Double-pane mode: show feed on left (flexible, maintains width), secondary stack on right (1042px, same as drawer) return (
{/* Left panel: Feed (flexible, takes remaining space after 1042px) */}
{/* Right panel: Secondary stack (1042px fixed width, same as drawer) */}
{secondaryStack.length > 0 ? ( secondaryStack.map((item, index) => { const isLast = index === secondaryStack.length - 1 return (
{item.component}
) }) ) : (
{/* Empty state - no secondary content */}
)}
) } else { // Single-pane mode: show feed only, drawer overlay for notes return ( ) } })()}
{drawerNoteId && ( { setDrawerOpen(open) // Only clear noteId when Sheet is fully closed (after animation completes) // Use 350ms to ensure animation is fully done (animation is 300ms) if (!open) { // Restore URL to current primary page const pageUrl = currentPrimaryPage === 'home' ? '/' : `/${currentPrimaryPage}` window.history.replaceState(null, '', pageUrl) setTimeout(() => setDrawerNoteId(null), 350) } }} noteId={drawerNoteId} /> )} {/* Generic drawer for secondary stack in single-pane mode (for relay pages, etc.) */} {panelMode === 'single' && !isSmallScreen && secondaryStack.length > 0 && !drawerOpen && ( { if (!open) { // Close drawer and go back popSecondaryPage() } }} >
{secondaryStack.map((item, index) => { const isLast = index === secondaryStack.length - 1 if (!isLast) return null return (
{item.component}
) })}
)}
) } export function SecondaryPageLink({ to, children, className, onClick }: { to: string children: React.ReactNode className?: string onClick?: (e: React.MouseEvent) => void }) { const { push } = useSecondaryPage() return ( { if (onClick) { onClick(e) } push(to) }} > {children} ) } function isCurrentPage(stack: TStackItem[], url: string) { const currentPage = stack[stack.length - 1] if (!currentPage) return false logger.component('PageManager', 'isCurrentPage check', { currentUrl: currentPage.url, newUrl: url, match: currentPage.url === url }) return currentPage.url === url } function findAndCreateComponent(url: string, index: number) { const path = url.split('?')[0].split('#')[0] logger.component('PageManager', 'findAndCreateComponent called', { url, path, routes: routes.length }) for (const { matcher, element } of routes) { const match = matcher(path) logger.component('PageManager', 'Trying route matcher', { path, matchResult: !!match, matchParams: match ? (match as any).params : null }) if (!match) continue if (!element) { logger.component('PageManager', 'No element for this route', { path }) return {} } const ref = createRef() // Decode URL parameters for relay pages const params = { ...(match as any).params } if (params.url && typeof params.url === 'string') { params.url = decodeURIComponent(params.url) logger.component('PageManager', 'Decoded URL parameter', { url: params.url }) } logger.component('PageManager', 'Creating component with params', { params, index }) try { const component = cloneElement(element, { ...params, index, ref } as any) logger.component('PageManager', 'Component created successfully', { hasComponent: !!component }) return { component, ref } } catch (error) { logger.error('PageManager', 'Error creating component', { error, params }) return {} } } logger.component('PageManager', 'No matching route found', { path, url }) return {} } function pushNewPageToStack( stack: TStackItem[], url: string, maxStackSize = 5, specificIndex?: number ) { const currentItem = stack[stack.length - 1] const currentIndex = specificIndex ?? (currentItem ? currentItem.index + 1 : 0) const { component, ref } = findAndCreateComponent(url, currentIndex) if (!component) { logger.error('PageManager', 'pushNewPageToStack: No component created', { url, currentIndex, path: url.split('?')[0].split('#')[0] }) return { newStack: stack, newItem: null } } const newItem = { component, ref, url, index: currentIndex } const newStack = [...stack, newItem] const lastCachedIndex = newStack.findIndex((stack) => stack.component) // Clear the oldest cached component if there are too many cached components if (newStack.length - lastCachedIndex > maxStackSize) { newStack[lastCachedIndex].component = null } logger.component('PageManager', 'pushNewPageToStack: Success', { url, newStackLength: newStack.length, newItemIndex: currentIndex }) return { newStack, newItem } }