This commit is contained in:
codytseng
2025-12-25 18:07:22 +08:00
parent 17d90a298a
commit 078a8fd348
2 changed files with 134 additions and 133 deletions

View File

@@ -6,7 +6,7 @@ import { useMuteList } from '@/providers/MuteListProvider'
import { useUserTrust } from '@/providers/UserTrustProvider' import { useUserTrust } from '@/providers/UserTrustProvider'
import threadService from '@/services/thread.service' import threadService from '@/services/thread.service'
import { Event as NEvent } from 'nostr-tools' 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 { useTranslation } from 'react-i18next'
import { LoadingBar } from '../LoadingBar' import { LoadingBar } from '../LoadingBar'
import ReplyNote, { ReplyNoteSkeleton } from '../ReplyNote' import ReplyNote, { ReplyNoteSkeleton } from '../ReplyNote'
@@ -57,15 +57,15 @@ export default function ReplyNoteList({ stuff }: { stuff: NEvent | string }) {
]) ])
const [hasMore, setHasMore] = useState(true) const [hasMore, setHasMore] = useState(true)
const [showCount, setShowCount] = useState(SHOW_COUNT) const [showCount, setShowCount] = useState(SHOW_COUNT)
const [loading, setLoading] = useState<boolean>(false) const [loading, setLoading] = useState(false)
const loadingRef = useRef(false)
const bottomRef = useRef<HTMLDivElement | null>(null) 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(() => { useEffect(() => {
loadingRef.current = true
setLoading(true) setLoading(true)
threadService.subscribe(stuff, LIMIT).finally(() => { threadService.subscribe(stuff, LIMIT).finally(() => {
loadingRef.current = false
setLoading(false) setLoading(false)
}) })
@@ -74,52 +74,50 @@ export default function ReplyNoteList({ stuff }: { stuff: NEvent | string }) {
} }
}, [stuff]) }, [stuff])
useEffect(() => { const loadMore = useCallback(async () => {
const options = { const { loading, hasMore, showCount, repliesLength } = stateRef.current
root: null,
rootMargin: '10px',
threshold: 0.1
}
const loadMore = async () => { if (loading || !hasMore) return
if (showCount < replies.length) {
// If there are more items to show, increase showCount first
if (showCount < repliesLength) {
setShowCount((prev) => prev + SHOW_COUNT) setShowCount((prev) => prev + SHOW_COUNT)
// preload more // Only fetch more data when remaining items are running low
if (replies.length - showCount > LIMIT / 2) { if (repliesLength - showCount > LIMIT / 2) {
return return
} }
} }
if (loadingRef.current) return
loadingRef.current = true
setLoading(true) setLoading(true)
const newHasMore = await threadService.loadMore(stuff, LIMIT) const newHasMore = await threadService.loadMore(stuff, LIMIT)
setHasMore(newHasMore) setHasMore(newHasMore)
loadingRef.current = false
setLoading(false) setLoading(false)
} }, [stuff])
const observerInstance = new IntersectionObserver((entries) => { // IntersectionObserver setup
if (entries[0].isIntersecting && hasMore) { useEffect(() => {
const currentBottomRef = bottomRef.current
if (!currentBottomRef) return
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
loadMore() loadMore()
} }
}, options) },
{
const currentBottomRef = bottomRef.current root: null,
rootMargin: '100px',
if (currentBottomRef) { threshold: 0
observerInstance.observe(currentBottomRef)
} }
)
observer.observe(currentBottomRef)
return () => { return () => {
if (observerInstance && currentBottomRef) { observer.disconnect()
observerInstance.unobserve(currentBottomRef)
} }
} }, [loadMore])
}, [replies, showCount, loading, stuff, hasMore])
return ( return (
<div className="min-h-[80vh]"> <div className="min-h-[80vh]">

View File

@@ -26,8 +26,10 @@ class ThreadService {
private subscriptions = new Map< private subscriptions = new Map<
string, string,
{ {
closer?: () => void promise: Promise<{
timelineKey?: string closer: () => void
timelineKey: string
}>
count: number count: number
until?: number until?: number
} }
@@ -54,15 +56,13 @@ class ThreadService {
const rootInfo = await this.parseRootInfo(stuff) const rootInfo = await this.parseRootInfo(stuff)
if (!rootInfo) return if (!rootInfo) return
let subscription = this.subscriptions.get(rootInfo.id) const subscription = this.subscriptions.get(rootInfo.id)
if (subscription) { if (subscription) {
subscription.count += 1 subscription.count += 1
return return
} }
subscription = { count: 1, until: dayjs().unix() } const _subscribe = async () => {
this.subscriptions.set(rootInfo.id, subscription)
let relayUrls: string[] = [] let relayUrls: string[] = []
const rootPubkey = (rootInfo as { pubkey?: string }).pubkey ?? event?.pubkey const rootPubkey = (rootInfo as { pubkey?: string }).pubkey ?? event?.pubkey
if (rootPubkey) { if (rootPubkey) {
@@ -116,10 +116,7 @@ class ThreadService {
limit limit
}) })
} }
const { closer, timelineKey } = await client.subscribeTimeline(
return new Promise<void>((resolve) => {
client
.subscribeTimeline(
filters.map((filter) => ({ filters.map((filter) => ({
urls: relayUrls.slice(0, 8), urls: relayUrls.slice(0, 8),
filter filter
@@ -130,9 +127,11 @@ class ThreadService {
this.addRepliesToThread(events) this.addRepliesToThread(events)
} }
if (eosed) { if (eosed) {
const subscription = this.subscriptions.get(rootInfo.id)
if (subscription) {
subscription.until = subscription.until =
events.length >= limit ? events[events.length - 1].created_at - 1 : undefined events.length >= limit ? events[events.length - 1].created_at - 1 : undefined
resolve() }
} }
}, },
onNew: (evt) => { onNew: (evt) => {
@@ -140,15 +139,16 @@ class ThreadService {
} }
} }
) )
.then(({ closer, timelineKey }) => { return { closer, timelineKey }
subscription.closer = closer }
subscription.timelineKey = timelineKey
}) const promise = _subscribe()
.catch(() => { this.subscriptions.set(rootInfo.id, {
this.subscriptions.delete(rootInfo.id) promise,
resolve() count: 1,
}) until: dayjs().unix()
}) })
await promise
} }
async unsubscribe(stuff: NostrEvent | string) { async unsubscribe(stuff: NostrEvent | string) {
@@ -162,7 +162,9 @@ class ThreadService {
subscription.count -= 1 subscription.count -= 1
if (subscription.count <= 0) { if (subscription.count <= 0) {
this.subscriptions.delete(rootInfo.id) this.subscriptions.delete(rootInfo.id)
subscription.closer?.() subscription.promise.then(({ closer }) => {
closer()
})
} }
}, 2000) }, 2000)
} }
@@ -172,12 +174,13 @@ class ThreadService {
if (!rootInfo) return false if (!rootInfo) return false
const subscription = this.subscriptions.get(rootInfo.id) const subscription = this.subscriptions.get(rootInfo.id)
if (!subscription) return false if (!subscription || !subscription.until) return false
const { timelineKey, until } = subscription const { timelineKey } = await subscription.promise
if (!timelineKey || !until) return false 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) this.addRepliesToThread(events)
const { event } = this.resolveStuff(stuff) const { event } = this.resolveStuff(stuff)