feat: 💨
This commit is contained in:
parent
695f2fe017
commit
ed843f637a
8 changed files with 198 additions and 197 deletions
|
|
@ -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')
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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])
|
||||||
|
|
||||||
|
|
|
||||||
8
src/providers/PageActiveProvider.tsx
Normal file
8
src/providers/PageActiveProvider.tsx
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue