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
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) |
|
}) |
|
} |
|
}
|
|
|