feat: replace trending notes service

This commit is contained in:
codytseng
2025-11-21 10:35:45 +08:00
parent 18ae2a5fd4
commit 8edec0f7f6
3 changed files with 18 additions and 116 deletions

View File

@@ -1,92 +1,27 @@
import NoteCard, { NoteCardLoadingSkeleton } from '@/components/NoteCard'
import { getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event'
import { useDeletedEvent } from '@/providers/DeletedEventProvider'
import { useUserTrust } from '@/providers/UserTrustProvider'
import client from '@/services/client.service'
import { NostrEvent } from 'nostr-tools'
import { useEffect, useMemo, useRef, useState } from 'react'
import { TRENDING_NOTES_RELAY_URLS } from '@/constants'
import { simplifyUrl } from '@/lib/url'
import { useTranslation } from 'react-i18next'
import NormalFeed from '../NormalFeed'
const SHOW_COUNT = 10
const RESOURCE_DESCRIPTION = TRENDING_NOTES_RELAY_URLS.map((url) => simplifyUrl(url)).join(', ')
export default function TrendingNotes() {
const { t } = useTranslation()
const { isEventDeleted } = useDeletedEvent()
const { hideUntrustedNotes, isUserTrusted } = useUserTrust()
const [trendingNotes, setTrendingNotes] = useState<NostrEvent[]>([])
const [showCount, setShowCount] = useState(10)
const [loading, setLoading] = useState(true)
const bottomRef = useRef<HTMLDivElement>(null)
const filteredEvents = useMemo(() => {
const idSet = new Set<string>()
return trendingNotes.slice(0, showCount).filter((evt) => {
if (isEventDeleted(evt)) return false
if (hideUntrustedNotes && !isUserTrusted(evt.pubkey)) return false
const id = isReplaceableEvent(evt.kind) ? getReplaceableCoordinateFromEvent(evt) : evt.id
if (idSet.has(id)) {
return false
}
idSet.add(id)
return true
})
}, [trendingNotes, hideUntrustedNotes, showCount, isEventDeleted])
useEffect(() => {
const fetchTrendingPosts = async () => {
setLoading(true)
const events = await client.fetchTrendingNotes()
setTrendingNotes(events)
setLoading(false)
}
fetchTrendingPosts()
}, [])
useEffect(() => {
if (showCount >= trendingNotes.length) return
const options = {
root: null,
rootMargin: '10px',
threshold: 0.1
}
const observerInstance = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
setShowCount((prev) => prev + SHOW_COUNT)
}
}, options)
const currentBottomRef = bottomRef.current
if (currentBottomRef) {
observerInstance.observe(currentBottomRef)
}
return () => {
if (observerInstance && currentBottomRef) {
observerInstance.unobserve(currentBottomRef)
}
}
}, [loading, trendingNotes, showCount])
return (
<div className="min-h-screen">
<div className="sticky top-12 h-12 px-4 flex flex-col justify-center text-lg font-bold bg-background z-30 border-b">
{t('Trending Notes')}
</div>
{filteredEvents.map((event) => (
<NoteCard key={event.id} className="w-full" event={event} />
))}
{showCount < trendingNotes.length || loading ? (
<div ref={bottomRef}>
<NoteCardLoadingSkeleton />
<div>
<div className="top-12 h-12 px-4 flex flex-col justify-center text-lg font-bold bg-background z-30 border-b">
<div className="flex items-center gap-2">
{t('Trending Notes')}
<span className="text-sm text-muted-foreground font-normal">
({RESOURCE_DESCRIPTION})
</span>
</div>
) : (
<div className="text-center text-sm text-muted-foreground mt-2">{t('no more notes')}</div>
)}
</div>
<NormalFeed
subRequests={[{ urls: TRENDING_NOTES_RELAY_URLS, filter: {} }]}
showRelayCloseReason
/>
</div>
)
}

View File

@@ -65,6 +65,8 @@ export const BIG_RELAY_URLS = [
export const SEARCHABLE_RELAY_URLS = ['wss://relay.nostr.band/', 'wss://search.nos.today/']
export const TRENDING_NOTES_RELAY_URLS = ['wss://trending.relays.land/']
export const GROUP_METADATA_EVENT_KIND = 39000
export const ExtendedKind = {

View File

@@ -24,9 +24,7 @@ import {
matchFilters,
Event as NEvent,
nip19,
Relay,
SimplePool,
validateEvent,
VerifiedEvent
} from 'nostr-tools'
import { AbstractRelay } from 'nostr-tools/abstract-relay'
@@ -799,39 +797,6 @@ class ClientService extends EventTarget {
return this.eventDataLoader.load(id)
}
async fetchTrendingNotes() {
if (this.trendingNotesCache) {
return this.trendingNotesCache
}
try {
const response = await fetch('https://api.nostr.band/v0/trending/notes')
const data = await response.json()
const events: NEvent[] = []
for (const note of data.notes ?? []) {
if (validateEvent(note.event)) {
events.push(note.event)
this.addEventToCache(note.event)
if (note.relays?.length) {
note.relays.map((r: string) => {
try {
const relay = new Relay(r)
this.trackEventSeenOn(note.event.id, relay)
} catch {
return null
}
})
}
}
}
this.trendingNotesCache = events
return this.trendingNotesCache
} catch (error) {
console.error('fetchTrendingNotes error', error)
return []
}
}
addEventToCache(event: NEvent) {
this.eventDataLoader.prime(event.id, Promise.resolve(event))
if (isReplaceableEvent(event.kind)) {