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

View File

@@ -88,7 +88,7 @@ export default function ReplyNote({
)}
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>
<div className="flex space-x-2 items-start px-4 pt-3">
<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"
>
<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={{
background: isExpanded
? 'currentColor'
@@ -132,9 +132,9 @@ export default function SubReplies({ parentKey }: { parentKey: string }) {
key={currentReplyKey}
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 && (
<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
className="flex-1 w-0 pl-10"

View File

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