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.
 
 
 
 

362 lines
12 KiB

/**
* Navigation Service
*
* Centralized navigation management for the application.
* Handles all navigation logic in a clean, testable way.
*/
import React, { lazy, ReactNode, Suspense } from 'react'
// Page components
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 RssFeedSettingsPage from '@/pages/secondary/RssFeedSettingsPage'
import FollowSetsSettingsPage from '@/pages/secondary/FollowSetsSettingsPage'
import EmojiSetsSettingsPage from '@/pages/secondary/EmojiSetsSettingsPage'
import CacheSettingsPage from '@/pages/secondary/CacheSettingsPage'
import PersonalListsSettingsPage from '@/pages/secondary/PersonalListsSettingsPage'
import NotePage from '@/pages/secondary/NotePage'
import SecondaryProfilePage from '@/pages/secondary/ProfilePage'
import FollowingListPage from '@/pages/secondary/FollowingListPage'
import MuteListPage from '@/pages/secondary/MuteListPage'
import OthersRelaySettingsPage from '@/pages/secondary/OthersRelaySettingsPage'
import SecondaryRelayPage from '@/pages/secondary/RelayPage'
/** Lazy avoids: NavigationService → NoteListPage → NormalFeed → NoteList → PageManager → navigation.service */
const SecondaryNoteListPageLazy = lazy(() => import('@/pages/secondary/NoteListPage'))
const navLazyFallback = React.createElement(
'div',
{ className: 'flex flex-1 items-center justify-center p-8 text-sm text-muted-foreground' },
'Loading…'
)
export type ViewType =
| 'note'
| 'settings'
| 'settings-sub'
| 'profile'
| 'hashtag'
| 'relay'
| 'following'
| 'mute'
| 'bookmarks'
| 'pins'
| 'interests'
| 'user-emojis'
| 'others-relay-settings'
| null
export interface NavigationContext {
setPrimaryNoteView: (view: ReactNode, type: ViewType) => void
}
export interface NavigationResult {
component: ReactNode
viewType: ViewType
}
/**
* URL parsing utilities
*/
export class URLParser {
static extractNoteId(url: string): string {
return url.replace('/notes/', '')
}
static extractRelayUrl(url: string): string {
return decodeURIComponent(url.replace('/relays/', ''))
}
static extractProfileId(url: string): string {
return url.replace('/users/', '')
}
static extractHashtag(url: string): string {
const searchParams = new URLSearchParams(url.split('?')[1] || '')
return searchParams.get('t') || ''
}
static isSettingsSubPage(url: string): boolean {
return url.startsWith('/settings/') && url !== '/settings'
}
static getSettingsSubPageType(url: string): string {
try {
const pathOnly = url.split('?')[0].split('#')[0]
const parts = pathOnly.split('/').filter(Boolean)
if (parts[0] !== 'settings') return 'general'
const sub = parts[1] ?? ''
const known = new Set([
'general',
'relays',
'wallet',
'posts',
'rss-feeds',
'follow-sets',
'emoji-sets',
'cache',
'personal-lists'
])
return known.has(sub) ? sub : 'general'
} catch {
return 'general'
}
}
}
/**
* Component factory for creating page components
*/
export class ComponentFactory {
static createNotePage(noteId: string): ReactNode {
return React.createElement(NotePage, { id: noteId, index: 0, hideTitlebar: true })
}
static createRelayPage(relayUrl: string): ReactNode {
return React.createElement(SecondaryRelayPage, { url: relayUrl, index: 0, hideTitlebar: true })
}
static createProfilePage(profileId: string): ReactNode {
return React.createElement(SecondaryProfilePage, { id: profileId, index: 0, hideTitlebar: true })
}
static createHashtagPage(): ReactNode {
return React.createElement(
Suspense,
{ fallback: navLazyFallback },
React.createElement(SecondaryNoteListPageLazy, { hideTitlebar: true })
)
}
static createFollowingListPage(profileId: string): ReactNode {
return React.createElement(FollowingListPage, { id: profileId, index: 0, hideTitlebar: true })
}
static createMuteListPage(_profileId: string): ReactNode {
return React.createElement(MuteListPage, { index: 0, hideTitlebar: true })
}
static createOthersRelaySettingsPage(profileId: string): ReactNode {
return React.createElement(OthersRelaySettingsPage, { id: profileId, index: 0, hideTitlebar: true })
}
static createSettingsPage(): ReactNode {
return React.createElement(SettingsPage, { index: 0, hideTitlebar: true })
}
static createSettingsSubPage(type: string): ReactNode {
switch (type) {
case 'relays':
return React.createElement(RelaySettingsPage, { index: 0, hideTitlebar: true })
case 'wallet':
return React.createElement(WalletPage, { index: 0, hideTitlebar: true })
case 'posts':
return React.createElement(PostSettingsPage, { index: 0, hideTitlebar: true })
case 'general':
return React.createElement(GeneralSettingsPage, { index: 0, hideTitlebar: true })
case 'rss-feeds':
return React.createElement(RssFeedSettingsPage, { index: 0, hideTitlebar: true })
case 'follow-sets':
return React.createElement(FollowSetsSettingsPage, { index: 0, hideTitlebar: true })
case 'emoji-sets':
return React.createElement(EmojiSetsSettingsPage, { index: 0, hideTitlebar: true })
case 'cache':
return React.createElement(CacheSettingsPage, { index: 0, hideTitlebar: true })
case 'personal-lists':
return React.createElement(PersonalListsSettingsPage, { index: 0, hideTitlebar: true })
default:
return React.createElement(GeneralSettingsPage, { index: 0, hideTitlebar: true })
}
}
}
/**
* Main navigation service
*/
export class NavigationService {
private context: NavigationContext
constructor(context: NavigationContext) {
this.context = context
}
/**
* Navigate to a note
*/
navigateToNote(url: string): void {
const noteId = URLParser.extractNoteId(url)
const component = ComponentFactory.createNotePage(noteId)
this.updateHistoryAndView(url, component, 'note')
}
/**
* Navigate to a relay
*/
navigateToRelay(url: string): void {
const relayUrl = URLParser.extractRelayUrl(url)
const component = ComponentFactory.createRelayPage(relayUrl)
this.updateHistoryAndView(url, component, 'relay')
}
/**
* Navigate to a profile
*/
navigateToProfile(url: string): void {
const profileId = URLParser.extractProfileId(url)
const component = ComponentFactory.createProfilePage(profileId)
this.updateHistoryAndView(url, component, 'profile')
}
/**
* Navigate to a hashtag page
*/
navigateToHashtag(url: string): void {
const component = ComponentFactory.createHashtagPage()
this.updateHistoryAndView(url, component, 'hashtag')
}
/**
* Navigate to following list
*/
navigateToFollowingList(url: string): void {
const profileId = URLParser.extractProfileId(url.replace('/following', ''))
const component = ComponentFactory.createFollowingListPage(profileId)
this.updateHistoryAndView(url, component, 'following')
}
/**
* Navigate to mute list
*/
navigateToMuteList(url: string): void {
const profileId = URLParser.extractProfileId(url.replace('/muted', ''))
const component = ComponentFactory.createMuteListPage(profileId)
this.updateHistoryAndView(url, component, 'mute')
}
/**
* Navigate to others relay settings
*/
navigateToOthersRelaySettings(url: string): void {
const profileId = URLParser.extractProfileId(url.replace('/relays', ''))
const component = ComponentFactory.createOthersRelaySettingsPage(profileId)
this.updateHistoryAndView(url, component, 'others-relay-settings')
}
/**
* Navigate to settings
*/
navigateToSettings(url: string): void {
if (URLParser.isSettingsSubPage(url)) {
const subPageType = URLParser.getSettingsSubPageType(url)
const component = ComponentFactory.createSettingsSubPage(subPageType)
this.updateHistoryAndView(url, component, 'settings-sub')
} else {
const component = ComponentFactory.createSettingsPage()
this.updateHistoryAndView(url, component, 'settings')
}
}
/**
* Get page title based on view type and URL
*/
getPageTitle(viewType: ViewType, pathname: string): string {
if (viewType === 'settings') return 'Settings'
if (viewType === 'settings-sub') {
if (pathname.includes('/general')) return 'General Settings'
if (pathname.includes('/relays')) return 'Relays and Storage Settings'
if (pathname.includes('/cache')) return 'Cache & offline storage'
if (pathname.includes('/wallet')) return 'Wallet Settings'
if (pathname.includes('/posts')) return 'Post Settings'
if (pathname.includes('/emoji-sets')) return 'Emoji sets'
return 'Settings'
}
if (viewType === 'profile') {
if (pathname.includes('/following')) return 'Following'
if (pathname.includes('/relays')) return 'Relays and Storage Settings'
return 'Profile'
}
if (viewType === 'hashtag') return 'Hashtag'
if (viewType === 'relay') return 'Relay'
if (viewType === 'note') {
// Try to get title from sessionStorage if NotePage has set it
// NotePage will store the title when it determines the event kind
const storedTitle = sessionStorage.getItem('notePageTitle')
if (storedTitle) {
sessionStorage.removeItem('notePageTitle') // Clean up after use
return storedTitle
}
return 'Note'
}
if (viewType === 'following') return 'Following'
if (viewType === 'mute') return 'Muted Users'
if (viewType === 'bookmarks') return 'Bookmarks'
if (viewType === 'pins') return 'Pinned notes'
if (viewType === 'interests') return 'Interests'
if (viewType === 'user-emojis') return 'Custom emoji list'
if (viewType === 'others-relay-settings') return 'Relays and Storage Settings'
return 'Page'
}
/**
* Handle back navigation
*/
handleBackNavigation(viewType: ViewType): void {
if (viewType === 'settings-sub') {
// Navigate back to main settings page
this.navigateToSettings('/settings')
} else {
// Use browser's back functionality
window.history.back()
}
}
/**
* Private helper to update history and view
*/
private updateHistoryAndView(url: string, component: ReactNode, viewType: ViewType): void {
window.history.pushState(null, '', url)
this.context.setPrimaryNoteView(component, viewType)
}
}
/**
* Hook factory for creating navigation hooks
*/
export function createNavigationHook(service: NavigationService) {
return {
useSmartNoteNavigation: () => ({
navigateToNote: (url: string) => service.navigateToNote(url)
}),
useSmartRelayNavigation: () => ({
navigateToRelay: (url: string) => service.navigateToRelay(url)
}),
useSmartProfileNavigation: () => ({
navigateToProfile: (url: string) => service.navigateToProfile(url)
}),
useSmartHashtagNavigation: () => ({
navigateToHashtag: (url: string) => service.navigateToHashtag(url)
}),
useSmartFollowingListNavigation: () => ({
navigateToFollowingList: (url: string) => service.navigateToFollowingList(url)
}),
useSmartMuteListNavigation: () => ({
navigateToMuteList: (url: string) => service.navigateToMuteList(url)
}),
useSmartOthersRelaySettingsNavigation: () => ({
navigateToOthersRelaySettings: (url: string) => service.navigateToOthersRelaySettings(url)
}),
useSmartSettingsNavigation: () => ({
navigateToSettings: (url: string) => service.navigateToSettings(url)
})
}
}