feat: 💨

This commit is contained in:
codytseng 2026-01-06 00:00:51 +08:00
parent 695f2fe017
commit ed843f637a
8 changed files with 198 additions and 197 deletions

View file

@ -10,6 +10,7 @@ import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useDeletedEvent } from '@/providers/DeletedEventProvider' import { useDeletedEvent } from '@/providers/DeletedEventProvider'
import { useMuteList } from '@/providers/MuteListProvider' import { useMuteList } from '@/providers/MuteListProvider'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { usePageActive } from '@/providers/PageActiveProvider'
import { useUserTrust } from '@/providers/UserTrustProvider' import { useUserTrust } from '@/providers/UserTrustProvider'
import client from '@/services/client.service' import client from '@/services/client.service'
import threadService from '@/services/thread.service' import threadService from '@/services/thread.service'
@ -75,6 +76,7 @@ const NoteList = forwardRef<
ref ref
) => { ) => {
const { t } = useTranslation() const { t } = useTranslation()
const active = usePageActive()
const { startLogin } = useNostr() const { startLogin } = useNostr()
const { isSpammer, meetsMinTrustScore } = useUserTrust() const { isSpammer, meetsMinTrustScore } = useUserTrust()
const { mutePubkeySet } = useMuteList() const { mutePubkeySet } = useMuteList()
@ -93,6 +95,12 @@ const NoteList = forwardRef<
const [refreshCount, setRefreshCount] = useState(0) const [refreshCount, setRefreshCount] = useState(0)
const supportTouch = useMemo(() => isTouchDevice(), []) const supportTouch = useMemo(() => isTouchDevice(), [])
const topRef = useRef<HTMLDivElement | null>(null) const topRef = useRef<HTMLDivElement | null>(null)
const sinceRef = useRef<number | undefined>(undefined)
sinceRef.current = newEvents.length
? newEvents[0].created_at + 1
: events.length
? events[0].created_at + 1
: undefined
const showNewNotesDirectlyRef = useRef(showNewNotesDirectly) const showNewNotesDirectlyRef = useRef(showNewNotesDirectly)
showNewNotesDirectlyRef.current = showNewNotesDirectly showNewNotesDirectlyRef.current = showNewNotesDirectly
@ -287,16 +295,24 @@ const NoteList = forwardRef<
useEffect(() => { useEffect(() => {
if (!subRequests.length) return if (!subRequests.length) return
sinceRef.current = undefined
setEvents([])
setStoredEvents([])
setNewEvents([])
}, [JSON.stringify(subRequests), refreshCount, JSON.stringify(showKinds)])
useEffect(() => {
if (!subRequests.length || !active) return
async function init() { async function init() {
setInitialLoading(true) setInitialLoading(true)
setEvents([])
setStoredEvents([])
setNewEvents([])
if (showKinds?.length === 0 && subRequests.every(({ filter }) => !filter.kinds)) { if (showKinds?.length === 0 && subRequests.every(({ filter }) => !filter.kinds)) {
return () => {} return () => {}
} }
const since = sinceRef.current
if (isPubkeyFeed) { if (isPubkeyFeed) {
const storedEvents = await client.getEventsFromIndexed({ const storedEvents = await client.getEventsFromIndexed({
authors: subRequests.flatMap(({ filter }) => filter.authors ?? []), authors: subRequests.flatMap(({ filter }) => filter.authors ?? []),
@ -320,12 +336,35 @@ const NoteList = forwardRef<
}) })
) )
const handleNewEvents = (newEvents: Event[]) => {
if (showNewNotesDirectlyRef.current) {
setEvents((oldEvents) => mergeTimelines([newEvents, oldEvents]))
} else {
const isAtTop = (() => {
if (!topRef.current) return true
const rect = topRef.current.getBoundingClientRect()
return rect.top >= 50
})()
if (isAtTop) {
setEvents((oldEvents) => mergeTimelines([newEvents, oldEvents]))
} else {
setNewEvents((oldEvents) => mergeTimelines([newEvents, oldEvents]))
}
}
}
const { closer, timelineKey } = await client.subscribeTimeline( const { closer, timelineKey } = await client.subscribeTimeline(
preprocessedSubRequests, preprocessedSubRequests,
{ {
onEvents: (events, eosed) => { onEvents: (events, eosed) => {
if (events.length > 0) { if (events.length > 0) {
setEvents(events) if (!since) {
setEvents(events)
} else {
const newEvents = events.filter((evt) => evt.created_at >= since)
handleNewEvents(newEvents)
}
} }
if (eosed) { if (eosed) {
threadService.addRepliesToThread(events) threadService.addRepliesToThread(events)
@ -333,27 +372,7 @@ const NoteList = forwardRef<
} }
}, },
onNew: (event) => { onNew: (event) => {
if (showNewNotesDirectlyRef.current) { handleNewEvents([event])
setEvents((oldEvents) =>
oldEvents.some((e) => e.id === event.id) ? oldEvents : [event, ...oldEvents]
)
} else {
const isAtTop = (() => {
if (!topRef.current) return true
const rect = topRef.current.getBoundingClientRect()
return rect.top >= 50
})()
if (isAtTop) {
setEvents((oldEvents) =>
oldEvents.some((e) => e.id === event.id) ? oldEvents : [event, ...oldEvents]
)
} else {
setNewEvents((oldEvents) =>
[event, ...oldEvents].sort((a, b) => b.created_at - a.created_at)
)
}
}
threadService.addRepliesToThread([event]) threadService.addRepliesToThread([event])
}, },
onClose: (url, reason) => { onClose: (url, reason) => {
@ -388,7 +407,7 @@ const NoteList = forwardRef<
return () => { return () => {
promise.then((closer) => closer()) promise.then((closer) => closer())
} }
}, [JSON.stringify(subRequests), refreshCount, JSON.stringify(showKinds)]) }, [JSON.stringify(subRequests), refreshCount, JSON.stringify(showKinds), active])
const handleLoadMore = useCallback(async () => { const handleLoadMore = useCallback(async () => {
if (!timelineKey || areAlgoRelays) return false if (!timelineKey || areAlgoRelays) return false
@ -412,7 +431,7 @@ const NoteList = forwardRef<
}) })
const showNewEvents = () => { const showNewEvents = () => {
setEvents((oldEvents) => [...newEvents, ...oldEvents]) setEvents((oldEvents) => mergeTimelines([newEvents, oldEvents]))
setNewEvents([]) setNewEvents([])
setTimeout(() => { setTimeout(() => {
scrollToTop('smooth') scrollToTop('smooth')

View file

@ -12,6 +12,7 @@ import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useDeletedEvent } from '@/providers/DeletedEventProvider' import { useDeletedEvent } from '@/providers/DeletedEventProvider'
import { useMuteList } from '@/providers/MuteListProvider' import { useMuteList } from '@/providers/MuteListProvider'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { usePageActive } from '@/providers/PageActiveProvider'
import { usePinnedUsers } from '@/providers/PinnedUsersProvider' import { usePinnedUsers } from '@/providers/PinnedUsersProvider'
import { useUserTrust } from '@/providers/UserTrustProvider' import { useUserTrust } from '@/providers/UserTrustProvider'
import client from '@/services/client.service' import client from '@/services/client.service'
@ -68,6 +69,7 @@ const UserAggregationList = forwardRef<
ref ref
) => { ) => {
const { t } = useTranslation() const { t } = useTranslation()
const active = usePageActive()
const { pubkey: currentPubkey, startLogin } = useNostr() const { pubkey: currentPubkey, startLogin } = useNostr()
const { push } = useSecondaryPage() const { push } = useSecondaryPage()
const { mutePubkeySet } = useMuteList() const { mutePubkeySet } = useMuteList()
@ -95,6 +97,12 @@ const UserAggregationList = forwardRef<
const bottomRef = useRef<HTMLDivElement | null>(null) const bottomRef = useRef<HTMLDivElement | null>(null)
const topRef = useRef<HTMLDivElement | null>(null) const topRef = useRef<HTMLDivElement | null>(null)
const nonPinnedTopRef = useRef<HTMLDivElement | null>(null) const nonPinnedTopRef = useRef<HTMLDivElement | null>(null)
const sinceRef = useRef<number | undefined>(undefined)
sinceRef.current = newEvents.length
? newEvents[0].created_at + 1
: events.length
? events[0].created_at + 1
: undefined
const scrollToTop = (behavior: ScrollBehavior = 'instant') => { const scrollToTop = (behavior: ScrollBehavior = 'instant') => {
setTimeout(() => { setTimeout(() => {
@ -120,15 +128,19 @@ const UserAggregationList = forwardRef<
useEffect(() => { useEffect(() => {
if (!subRequests.length) return if (!subRequests.length) return
sinceRef.current = undefined
setSince(dayjs().subtract(1, 'day').unix()) setSince(dayjs().subtract(1, 'day').unix())
setStoredEvents([])
setEvents([])
setNewEvents([])
setHasMore(true) setHasMore(true)
}, [feedId, refreshCount])
useEffect(() => {
if (!subRequests.length || !active) return
async function init() { async function init() {
setLoading(true) setLoading(true)
setStoredEvents([])
setEvents([])
setNewEvents([])
setHasMore(true)
if (showKinds?.length === 0 && subRequests.every(({ filter }) => !filter.kinds)) { if (showKinds?.length === 0 && subRequests.every(({ filter }) => !filter.kinds)) {
setLoading(false) setLoading(false)
@ -136,6 +148,8 @@ const UserAggregationList = forwardRef<
return () => {} return () => {}
} }
const since = sinceRef.current
if (isPubkeyFeed) { if (isPubkeyFeed) {
const storedEvents = await client.getEventsFromIndexed({ const storedEvents = await client.getEventsFromIndexed({
authors: subRequests.flatMap(({ filter }) => filter.authors ?? []), authors: subRequests.flatMap(({ filter }) => filter.authors ?? []),
@ -164,21 +178,23 @@ const UserAggregationList = forwardRef<
{ {
onEvents: (events, eosed) => { onEvents: (events, eosed) => {
if (events.length > 0) { if (events.length > 0) {
setEvents(events) if (!since) {
setEvents(events)
} else {
const newEvents = events.filter((evt) => evt.created_at >= since)
setNewEvents((oldEvents) => mergeTimelines([newEvents, oldEvents]))
}
} }
if (areAlgoRelays) { if (areAlgoRelays) {
setHasMore(false) setHasMore(false)
} }
if (eosed) { if (eosed) {
setLoading(false) setLoading(false)
setHasMore(events.length > 0)
threadService.addRepliesToThread(events) threadService.addRepliesToThread(events)
} }
}, },
onNew: (event) => { onNew: (event) => {
setNewEvents((oldEvents) => setNewEvents((oldEvents) => mergeTimelines([[event], oldEvents]))
[event, ...oldEvents].sort((a, b) => b.created_at - a.created_at)
)
threadService.addRepliesToThread([event]) threadService.addRepliesToThread([event])
}, },
onClose: (url, reason) => { onClose: (url, reason) => {
@ -214,7 +230,7 @@ const UserAggregationList = forwardRef<
return () => { return () => {
promise.then((closer) => closer()) promise.then((closer) => closer())
} }
}, [feedId, refreshCount]) }, [feedId, refreshCount, active])
useEffect(() => { useEffect(() => {
if (loading || !hasMore || !timelineKey || !events.length) { if (loading || !hasMore || !timelineKey || !events.length) {

View file

@ -4,6 +4,7 @@ import { ScrollArea } from '@/components/ui/scroll-area'
import { usePrimaryPage } from '@/PageManager' import { usePrimaryPage } from '@/PageManager'
import { DeepBrowsingProvider } from '@/providers/DeepBrowsingProvider' import { DeepBrowsingProvider } from '@/providers/DeepBrowsingProvider'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { PageActiveContext } from '@/providers/PageActiveProvider'
import { useUserPreferences } from '@/providers/UserPreferencesProvider' import { useUserPreferences } from '@/providers/UserPreferencesProvider'
import { TPrimaryPageName } from '@/routes/primary' import { TPrimaryPageName } from '@/routes/primary'
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react' import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'
@ -76,38 +77,45 @@ const PrimaryPageLayout = forwardRef(
if (enableSingleColumnLayout) { if (enableSingleColumnLayout) {
return ( return (
<DeepBrowsingProvider active={current === pageName && display}> <PageActiveContext.Provider value={current === pageName && display}>
<div <DeepBrowsingProvider active={current === pageName && display}>
ref={smallScreenScrollAreaRef} <div
style={{ ref={smallScreenScrollAreaRef}
paddingBottom: 'calc(env(safe-area-inset-bottom) + 3rem)' style={{
}} paddingBottom: 'calc(env(safe-area-inset-bottom) + 3rem)'
}}
>
<PrimaryPageTitlebar hideBottomBorder={hideTitlebarBottomBorder}>
{titlebar}
</PrimaryPageTitlebar>
{children}
</div>
{displayScrollToTopButton && <ScrollToTopButton />}
</DeepBrowsingProvider>
</PageActiveContext.Provider>
)
}
return (
<PageActiveContext.Provider value={current === pageName && display}>
<DeepBrowsingProvider
active={current === pageName && display}
scrollAreaRef={scrollAreaRef}
>
<ScrollArea
className="h-full overflow-auto"
scrollBarClassName="z-30 pt-12"
ref={scrollAreaRef}
> >
<PrimaryPageTitlebar hideBottomBorder={hideTitlebarBottomBorder}> <PrimaryPageTitlebar hideBottomBorder={hideTitlebarBottomBorder}>
{titlebar} {titlebar}
</PrimaryPageTitlebar> </PrimaryPageTitlebar>
{children} {children}
</div> <div className="h-4" />
{displayScrollToTopButton && <ScrollToTopButton />} </ScrollArea>
{displayScrollToTopButton && <ScrollToTopButton scrollAreaRef={scrollAreaRef} />}
</DeepBrowsingProvider> </DeepBrowsingProvider>
) </PageActiveContext.Provider>
}
return (
<DeepBrowsingProvider active={current === pageName && display} scrollAreaRef={scrollAreaRef}>
<ScrollArea
className="h-full overflow-auto"
scrollBarClassName="z-30 pt-12"
ref={scrollAreaRef}
>
<PrimaryPageTitlebar hideBottomBorder={hideTitlebarBottomBorder}>
{titlebar}
</PrimaryPageTitlebar>
{children}
<div className="h-4" />
</ScrollArea>
{displayScrollToTopButton && <ScrollToTopButton scrollAreaRef={scrollAreaRef} />}
</DeepBrowsingProvider>
) )
} }
) )

View file

@ -4,6 +4,7 @@ import { Button } from '@/components/ui/button'
import { ScrollArea } from '@/components/ui/scroll-area' import { ScrollArea } from '@/components/ui/scroll-area'
import { useSecondaryPage } from '@/PageManager' import { useSecondaryPage } from '@/PageManager'
import { DeepBrowsingProvider } from '@/providers/DeepBrowsingProvider' import { DeepBrowsingProvider } from '@/providers/DeepBrowsingProvider'
import { PageActiveContext } from '@/providers/PageActiveProvider'
import { useUserPreferences } from '@/providers/UserPreferencesProvider' import { useUserPreferences } from '@/providers/UserPreferencesProvider'
import { ChevronLeft } from 'lucide-react' import { ChevronLeft } from 'lucide-react'
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react' import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'
@ -60,11 +61,35 @@ const SecondaryPageLayout = forwardRef(
if (enableSingleColumnLayout) { if (enableSingleColumnLayout) {
return ( return (
<DeepBrowsingProvider active={currentIndex === index}> <PageActiveContext.Provider value={currentIndex === index}>
<div <DeepBrowsingProvider active={currentIndex === index}>
style={{ <div
paddingBottom: 'calc(env(safe-area-inset-bottom) + 3rem)' style={{
}} paddingBottom: 'calc(env(safe-area-inset-bottom) + 3rem)'
}}
>
<SecondaryPageTitlebar
title={title}
controls={controls}
hideBackButton={hideBackButton}
hideBottomBorder={hideTitlebarBottomBorder}
titlebar={titlebar}
/>
{children}
</div>
{displayScrollToTopButton && <ScrollToTopButton />}
</DeepBrowsingProvider>
</PageActiveContext.Provider>
)
}
return (
<PageActiveContext.Provider value={currentIndex === index}>
<DeepBrowsingProvider active={currentIndex === index} scrollAreaRef={scrollAreaRef}>
<ScrollArea
className="h-full overflow-auto"
scrollBarClassName="z-30 pt-12"
ref={scrollAreaRef}
> >
<SecondaryPageTitlebar <SecondaryPageTitlebar
title={title} title={title}
@ -74,31 +99,11 @@ const SecondaryPageLayout = forwardRef(
titlebar={titlebar} titlebar={titlebar}
/> />
{children} {children}
</div> <div className="h-4" />
{displayScrollToTopButton && <ScrollToTopButton />} </ScrollArea>
{displayScrollToTopButton && <ScrollToTopButton scrollAreaRef={scrollAreaRef} />}
</DeepBrowsingProvider> </DeepBrowsingProvider>
) </PageActiveContext.Provider>
}
return (
<DeepBrowsingProvider active={currentIndex === index} scrollAreaRef={scrollAreaRef}>
<ScrollArea
className="h-full overflow-auto"
scrollBarClassName="z-30 pt-12"
ref={scrollAreaRef}
>
<SecondaryPageTitlebar
title={title}
controls={controls}
hideBackButton={hideBackButton}
hideBottomBorder={hideTitlebarBottomBorder}
titlebar={titlebar}
/>
{children}
<div className="h-4" />
</ScrollArea>
{displayScrollToTopButton && <ScrollToTopButton scrollAreaRef={scrollAreaRef} />}
</DeepBrowsingProvider>
) )
} }
) )

View file

@ -3,7 +3,7 @@ import { AbstractRelay } from 'nostr-tools/abstract-relay'
const DEFAULT_CONNECTION_TIMEOUT = 10 * 1000 // 10 seconds const DEFAULT_CONNECTION_TIMEOUT = 10 * 1000 // 10 seconds
const CLEANUP_THRESHOLD = 15 // number of relays to trigger cleanup const CLEANUP_THRESHOLD = 15 // number of relays to trigger cleanup
const CLEANUP_INTERVAL = 5 * 1000 // 5 seconds const CLEANUP_INTERVAL = 30 * 1000 // 30 seconds
const IDLE_TIMEOUT = 10 * 1000 // 10 seconds const IDLE_TIMEOUT = 10 * 1000 // 10 seconds
export class SmartPool extends SimplePool { export class SmartPool extends SimplePool {

View file

@ -6,7 +6,6 @@ import { usePrimaryPage } from '@/PageManager'
import client from '@/services/client.service' import client from '@/services/client.service'
import storage from '@/services/local-storage.service' import storage from '@/services/local-storage.service'
import { kinds, NostrEvent } from 'nostr-tools' import { kinds, NostrEvent } from 'nostr-tools'
import { SubCloser } from 'nostr-tools/abstract-pool'
import { createContext, useContext, useEffect, useMemo, useState } from 'react' import { createContext, useContext, useEffect, useMemo, useState } from 'react'
import { useContentPolicy } from './ContentPolicyProvider' import { useContentPolicy } from './ContentPolicyProvider'
import { useMuteList } from './MuteListProvider' import { useMuteList } from './MuteListProvider'
@ -88,110 +87,61 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
setNewNotifications([]) setNewNotifications([])
setReadNotificationIdSet(new Set()) setReadNotificationIdSet(new Set())
// Track if component is mounted
const isMountedRef = { current: true }
const subCloserRef: {
current: SubCloser | null
} = { current: null }
const subscribe = async () => { const subscribe = async () => {
if (subCloserRef.current) { let eosed = false
subCloserRef.current.close() const relayList = await client.fetchRelayList(pubkey)
subCloserRef.current = null const relays = relayList.read.length > 0 ? relayList.read.slice(0, 5) : getDefaultRelayUrls()
} return client.subscribe(
if (!isMountedRef.current) return null relays,
[
try {
let eosed = false
const relayList = await client.fetchRelayList(pubkey)
const relays =
relayList.read.length > 0 ? relayList.read.slice(0, 5) : getDefaultRelayUrls()
const subCloser = client.subscribe(
relays,
[
{
kinds: [
kinds.ShortTextNote,
kinds.Repost,
kinds.Reaction,
kinds.Zap,
ExtendedKind.COMMENT,
ExtendedKind.POLL_RESPONSE,
ExtendedKind.VOICE_COMMENT,
ExtendedKind.POLL
],
'#p': [pubkey],
limit: 20
}
],
{ {
oneose: (e) => { kinds: [
if (e) { kinds.ShortTextNote,
eosed = e kinds.Repost,
setNewNotifications((prev) => { kinds.GenericRepost,
return [...prev.sort((a, b) => compareEvents(b, a))] kinds.Reaction,
}) kinds.Zap,
} kinds.Highlights,
}, ExtendedKind.COMMENT,
onevent: (evt) => { ExtendedKind.POLL_RESPONSE,
if (evt.pubkey !== pubkey) { ExtendedKind.VOICE_COMMENT,
setNewNotifications((prev) => { ExtendedKind.POLL
if (!eosed) { ],
return [evt, ...prev] '#p': [pubkey],
} limit: 20
if (prev.length && compareEvents(prev[0], evt) >= 0) { }
return prev ],
} {
oneose: (e) => {
client.emitNewEvent(evt, relays) if (e) {
eosed = e
setNewNotifications((prev) => {
return [...prev.sort((a, b) => compareEvents(b, a))]
})
}
},
onevent: (evt) => {
if (evt.pubkey !== pubkey) {
setNewNotifications((prev) => {
if (!eosed) {
return [evt, ...prev] return [evt, ...prev]
}) }
} if (prev.length && compareEvents(prev[0], evt) >= 0) {
}, return prev
onAllClose: (reasons) => { }
if (reasons.every((reason) => reason === 'closed by caller')) {
return
}
// Only reconnect if still mounted and not a manual close client.emitNewEvent(evt, relays)
if (isMountedRef.current) { return [evt, ...prev]
setTimeout(() => { })
if (isMountedRef.current) {
subscribe()
}
}, 5_000)
}
} }
} }
)
subCloserRef.current = subCloser
return subCloser
} catch (error) {
console.error('Subscription error:', error)
// Retry on error if still mounted
if (isMountedRef.current) {
setTimeout(() => {
if (isMountedRef.current) {
subscribe()
}
}, 5_000)
} }
return null )
}
} }
// Initial subscription const promise = subscribe()
subscribe()
// Cleanup function
return () => { return () => {
isMountedRef.current = false promise.then((closer) => closer.close())
if (subCloserRef.current) {
subCloserRef.current.close()
subCloserRef.current = null
}
} }
}, [pubkey]) }, [pubkey])

View file

@ -0,0 +1,8 @@
import { createContext, useContext } from 'react'
export const PageActiveContext = createContext<boolean | null>(null)
export function usePageActive() {
const ctx = useContext(PageActiveContext)
return ctx ?? false
}

View file

@ -8,14 +8,9 @@ import RelayPage from '@/pages/primary/RelayPage'
import SearchPage from '@/pages/primary/SearchPage' import SearchPage from '@/pages/primary/SearchPage'
import SettingsPage from '@/pages/primary/SettingsPage' import SettingsPage from '@/pages/primary/SettingsPage'
import { TPageRef } from '@/types' import { TPageRef } from '@/types'
import { createRef, ForwardRefExoticComponent, RefAttributes } from 'react' import { createRef } from 'react'
type RouteConfig = { const PRIMARY_ROUTE_CONFIGS = [
key: string
component: ForwardRefExoticComponent<RefAttributes<TPageRef>>
}
const PRIMARY_ROUTE_CONFIGS: RouteConfig[] = [
{ key: 'home', component: NoteListPage }, { key: 'home', component: NoteListPage },
{ key: 'explore', component: ExplorePage }, { key: 'explore', component: ExplorePage },
{ key: 'notifications', component: NotificationListPage }, { key: 'notifications', component: NotificationListPage },
@ -25,7 +20,7 @@ const PRIMARY_ROUTE_CONFIGS: RouteConfig[] = [
{ key: 'search', component: SearchPage }, { key: 'search', component: SearchPage },
{ key: 'bookmark', component: BookmarkPage }, { key: 'bookmark', component: BookmarkPage },
{ key: 'settings', component: SettingsPage } { key: 'settings', component: SettingsPage }
] ] as const
export const PRIMARY_PAGE_REF_MAP = PRIMARY_ROUTE_CONFIGS.reduce( export const PRIMARY_PAGE_REF_MAP = PRIMARY_ROUTE_CONFIGS.reduce(
(acc, { key }) => { (acc, { key }) => {
@ -40,7 +35,7 @@ export const PRIMARY_PAGE_MAP = PRIMARY_ROUTE_CONFIGS.reduce(
acc[key] = <Component ref={PRIMARY_PAGE_REF_MAP[key]} /> acc[key] = <Component ref={PRIMARY_PAGE_REF_MAP[key]} />
return acc return acc
}, },
{} as Record<string, JSX.Element> {} as Record<(typeof PRIMARY_ROUTE_CONFIGS)[number]['key'], JSX.Element>
) )
export type TPrimaryPageName = keyof typeof PRIMARY_PAGE_MAP export type TPrimaryPageName = keyof typeof PRIMARY_PAGE_MAP