You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1739 lines
71 KiB
1739 lines
71 KiB
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<TPageRef> | null |
|
} |
|
|
|
const PRIMARY_PAGE_REF_MAP = { |
|
home: createRef<TPageRef>(), |
|
explore: createRef<TPageRef>(), |
|
notifications: createRef<TPageRef>(), |
|
me: createRef<TPageRef>(), |
|
profile: createRef<TPageRef>(), |
|
relay: createRef<TPageRef>(), |
|
search: createRef<TPageRef>(), |
|
discussions: createRef<TPageRef>() |
|
} |
|
|
|
// 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: <NoteListPage ref={PRIMARY_PAGE_REF_MAP.home} />, |
|
explore: <ExplorePage ref={PRIMARY_PAGE_REF_MAP.explore} />, |
|
notifications: <NotificationListPage ref={PRIMARY_PAGE_REF_MAP.notifications} />, |
|
me: <MePage ref={PRIMARY_PAGE_REF_MAP.me} />, |
|
profile: <ProfilePage ref={PRIMARY_PAGE_REF_MAP.profile} />, |
|
relay: <RelayPage ref={PRIMARY_PAGE_REF_MAP.relay} />, |
|
search: <SearchPage ref={PRIMARY_PAGE_REF_MAP.search} />, |
|
discussions: <DiscussionsPage ref={PRIMARY_PAGE_REF_MAP.discussions} /> |
|
}) |
|
|
|
// Type for primary page names - use the return type of getPrimaryPageMap |
|
export type TPrimaryPageName = keyof ReturnType<typeof getPrimaryPageMap> |
|
|
|
export const PrimaryPageContext = createContext<TPrimaryPageContext | undefined>(undefined) |
|
|
|
const SecondaryPageContext = createContext<TSecondaryPageContext | undefined>(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(<SecondaryRelayPage url={relayUrl} index={0} hideTitlebar={true} />, '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(<SecondaryProfilePage id={profileId} index={0} hideTitlebar={true} />, '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(<SecondaryProfilePage id={profileId} index={0} hideTitlebar={true} />, '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(<SecondaryNoteListPage key={key} hideTitlebar={true} />, '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(<FollowingListPage id={profileId} index={0} hideTitlebar={true} />, '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(<MuteListPage index={0} hideTitlebar={true} />, '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(<OthersRelaySettingsPage id={profileId} index={0} hideTitlebar={true} />, '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(<SettingsPage key="settings" index={0} hideTitlebar={true} />, 'settings') |
|
} else if (url.startsWith('/settings/relays')) { |
|
window.history.pushState(null, '', url) |
|
setPrimaryNoteView(<RelaySettingsPage key="relay-settings" index={0} hideTitlebar={true} />, 'settings-sub') |
|
} else if (url === '/settings/wallet') { |
|
window.history.pushState(null, '', url) |
|
setPrimaryNoteView(<WalletPage key="wallet" index={0} hideTitlebar={true} />, 'settings-sub') |
|
} else if (url === '/settings/posts') { |
|
window.history.pushState(null, '', url) |
|
setPrimaryNoteView(<PostSettingsPage key="post-settings" index={0} hideTitlebar={true} />, 'settings-sub') |
|
} else if (url === '/settings/general') { |
|
window.history.pushState(null, '', url) |
|
setPrimaryNoteView(<GeneralSettingsPage key="general-settings" index={0} hideTitlebar={true} />, 'settings-sub') |
|
} else if (url === '/settings/translation') { |
|
window.history.pushState(null, '', url) |
|
setPrimaryNoteView(<TranslationPage key="translation" index={0} hideTitlebar={true} />, 'settings-sub') |
|
} else if (url === '/settings/rss-feeds') { |
|
window.history.pushState(null, '', url) |
|
setPrimaryNoteView(<RssFeedSettingsPage key="rss-feed-settings" index={0} hideTitlebar={true} />, '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 ( |
|
<div className="grid grid-cols-1 gap-2 w-full pr-2 py-2"> |
|
<div className="rounded-lg shadow-lg bg-background overflow-hidden"> |
|
{primaryNoteView ? ( |
|
// Show note view with back button |
|
<div className="flex flex-col h-full w-full"> |
|
<div className="flex justify-center py-1 border-b"> |
|
<span className="text-green-600 dark:text-green-500 font-semibold text-sm"> |
|
Imwald |
|
</span> |
|
</div> |
|
<div className="flex gap-1 p-1 items-center justify-between font-semibold border-b"> |
|
<div className="flex items-center flex-1 w-0"> |
|
<Button |
|
className="flex gap-1 items-center w-fit max-w-full justify-start pl-2 pr-3" |
|
variant="ghost" |
|
size="titlebar-icon" |
|
title="Back" |
|
onClick={goBack} |
|
> |
|
<ChevronLeft /> |
|
<div className="truncate text-lg font-semibold"> |
|
Back |
|
</div> |
|
</Button> |
|
</div> |
|
<div className="flex-1 flex justify-center"> |
|
<div className="text-lg font-semibold"> |
|
{getPageTitle(primaryViewType, window.location.pathname)} |
|
</div> |
|
</div> |
|
<div className="flex-1 w-0"></div> |
|
</div> |
|
<div className="flex-1 overflow-auto"> |
|
{primaryNoteView} |
|
</div> |
|
</div> |
|
) : ( |
|
// Show normal primary pages |
|
primaryPages.map(({ name, element, props }) => { |
|
const isCurrentPage = currentPrimaryPage === name |
|
logger.debug(`Primary page ${name}:`, { isCurrentPage, currentPrimaryPage }) |
|
return ( |
|
<div |
|
key={name} |
|
className="flex flex-col h-full w-full" |
|
style={{ |
|
display: isCurrentPage ? 'block' : 'none' |
|
}} |
|
> |
|
{(() => { |
|
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 <div>Error rendering {name}: {error instanceof Error ? error.message : String(error)}</div> |
|
} |
|
})()} |
|
</div> |
|
) |
|
}) |
|
)} |
|
</div> |
|
{/* DEPRECATED: Secondary panel removed - double-panel functionality disabled */} |
|
</div> |
|
) |
|
} |
|
|
|
export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { |
|
const { isSmallScreen } = useScreenSize() |
|
// DEPRECATED: showRecommendedRelaysPanel removed - double-panel functionality disabled |
|
const [currentPrimaryPage, setCurrentPrimaryPage] = useState<TPrimaryPageName>('home') |
|
const [primaryPages, setPrimaryPages] = useState< |
|
{ name: TPrimaryPageName; element: ReactNode; props?: any }[] |
|
>([ |
|
{ |
|
name: 'home', |
|
element: getPrimaryPageMap().home |
|
} |
|
]) |
|
const [secondaryStack, setSecondaryStack] = useState<TStackItem[]>([]) |
|
const [primaryNoteView, setPrimaryNoteViewState] = useState<ReactNode | null>(null) |
|
const [primaryViewType, setPrimaryViewType] = useState<'note' | 'settings' | 'settings-sub' | 'profile' | 'hashtag' | 'relay' | 'following' | 'mute' | 'others-relay-settings' | null>(null) |
|
const [savedPrimaryPage, setSavedPrimaryPage] = useState<TPrimaryPageName | null>(null) |
|
const [drawerOpen, setDrawerOpen] = useState(false) |
|
const [drawerNoteId, setDrawerNoteId] = useState<string | null>(null) |
|
const [panelMode, setPanelMode] = useState<'single' | 'double'>(() => storage.getPanelMode()) |
|
const navigationCounterRef = useRef(0) |
|
const savedFeedStateRef = useRef<Map<TPrimaryPageName, { |
|
tab?: string, |
|
discussionsState?: { selectedTopic: string, timeSpan: '30days' | '90days' | 'all' }, |
|
trendingTab?: 'nostr' | 'relays' | 'hashtags' |
|
}>>(new Map()) |
|
const currentTabStateRef = useRef<Map<TPrimaryPageName, string>>(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(<SettingsPage index={0} hideTitlebar={true} />, '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(<SecondaryProfilePage id={profileId} index={0} hideTitlebar={true} />, '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 ( |
|
<PrimaryPageContext.Provider |
|
value={{ |
|
navigate: navigatePrimaryPage, |
|
current: currentPrimaryPage, |
|
display: secondaryStack.length === 0 |
|
}} |
|
> |
|
<SecondaryPageContext.Provider |
|
value={{ |
|
push: pushSecondaryPage, |
|
pop: popSecondaryPage, |
|
currentIndex: secondaryStack.length |
|
? secondaryStack[secondaryStack.length - 1].index |
|
: 0, |
|
navigateToPrimaryPage: navigatePrimaryPage |
|
}} |
|
> |
|
<CurrentRelaysProvider> |
|
<NotificationProvider> |
|
<PrimaryNoteViewContext.Provider value={{ setPrimaryNoteView, primaryViewType, getNavigationCounter: () => navigationCounterRef.current }}> |
|
<NoteDrawerContext.Provider value={{ openDrawer, closeDrawer, isDrawerOpen: drawerOpen, drawerNoteId }}> |
|
{primaryNoteView ? ( |
|
// Show primary note view with back button on mobile |
|
<div className="flex flex-col h-full w-full"> |
|
<div className="flex justify-center py-1 border-b"> |
|
<span className="text-green-600 dark:text-green-500 font-semibold text-sm"> |
|
Imwald |
|
</span> |
|
</div> |
|
<div className="flex gap-1 p-1 items-center justify-between font-semibold border-b"> |
|
<div className="flex items-center flex-1 w-0"> |
|
<Button |
|
className="flex gap-1 items-center w-fit max-w-full justify-start pl-2 pr-3" |
|
variant="ghost" |
|
size="titlebar-icon" |
|
title="Back to feed" |
|
onClick={() => setPrimaryNoteView(null)} |
|
> |
|
<ChevronLeft /> |
|
<div className="truncate text-lg font-semibold"> |
|
{primaryViewType === 'settings' ? 'Settings' : |
|
primaryViewType === 'settings-sub' ? 'Settings' : |
|
primaryViewType === 'profile' ? 'Back' : |
|
primaryViewType === 'hashtag' ? 'Hashtag' : |
|
primaryViewType === 'note' ? getPageTitle(primaryViewType, window.location.pathname) : 'Note'} |
|
</div> |
|
</Button> |
|
</div> |
|
</div> |
|
<div className="flex-1 overflow-auto"> |
|
{primaryNoteView} |
|
</div> |
|
</div> |
|
) : ( |
|
<> |
|
{!!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 ( |
|
<div |
|
key={item.index} |
|
style={{ |
|
display: isLast ? 'block' : 'none' |
|
}} |
|
> |
|
{item.component} |
|
</div> |
|
) |
|
})} |
|
{primaryPages.map(({ name, element, props }) => ( |
|
<div |
|
key={name} |
|
style={{ |
|
display: |
|
secondaryStack.length === 0 && currentPrimaryPage === name ? 'block' : 'none' |
|
}} |
|
> |
|
{props ? cloneElement(element as React.ReactElement, props) : element} |
|
</div> |
|
))} |
|
</> |
|
)} |
|
{drawerNoteId && ( |
|
<NoteDrawer |
|
open={drawerOpen} |
|
onOpenChange={(open) => { |
|
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} |
|
/> |
|
)} |
|
<BottomNavigationBar /> |
|
<TooManyRelaysAlertDialog /> |
|
<CreateWalletGuideToast /> |
|
</NoteDrawerContext.Provider> |
|
</PrimaryNoteViewContext.Provider> |
|
</NotificationProvider> |
|
</CurrentRelaysProvider> |
|
</SecondaryPageContext.Provider> |
|
</PrimaryPageContext.Provider> |
|
) |
|
} |
|
|
|
return ( |
|
<PrimaryPageContext.Provider |
|
value={{ |
|
navigate: navigatePrimaryPage, |
|
current: currentPrimaryPage, |
|
display: true |
|
}} |
|
> |
|
<SecondaryPageContext.Provider |
|
value={{ |
|
push: pushSecondaryPage, |
|
pop: popSecondaryPage, |
|
currentIndex: secondaryStack.length ? secondaryStack[secondaryStack.length - 1].index : 0, |
|
navigateToPrimaryPage: navigatePrimaryPage |
|
}} |
|
> |
|
<CurrentRelaysProvider> |
|
<NotificationProvider> |
|
<PrimaryNoteViewContext.Provider value={{ setPrimaryNoteView, primaryViewType, getNavigationCounter: () => navigationCounterRef.current }}> |
|
<NoteDrawerContext.Provider value={{ openDrawer, closeDrawer, isDrawerOpen: drawerOpen, drawerNoteId }}> |
|
<div className="flex flex-col items-center bg-surface-background"> |
|
<div |
|
className="flex h-[var(--vh)] w-full bg-surface-background" |
|
style={{ |
|
maxWidth: '1920px' |
|
}} |
|
> |
|
<Sidebar /> |
|
{(() => { |
|
if (panelMode === 'double') { |
|
// Double-pane mode: show feed on left (flexible, maintains width), secondary stack on right (1042px, same as drawer) |
|
return ( |
|
<div className="flex-1 flex overflow-hidden"> |
|
{/* Left panel: Feed (flexible, takes remaining space after 1042px) */} |
|
<div className="flex-1 min-w-0 overflow-auto border-r"> |
|
<MainContentArea |
|
primaryPages={primaryPages} |
|
currentPrimaryPage={currentPrimaryPage} |
|
primaryNoteView={primaryNoteView} |
|
primaryViewType={primaryViewType} |
|
goBack={goBack} |
|
/> |
|
</div> |
|
{/* Right panel: Secondary stack (1042px fixed width, same as drawer) */} |
|
<div className="w-[1042px] shrink-0 overflow-auto"> |
|
{secondaryStack.length > 0 ? ( |
|
secondaryStack.map((item, index) => { |
|
const isLast = index === secondaryStack.length - 1 |
|
return ( |
|
<div |
|
key={item.index} |
|
style={{ |
|
display: isLast ? 'block' : 'none' |
|
}} |
|
> |
|
{item.component} |
|
</div> |
|
) |
|
}) |
|
) : ( |
|
<div className="h-full flex items-center justify-center text-muted-foreground"> |
|
{/* Empty state - no secondary content */} |
|
</div> |
|
)} |
|
</div> |
|
</div> |
|
) |
|
} else { |
|
// Single-pane mode: show feed only, drawer overlay for notes |
|
return ( |
|
<MainContentArea |
|
primaryPages={primaryPages} |
|
currentPrimaryPage={currentPrimaryPage} |
|
primaryNoteView={primaryNoteView} |
|
primaryViewType={primaryViewType} |
|
goBack={goBack} |
|
/> |
|
) |
|
} |
|
})()} |
|
</div> |
|
</div> |
|
{drawerNoteId && ( |
|
<NoteDrawer |
|
open={drawerOpen} |
|
onOpenChange={(open) => { |
|
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 && ( |
|
<Sheet |
|
open={true} |
|
onOpenChange={(open) => { |
|
if (!open) { |
|
// Close drawer and go back |
|
popSecondaryPage() |
|
} |
|
}} |
|
> |
|
<SheetContent side="right" className="w-full sm:max-w-[1042px] overflow-y-auto p-0"> |
|
<div className="h-full"> |
|
{secondaryStack.map((item, index) => { |
|
const isLast = index === secondaryStack.length - 1 |
|
if (!isLast) return null |
|
return ( |
|
<div key={item.index}> |
|
{item.component} |
|
</div> |
|
) |
|
})} |
|
</div> |
|
</SheetContent> |
|
</Sheet> |
|
)} |
|
<TooManyRelaysAlertDialog /> |
|
<CreateWalletGuideToast /> |
|
</NoteDrawerContext.Provider> |
|
</PrimaryNoteViewContext.Provider> |
|
</NotificationProvider> |
|
</CurrentRelaysProvider> |
|
</SecondaryPageContext.Provider> |
|
</PrimaryPageContext.Provider> |
|
) |
|
} |
|
|
|
export function SecondaryPageLink({ |
|
to, |
|
children, |
|
className, |
|
onClick |
|
}: { |
|
to: string |
|
children: React.ReactNode |
|
className?: string |
|
onClick?: (e: React.MouseEvent) => void |
|
}) { |
|
const { push } = useSecondaryPage() |
|
|
|
return ( |
|
<span |
|
className={cn('cursor-pointer', className)} |
|
onClick={(e) => { |
|
if (onClick) { |
|
onClick(e) |
|
} |
|
push(to) |
|
}} |
|
> |
|
{children} |
|
</span> |
|
) |
|
} |
|
|
|
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<TPageRef>() |
|
|
|
// 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 } |
|
}
|
|
|