This commit is contained in:
codytseng 2024-11-03 22:43:51 +08:00
parent 9beaffb272
commit bfc07545b3
28 changed files with 665 additions and 608 deletions

View file

@ -1,99 +1,68 @@
import { Button } from '@renderer/components/ui/button'
import { isReplyNoteEvent } from '@renderer/lib/event'
import { cn } from '@renderer/lib/utils'
import { useRelaySettings } from '@renderer/providers/RelaySettingsProvider'
import client from '@renderer/services/client.service'
import { EVENT_TYPES, eventBus } from '@renderer/services/event-bus.service'
import dayjs from 'dayjs'
import { RefreshCcw } from 'lucide-react'
import { Event, Filter, kinds } from 'nostr-tools'
import { useEffect, useMemo, useRef, useState } from 'react'
import NoteCard from '../NoteCard'
export default function NoteList({
filter = {},
className,
isHomeTimeline = false
className
}: {
filter?: Filter
className?: string
isHomeTimeline?: boolean
}) {
const [events, setEvents] = useState<Event[]>([])
const [since, setSince] = useState<number>(() => dayjs().unix() + 1)
const [newEvents, setNewEvents] = useState<Event[]>([])
const [until, setUntil] = useState<number>(() => dayjs().unix())
const [hasMore, setHasMore] = useState<boolean>(true)
const [refreshedAt, setRefreshedAt] = useState<number>(() => dayjs().unix())
const [refreshing, setRefreshing] = useState<boolean>(false)
const [initialized, setInitialized] = useState(false)
const observer = useRef<IntersectionObserver | null>(null)
const bottomRef = useRef<HTMLDivElement | null>(null)
const { relayUrls } = useRelaySettings()
const noteFilter = useMemo(() => {
return {
kinds: [kinds.ShortTextNote, kinds.Repost],
limit: 50,
limit: 100,
...filter
}
}, [filter])
}, [JSON.stringify(filter)])
useEffect(() => {
if (!isHomeTimeline) return
if (relayUrls.length === 0) return
const handleClearList = () => {
setEvents([])
setSince(dayjs().unix() + 1)
setUntil(dayjs().unix())
setHasMore(true)
setRefreshedAt(dayjs().unix())
setRefreshing(false)
}
setInitialized(false)
setEvents([])
setNewEvents([])
setHasMore(true)
eventBus.on(EVENT_TYPES.RELOAD_TIMELINE, handleClearList)
const sub = client.subscribeEvents(relayUrls, noteFilter, {
onEose: (events) => {
const processedEvents = events.filter((e) => !isReplyNoteEvent(e))
setEvents((pre) => [...pre, ...processedEvents])
if (events.length > 0) {
setUntil(events[events.length - 1].created_at - 1)
}
setInitialized(true)
},
onNew: (event) => {
if (!isReplyNoteEvent(event)) {
setNewEvents((oldEvents) => [event, ...oldEvents])
}
}
})
return () => {
eventBus.remove(EVENT_TYPES.RELOAD_TIMELINE, handleClearList)
sub.close()
}
}, [])
const loadMore = async () => {
const events = await client.fetchEvents([{ ...noteFilter, until }])
if (events.length === 0) {
setHasMore(false)
return
}
const sortedEvents = events.sort((a, b) => b.created_at - a.created_at)
const processedEvents = sortedEvents.filter((e) => !isReplyNoteEvent(e))
if (processedEvents.length > 0) {
setEvents((oldEvents) => [...oldEvents, ...processedEvents])
}
setUntil(sortedEvents[sortedEvents.length - 1].created_at - 1)
}
const refresh = async () => {
const now = dayjs().unix()
setRefreshing(true)
const events = await client.fetchEvents([{ ...noteFilter, until: now, since }])
const sortedEvents = events.sort((a, b) => b.created_at - a.created_at)
const processedEvents = sortedEvents.filter((e) => !isReplyNoteEvent(e))
if (sortedEvents.length >= noteFilter.limit) {
// reset
setEvents(processedEvents)
setUntil(sortedEvents[sortedEvents.length - 1].created_at - 1)
} else if (processedEvents.length > 0) {
// append
setEvents((oldEvents) => [...processedEvents, ...oldEvents])
}
if (sortedEvents.length > 0) {
setSince(sortedEvents[0].created_at + 1)
}
setRefreshedAt(now)
setRefreshing(false)
}
}, [JSON.stringify(relayUrls), JSON.stringify(noteFilter)])
useEffect(() => {
if (!initialized) return
const options = {
root: null,
rootMargin: '10px',
@ -115,26 +84,39 @@ export default function NoteList({
observer.current.unobserve(bottomRef.current)
}
}
}, [until])
}, [initialized])
const loadMore = async () => {
const events = await client.fetchEvents({ ...noteFilter, until })
const sortedEvents = events.sort((a, b) => b.created_at - a.created_at)
if (sortedEvents.length === 0) {
setHasMore(false)
return
}
const processedEvents = sortedEvents.filter((e) => !isReplyNoteEvent(e))
if (processedEvents.length > 0) {
setEvents((oldEvents) => [...oldEvents, ...processedEvents])
}
setUntil(sortedEvents[sortedEvents.length - 1].created_at - 1)
}
const showNewEvents = () => {
setEvents((oldEvents) => [...newEvents, ...oldEvents])
setNewEvents([])
}
return (
<>
{events.length > 0 && (
<div
className={`flex justify-center items-center gap-1 mb-2 text-muted-foreground ${!refreshing ? 'hover:text-foreground cursor-pointer' : ''}`}
onClick={refresh}
>
<RefreshCcw size={12} className={`${refreshing ? 'animate-spin' : ''}`} />
<div className="text-xs">
{refreshing
? 'refreshing...'
: `last refreshed at ${dayjs(refreshedAt * 1000).format('HH:mm:ss')}`}
</div>
{newEvents.length > 0 && (
<div className="flex justify-center w-full mb-4">
<Button onClick={showNewEvents}>show new notes</Button>
</div>
)}
<div className={cn('flex flex-col gap-4', className)}>
{events.map((event, i) => (
<NoteCard key={i} className="w-full" event={event} />
<NoteCard key={`${i}-${event.id}`} className="w-full" event={event} />
))}
</div>
<div className="text-center text-xs text-muted-foreground mt-2">