💨
This commit is contained in:
@@ -6,7 +6,7 @@ import { useMuteList } from '@/providers/MuteListProvider'
|
||||
import { useUserTrust } from '@/providers/UserTrustProvider'
|
||||
import threadService from '@/services/thread.service'
|
||||
import { Event as NEvent } from 'nostr-tools'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { LoadingBar } from '../LoadingBar'
|
||||
import ReplyNote, { ReplyNoteSkeleton } from '../ReplyNote'
|
||||
@@ -57,15 +57,15 @@ export default function ReplyNoteList({ stuff }: { stuff: NEvent | string }) {
|
||||
])
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [showCount, setShowCount] = useState(SHOW_COUNT)
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const loadingRef = useRef(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const bottomRef = useRef<HTMLDivElement | null>(null)
|
||||
const stateRef = useRef({ loading, hasMore, showCount, repliesLength: replies.length })
|
||||
stateRef.current = { loading, hasMore, showCount, repliesLength: replies.length }
|
||||
|
||||
// Initial subscription
|
||||
useEffect(() => {
|
||||
loadingRef.current = true
|
||||
setLoading(true)
|
||||
threadService.subscribe(stuff, LIMIT).finally(() => {
|
||||
loadingRef.current = false
|
||||
setLoading(false)
|
||||
})
|
||||
|
||||
@@ -74,52 +74,50 @@ export default function ReplyNoteList({ stuff }: { stuff: NEvent | string }) {
|
||||
}
|
||||
}, [stuff])
|
||||
|
||||
const loadMore = useCallback(async () => {
|
||||
const { loading, hasMore, showCount, repliesLength } = stateRef.current
|
||||
|
||||
if (loading || !hasMore) return
|
||||
|
||||
// If there are more items to show, increase showCount first
|
||||
if (showCount < repliesLength) {
|
||||
setShowCount((prev) => prev + SHOW_COUNT)
|
||||
// Only fetch more data when remaining items are running low
|
||||
if (repliesLength - showCount > LIMIT / 2) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
const newHasMore = await threadService.loadMore(stuff, LIMIT)
|
||||
setHasMore(newHasMore)
|
||||
setLoading(false)
|
||||
}, [stuff])
|
||||
|
||||
// IntersectionObserver setup
|
||||
useEffect(() => {
|
||||
const options = {
|
||||
root: null,
|
||||
rootMargin: '10px',
|
||||
threshold: 0.1
|
||||
}
|
||||
|
||||
const loadMore = async () => {
|
||||
if (showCount < replies.length) {
|
||||
setShowCount((prev) => prev + SHOW_COUNT)
|
||||
// preload more
|
||||
if (replies.length - showCount > LIMIT / 2) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (loadingRef.current) return
|
||||
|
||||
loadingRef.current = true
|
||||
setLoading(true)
|
||||
|
||||
const newHasMore = await threadService.loadMore(stuff, LIMIT)
|
||||
|
||||
setHasMore(newHasMore)
|
||||
loadingRef.current = false
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const observerInstance = new IntersectionObserver((entries) => {
|
||||
if (entries[0].isIntersecting && hasMore) {
|
||||
loadMore()
|
||||
}
|
||||
}, options)
|
||||
|
||||
const currentBottomRef = bottomRef.current
|
||||
if (!currentBottomRef) return
|
||||
|
||||
if (currentBottomRef) {
|
||||
observerInstance.observe(currentBottomRef)
|
||||
}
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
loadMore()
|
||||
}
|
||||
},
|
||||
{
|
||||
root: null,
|
||||
rootMargin: '100px',
|
||||
threshold: 0
|
||||
}
|
||||
)
|
||||
|
||||
observer.observe(currentBottomRef)
|
||||
|
||||
return () => {
|
||||
if (observerInstance && currentBottomRef) {
|
||||
observerInstance.unobserve(currentBottomRef)
|
||||
}
|
||||
observer.disconnect()
|
||||
}
|
||||
}, [replies, showCount, loading, stuff, hasMore])
|
||||
}, [loadMore])
|
||||
|
||||
return (
|
||||
<div className="min-h-[80vh]">
|
||||
|
||||
@@ -26,8 +26,10 @@ class ThreadService {
|
||||
private subscriptions = new Map<
|
||||
string,
|
||||
{
|
||||
closer?: () => void
|
||||
timelineKey?: string
|
||||
promise: Promise<{
|
||||
closer: () => void
|
||||
timelineKey: string
|
||||
}>
|
||||
count: number
|
||||
until?: number
|
||||
}
|
||||
@@ -54,101 +56,99 @@ class ThreadService {
|
||||
const rootInfo = await this.parseRootInfo(stuff)
|
||||
if (!rootInfo) return
|
||||
|
||||
let subscription = this.subscriptions.get(rootInfo.id)
|
||||
const subscription = this.subscriptions.get(rootInfo.id)
|
||||
if (subscription) {
|
||||
subscription.count += 1
|
||||
return
|
||||
}
|
||||
|
||||
subscription = { count: 1, until: dayjs().unix() }
|
||||
this.subscriptions.set(rootInfo.id, subscription)
|
||||
|
||||
let relayUrls: string[] = []
|
||||
const rootPubkey = (rootInfo as { pubkey?: string }).pubkey ?? event?.pubkey
|
||||
if (rootPubkey) {
|
||||
const relayList = await client.fetchRelayList(rootPubkey)
|
||||
relayUrls = relayList.read
|
||||
}
|
||||
relayUrls = relayUrls.concat(BIG_RELAY_URLS).slice(0, 4)
|
||||
|
||||
// If current event is protected, we can assume its replies are also protected and stored on the same relays
|
||||
if (event && isProtectedEvent(event)) {
|
||||
const seenOn = client.getSeenEventRelayUrls(event.id)
|
||||
relayUrls.concat(...seenOn)
|
||||
}
|
||||
|
||||
const filters: (Omit<Filter, 'since' | 'until'> & {
|
||||
limit: number
|
||||
})[] = []
|
||||
if (rootInfo.type === 'E') {
|
||||
filters.push({
|
||||
'#e': [rootInfo.id],
|
||||
kinds: [kinds.ShortTextNote],
|
||||
limit
|
||||
})
|
||||
if (event?.kind !== kinds.ShortTextNote) {
|
||||
filters.push({
|
||||
'#E': [rootInfo.id],
|
||||
kinds: [ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT],
|
||||
limit
|
||||
})
|
||||
const _subscribe = async () => {
|
||||
let relayUrls: string[] = []
|
||||
const rootPubkey = (rootInfo as { pubkey?: string }).pubkey ?? event?.pubkey
|
||||
if (rootPubkey) {
|
||||
const relayList = await client.fetchRelayList(rootPubkey)
|
||||
relayUrls = relayList.read
|
||||
}
|
||||
} else if (rootInfo.type === 'A') {
|
||||
filters.push(
|
||||
{
|
||||
'#a': [rootInfo.id],
|
||||
relayUrls = relayUrls.concat(BIG_RELAY_URLS).slice(0, 4)
|
||||
|
||||
// If current event is protected, we can assume its replies are also protected and stored on the same relays
|
||||
if (event && isProtectedEvent(event)) {
|
||||
const seenOn = client.getSeenEventRelayUrls(event.id)
|
||||
relayUrls.concat(...seenOn)
|
||||
}
|
||||
|
||||
const filters: (Omit<Filter, 'since' | 'until'> & {
|
||||
limit: number
|
||||
})[] = []
|
||||
if (rootInfo.type === 'E') {
|
||||
filters.push({
|
||||
'#e': [rootInfo.id],
|
||||
kinds: [kinds.ShortTextNote],
|
||||
limit
|
||||
},
|
||||
{
|
||||
'#A': [rootInfo.id],
|
||||
kinds: [ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT],
|
||||
limit
|
||||
})
|
||||
if (event?.kind !== kinds.ShortTextNote) {
|
||||
filters.push({
|
||||
'#E': [rootInfo.id],
|
||||
kinds: [ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT],
|
||||
limit
|
||||
})
|
||||
}
|
||||
)
|
||||
if (rootInfo.relay) {
|
||||
relayUrls.push(rootInfo.relay)
|
||||
}
|
||||
} else {
|
||||
filters.push({
|
||||
'#I': [rootInfo.id],
|
||||
kinds: [ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT],
|
||||
limit
|
||||
})
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
client
|
||||
.subscribeTimeline(
|
||||
filters.map((filter) => ({
|
||||
urls: relayUrls.slice(0, 8),
|
||||
filter
|
||||
})),
|
||||
} else if (rootInfo.type === 'A') {
|
||||
filters.push(
|
||||
{
|
||||
onEvents: (events, eosed) => {
|
||||
if (events.length > 0) {
|
||||
this.addRepliesToThread(events)
|
||||
}
|
||||
if (eosed) {
|
||||
subscription.until =
|
||||
events.length >= limit ? events[events.length - 1].created_at - 1 : undefined
|
||||
resolve()
|
||||
}
|
||||
},
|
||||
onNew: (evt) => {
|
||||
this.addRepliesToThread([evt])
|
||||
}
|
||||
'#a': [rootInfo.id],
|
||||
kinds: [kinds.ShortTextNote],
|
||||
limit
|
||||
},
|
||||
{
|
||||
'#A': [rootInfo.id],
|
||||
kinds: [ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT],
|
||||
limit
|
||||
}
|
||||
)
|
||||
.then(({ closer, timelineKey }) => {
|
||||
subscription.closer = closer
|
||||
subscription.timelineKey = timelineKey
|
||||
})
|
||||
.catch(() => {
|
||||
this.subscriptions.delete(rootInfo.id)
|
||||
resolve()
|
||||
if (rootInfo.relay) {
|
||||
relayUrls.push(rootInfo.relay)
|
||||
}
|
||||
} else {
|
||||
filters.push({
|
||||
'#I': [rootInfo.id],
|
||||
kinds: [ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT],
|
||||
limit
|
||||
})
|
||||
}
|
||||
const { closer, timelineKey } = await client.subscribeTimeline(
|
||||
filters.map((filter) => ({
|
||||
urls: relayUrls.slice(0, 8),
|
||||
filter
|
||||
})),
|
||||
{
|
||||
onEvents: (events, eosed) => {
|
||||
if (events.length > 0) {
|
||||
this.addRepliesToThread(events)
|
||||
}
|
||||
if (eosed) {
|
||||
const subscription = this.subscriptions.get(rootInfo.id)
|
||||
if (subscription) {
|
||||
subscription.until =
|
||||
events.length >= limit ? events[events.length - 1].created_at - 1 : undefined
|
||||
}
|
||||
}
|
||||
},
|
||||
onNew: (evt) => {
|
||||
this.addRepliesToThread([evt])
|
||||
}
|
||||
}
|
||||
)
|
||||
return { closer, timelineKey }
|
||||
}
|
||||
|
||||
const promise = _subscribe()
|
||||
this.subscriptions.set(rootInfo.id, {
|
||||
promise,
|
||||
count: 1,
|
||||
until: dayjs().unix()
|
||||
})
|
||||
await promise
|
||||
}
|
||||
|
||||
async unsubscribe(stuff: NostrEvent | string) {
|
||||
@@ -162,7 +162,9 @@ class ThreadService {
|
||||
subscription.count -= 1
|
||||
if (subscription.count <= 0) {
|
||||
this.subscriptions.delete(rootInfo.id)
|
||||
subscription.closer?.()
|
||||
subscription.promise.then(({ closer }) => {
|
||||
closer()
|
||||
})
|
||||
}
|
||||
}, 2000)
|
||||
}
|
||||
@@ -172,12 +174,13 @@ class ThreadService {
|
||||
if (!rootInfo) return false
|
||||
|
||||
const subscription = this.subscriptions.get(rootInfo.id)
|
||||
if (!subscription) return false
|
||||
if (!subscription || !subscription.until) return false
|
||||
|
||||
const { timelineKey, until } = subscription
|
||||
if (!timelineKey || !until) return false
|
||||
const { timelineKey } = await subscription.promise
|
||||
console.log('loadMore', { timelineKey, until: subscription.until })
|
||||
if (!timelineKey) return false
|
||||
|
||||
const events = await client.loadMoreTimeline(timelineKey, until, limit)
|
||||
const events = await client.loadMoreTimeline(timelineKey, subscription.until, limit)
|
||||
this.addRepliesToThread(events)
|
||||
|
||||
const { event } = this.resolveStuff(stuff)
|
||||
|
||||
Reference in New Issue
Block a user