refactor: reverse top-level replies order

This commit is contained in:
codytseng
2025-12-24 13:00:46 +08:00
parent 7459a3d33a
commit 89f79b999c
4 changed files with 45 additions and 39 deletions

View File

@@ -92,6 +92,7 @@ const NoteList = forwardRef<
const supportTouch = useMemo(() => isTouchDevice(), []) const supportTouch = useMemo(() => isTouchDevice(), [])
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 loadingRef = useRef(false)
const shouldHideEvent = useCallback( const shouldHideEvent = useCallback(
(evt: Event) => { (evt: Event) => {
@@ -273,12 +274,14 @@ const NoteList = forwardRef<
if (!subRequests.length) return if (!subRequests.length) return
async function init() { async function init() {
loadingRef.current = true
setLoading(true) setLoading(true)
setEvents([]) setEvents([])
setNewEvents([]) setNewEvents([])
setHasMore(true) setHasMore(true)
if (showKinds?.length === 0 && subRequests.every(({ filter }) => !filter.kinds)) { if (showKinds?.length === 0 && subRequests.every(({ filter }) => !filter.kinds)) {
loadingRef.current = false
setLoading(false) setLoading(false)
setHasMore(false) setHasMore(false)
return () => {} return () => {}
@@ -309,6 +312,7 @@ const NoteList = forwardRef<
setHasMore(false) setHasMore(false)
} }
if (eosed) { if (eosed) {
loadingRef.current = false
setLoading(false) setLoading(false)
addReplies(events) addReplies(events)
} }
@@ -374,13 +378,15 @@ const NoteList = forwardRef<
} }
} }
if (!timelineKey || loading || !hasMore) return if (!timelineKey || loadingRef.current || !hasMore) return
loadingRef.current = true
setLoading(true) setLoading(true)
const newEvents = await client.loadMoreTimeline( const newEvents = await client.loadMoreTimeline(
timelineKey, timelineKey,
events.length ? events[events.length - 1].created_at - 1 : dayjs().unix(), events.length ? events[events.length - 1].created_at - 1 : dayjs().unix(),
LIMIT LIMIT
) )
loadingRef.current = false
setLoading(false) setLoading(false)
if (newEvents.length === 0) { if (newEvents.length === 0) {
setHasMore(false) setHasMore(false)
@@ -406,7 +412,7 @@ const NoteList = forwardRef<
observerInstance.unobserve(currentBottomRef) observerInstance.unobserve(currentBottomRef)
} }
} }
}, [loading, hasMore, events, showCount, timelineKey]) }, [hasMore, events, showCount, timelineKey])
const showNewEvents = () => { const showNewEvents = () => {
setEvents((oldEvents) => [...newEvents, ...oldEvents]) setEvents((oldEvents) => [...newEvents, ...oldEvents])

View File

@@ -88,7 +88,7 @@ export default function ReplyNote({
)} )}
onClick={() => push(toNote(event))} onClick={() => push(toNote(event))}
> >
{hasReplies && <div className="absolute left-[34px] top-14 bottom-0 border-l" />} {hasReplies && <div className="absolute left-[34px] top-14 bottom-0 border-l z-20" />}
<Collapsible> <Collapsible>
<div className="flex space-x-2 items-start px-4 pt-3"> <div className="flex space-x-2 items-start px-4 pt-3">
<UserAvatar userId={event.pubkey} size="medium" className="shrink-0 mt-0.5" /> <UserAvatar userId={event.pubkey} size="medium" className="shrink-0 mt-0.5" />

View File

@@ -94,7 +94,7 @@ export default function SubReplies({ parentKey }: { parentKey: string }) {
className="relative w-full flex items-center gap-1.5 pl-14 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors clickable" className="relative w-full flex items-center gap-1.5 pl-14 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors clickable"
> >
<div <div
className={cn('absolute left-[34px] top-0 bottom-0 w-px text-border')} className={cn('absolute left-[34px] top-0 bottom-0 w-px text-border z-20')}
style={{ style={{
background: isExpanded background: isExpanded
? 'currentColor' ? 'currentColor'
@@ -132,9 +132,9 @@ export default function SubReplies({ parentKey }: { parentKey: string }) {
key={currentReplyKey} key={currentReplyKey}
className="scroll-mt-12 flex relative" className="scroll-mt-12 flex relative"
> >
<div className="absolute left-[34px] top-0 h-8 w-4 rounded-bl-lg border-l border-b" /> <div className="absolute left-[34px] top-0 h-8 w-4 rounded-bl-lg border-l border-b z-20" />
{index < replies.length - 1 && ( {index < replies.length - 1 && (
<div className="absolute left-[34px] top-0 bottom-0 border-l" /> <div className="absolute left-[34px] top-0 bottom-0 border-l z-20" />
)} )}
<ReplyNote <ReplyNote
className="flex-1 w-0 pl-10" className="flex-1 w-0 pl-10"

View File

@@ -16,7 +16,7 @@ import { useReply } from '@/providers/ReplyProvider'
import { useUserTrust } from '@/providers/UserTrustProvider' import { useUserTrust } from '@/providers/UserTrustProvider'
import client from '@/services/client.service' import client from '@/services/client.service'
import { Filter, Event as NEvent, kinds } from 'nostr-tools' import { Filter, Event as NEvent, kinds } from 'nostr-tools'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { 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'
@@ -69,7 +69,7 @@ export default function ReplyNoteList({
replyKeySet.add(key) replyKeySet.add(key)
return true return true
}) })
return replyEvents.sort((a, b) => a.created_at - b.created_at) return replyEvents.sort((a, b) => b.created_at - a.created_at)
}, [ }, [
stuffKey, stuffKey,
repliesMap, repliesMap,
@@ -79,8 +79,9 @@ export default function ReplyNoteList({
]) ])
const [timelineKey, setTimelineKey] = useState<string | undefined>(undefined) const [timelineKey, setTimelineKey] = useState<string | undefined>(undefined)
const [until, setUntil] = useState<number | undefined>(undefined) const [until, setUntil] = useState<number | undefined>(undefined)
const [loading, setLoading] = useState<boolean>(false)
const [showCount, setShowCount] = useState(SHOW_COUNT) const [showCount, setShowCount] = useState(SHOW_COUNT)
const [loading, setLoading] = useState<boolean>(false)
const loadingRef = useRef(false)
const bottomRef = useRef<HTMLDivElement | null>(null) const bottomRef = useRef<HTMLDivElement | null>(null)
useEffect(() => { useEffect(() => {
@@ -125,9 +126,10 @@ export default function ReplyNoteList({
}, [event]) }, [event])
useEffect(() => { useEffect(() => {
if (loading || !rootInfo || currentIndex !== index) return if (loadingRef.current || !rootInfo || currentIndex !== index) return
const init = async () => { const init = async () => {
loadingRef.current = true
setLoading(true) setLoading(true)
try { try {
@@ -195,6 +197,7 @@ export default function ReplyNoteList({
addReplies(evts) addReplies(evts)
} }
if (eosed) { if (eosed) {
loadingRef.current = false
setUntil(evts.length >= LIMIT ? evts[evts.length - 1].created_at - 1 : undefined) setUntil(evts.length >= LIMIT ? evts[evts.length - 1].created_at - 1 : undefined)
setLoading(false) setLoading(false)
} }
@@ -207,6 +210,7 @@ export default function ReplyNoteList({
setTimelineKey(timelineKey) setTimelineKey(timelineKey)
return closer return closer
} catch { } catch {
loadingRef.current = false
setLoading(false) setLoading(false)
} }
return return
@@ -218,12 +222,6 @@ export default function ReplyNoteList({
} }
}, [rootInfo, currentIndex, index]) }, [rootInfo, currentIndex, index])
useEffect(() => {
if (replies.length === 0) {
loadMore()
}
}, [replies])
useEffect(() => { useEffect(() => {
const options = { const options = {
root: null, root: null,
@@ -231,9 +229,28 @@ export default function ReplyNoteList({
threshold: 0.1 threshold: 0.1
} }
const observerInstance = new IntersectionObserver((entries) => { const loadMore = async () => {
if (entries[0].isIntersecting && showCount < replies.length) { if (showCount < replies.length) {
setShowCount((prev) => prev + SHOW_COUNT) setShowCount((prev) => prev + SHOW_COUNT)
// preload more
if (replies.length - showCount > LIMIT / 2) {
return
}
}
if (loadingRef.current || !until || !timelineKey) return
loadingRef.current = true
setLoading(true)
const events = await client.loadMoreTimeline(timelineKey, until, LIMIT)
addReplies(events)
setUntil(events.length ? events[events.length - 1].created_at - 1 : undefined)
loadingRef.current = false
setLoading(false)
}
const observerInstance = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && !!until) {
loadMore()
} }
}, options) }, options)
@@ -248,41 +265,24 @@ export default function ReplyNoteList({
observerInstance.unobserve(currentBottomRef) observerInstance.unobserve(currentBottomRef)
} }
} }
}, [replies, showCount]) }, [replies, showCount, until, timelineKey])
const loadMore = useCallback(async () => {
if (loading || !until || !timelineKey) return
setLoading(true)
const events = await client.loadMoreTimeline(timelineKey, until, LIMIT)
addReplies(events)
setUntil(events.length ? events[events.length - 1].created_at - 1 : undefined)
setLoading(false)
}, [loading, until, timelineKey])
return ( return (
<div className="min-h-[80vh]"> <div className="min-h-[80vh]">
{loading && <LoadingBar />} {loading && <LoadingBar />}
{!loading && until && (!event || until > event.created_at) && (
<div
className={`text-sm text-center text-muted-foreground border-b py-2 ${!loading ? 'hover:text-foreground cursor-pointer' : ''}`}
onClick={loadMore}
>
{t('load more older replies')}
</div>
)}
<div> <div>
{replies.slice(0, showCount).map((reply) => ( {replies.slice(0, showCount).map((reply) => (
<Item key={reply.id} reply={reply} /> <Item key={reply.id} reply={reply} />
))} ))}
</div> </div>
{!loading && ( {!!until || showCount < replies.length || loading ? (
<ReplyNoteSkeleton />
) : (
<div className="text-sm mt-2 mb-3 text-center text-muted-foreground"> <div className="text-sm mt-2 mb-3 text-center text-muted-foreground">
{replies.length > 0 ? t('no more replies') : t('no replies')} {replies.length > 0 ? t('no more replies') : t('no replies')}
</div> </div>
)} )}
<div ref={bottomRef} /> <div ref={bottomRef} />
{loading && <ReplyNoteSkeleton />}
</div> </div>
) )
} }