Browse Source

fix empty message while relay reviews are still loading

imwald
Silberengel 1 month ago
parent
commit
f13fba50bf
  1. 4
      package-lock.json
  2. 2
      package.json
  3. 53
      src/components/Explore/ExploreRelayReviews.tsx

4
package-lock.json generated

@ -1,12 +1,12 @@
{ {
"name": "jumble-imwald", "name": "jumble-imwald",
"version": "19.1.1", "version": "19.2.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "jumble-imwald", "name": "jumble-imwald",
"version": "19.1.1", "version": "19.2.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@asciidoctor/core": "^3.0.4", "@asciidoctor/core": "^3.0.4",

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "jumble-imwald", "name": "jumble-imwald",
"version": "19.1.1", "version": "19.2.0",
"description": "A user-friendly Nostr client focused on relay feed browsing and relay discovery, forked from Jumble", "description": "A user-friendly Nostr client focused on relay feed browsing and relay discovery, forked from Jumble",
"private": true, "private": true,
"type": "module", "type": "module",

53
src/components/Explore/ExploreRelayReviews.tsx

@ -8,12 +8,17 @@ import { appendCuratedReadOnlyRelays } from '@/pages/primary/SpellsPage/fauxSpel
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import client from '@/services/client.service' import client from '@/services/client.service'
import { Loader2 } from 'lucide-react'
import type { Event } from 'nostr-tools' import type { Event } from 'nostr-tools'
import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
const REVIEW_QUERY_LIMIT = 100 const REVIEW_QUERY_LIMIT = 100
const SHOW_COUNT = 20 const SHOW_COUNT = 20
/** Fewer sockets + faster aggregate EOSE than full inbox stack; read-only mirrors still appended then capped. */
const EXPLORE_REVIEWS_MAX_RELAYS = 12
/** After all relays EOSE, wait longer than default so slow mirrors can flush events (default query eose is 500ms). */
const EXPLORE_REVIEWS_EOSE_TAIL_MS = 4500
function dedupeRelayReviewsNewestFirst(events: Event[]): Event[] { function dedupeRelayReviewsNewestFirst(events: Event[]): Event[] {
const sorted = [...events].sort((a, b) => b.created_at - a.created_at) const sorted = [...events].sort((a, b) => b.created_at - a.created_at)
@ -33,19 +38,22 @@ export default function ExploreRelayReviews() {
const { favoriteRelays, blockedRelays } = useFavoriteRelays() const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const { relayList } = useNostr() const { relayList } = useNostr()
const relayUrls = useMemo( const relayUrls = useMemo(() => {
() => const stacked = appendCuratedReadOnlyRelays(
appendCuratedReadOnlyRelays(
getRelayUrlsWithFavoritesFastReadAndInbox( getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays, favoriteRelays,
blockedRelays, blockedRelays,
relayList?.read ?? [], relayList?.read ?? [],
{ userWriteRelays: relayList?.write ?? [] } {
userWriteRelays: relayList?.write ?? [],
maxRelays: EXPLORE_REVIEWS_MAX_RELAYS,
applyKind1BlockedFilter: false
}
), ),
blockedRelays blockedRelays
),
[favoriteRelays, blockedRelays, relayList]
) )
return stacked.slice(0, EXPLORE_REVIEWS_MAX_RELAYS)
}, [favoriteRelays, blockedRelays, relayList])
const relayUrlsKey = useMemo(() => relayUrls.join('|'), [relayUrls]) const relayUrlsKey = useMemo(() => relayUrls.join('|'), [relayUrls])
@ -53,8 +61,10 @@ export default function ExploreRelayReviews() {
const [events, setEvents] = useState<Event[]>([]) const [events, setEvents] = useState<Event[]>([])
const [showCount, setShowCount] = useState(SHOW_COUNT) const [showCount, setShowCount] = useState(SHOW_COUNT)
const bottomRef = useRef<HTMLDivElement>(null) const bottomRef = useRef<HTMLDivElement>(null)
const fetchGenRef = useRef(0)
useEffect(() => { useEffect(() => {
const gen = ++fetchGenRef.current
let cancelled = false let cancelled = false
setLoading(true) setLoading(true)
setEvents([]) setEvents([])
@ -67,27 +77,26 @@ export default function ExploreRelayReviews() {
{ kinds: [ExtendedKind.RELAY_REVIEW], limit: REVIEW_QUERY_LIMIT }, { kinds: [ExtendedKind.RELAY_REVIEW], limit: REVIEW_QUERY_LIMIT },
{ {
onevent: (e) => { onevent: (e) => {
if (cancelled) return if (cancelled || fetchGenRef.current !== gen) return
if (e.kind === ExtendedKind.RELAY_REVIEW && getRelayUrlFromRelayReviewEvent(e)) { if (e.kind === ExtendedKind.RELAY_REVIEW && getRelayUrlFromRelayReviewEvent(e)) {
setLoading(false)
setEvents((prev) => dedupeRelayReviewsNewestFirst([...prev, e])) setEvents((prev) => dedupeRelayReviewsNewestFirst([...prev, e]))
} }
}, },
firstRelayResultGraceMs: FIRST_RELAY_RESULT_GRACE_MS, firstRelayResultGraceMs: FIRST_RELAY_RESULT_GRACE_MS,
globalTimeout: 12_000, globalTimeout: 12_000,
eoseTimeout: 800, eoseTimeout: EXPLORE_REVIEWS_EOSE_TAIL_MS,
cache: true cache: true
} }
) )
if (cancelled) return if (cancelled || fetchGenRef.current !== gen) return
const withRelay = raw.filter( const withRelay = raw.filter(
(e) => e.kind === ExtendedKind.RELAY_REVIEW && getRelayUrlFromRelayReviewEvent(e) (e) => e.kind === ExtendedKind.RELAY_REVIEW && getRelayUrlFromRelayReviewEvent(e)
) )
setEvents(dedupeRelayReviewsNewestFirst(withRelay)) setEvents((prev) => dedupeRelayReviewsNewestFirst([...prev, ...withRelay]))
} catch { } catch {
if (!cancelled) setEvents([]) if (!cancelled && fetchGenRef.current === gen) setEvents([])
} finally { } finally {
if (!cancelled) setLoading(false) if (!cancelled && fetchGenRef.current === gen) setLoading(false)
} }
})() })()
@ -111,16 +120,18 @@ export default function ExploreRelayReviews() {
}, [showCount, events.length]) }, [showCount, events.length])
const visible = events.slice(0, showCount) const visible = events.slice(0, showCount)
const showInitialSkeleton = loading && events.length === 0
const showEmptyAfterLoad = !loading && events.length === 0
return ( return (
<div className="min-w-0 pt-1 pb-8"> <div className="min-w-0 pt-1 pb-8">
{loading ? ( {showInitialSkeleton ? (
<div className="grid min-w-0 md:px-4 md:grid-cols-2 md:gap-3"> <div className="grid min-w-0 md:px-4 md:grid-cols-2 md:gap-3">
{Array.from({ length: 6 }).map((_, i) => ( {Array.from({ length: 6 }).map((_, i) => (
<Skeleton key={i} className="h-40 rounded-lg border md:border" /> <Skeleton key={i} className="h-40 rounded-lg border md:border" />
))} ))}
</div> </div>
) : events.length === 0 ? ( ) : showEmptyAfterLoad ? (
<p className="px-4 py-6 text-center text-sm text-muted-foreground">{t('no relays found')}</p> <p className="px-4 py-6 text-center text-sm text-muted-foreground">{t('no relays found')}</p>
) : ( ) : (
<> <>
@ -129,8 +140,18 @@ export default function ExploreRelayReviews() {
<RelayReviewCard key={event.id} event={event} className="border-b md:border md:border-border" /> <RelayReviewCard key={event.id} event={event} className="border-b md:border md:border-border" />
))} ))}
</div> </div>
{loading ? (
<div
className="mt-4 flex items-center justify-center gap-2 text-sm text-muted-foreground"
aria-busy="true"
aria-live="polite"
>
<Loader2 className="size-4 shrink-0 animate-spin" aria-hidden />
{t('Loading...')}
</div>
) : null}
{showCount < events.length ? <div ref={bottomRef} className="h-4" aria-hidden /> : null} {showCount < events.length ? <div ref={bottomRef} className="h-4" aria-hidden /> : null}
{showCount >= events.length ? ( {!loading && showCount >= events.length ? (
<p className="mt-3 text-center text-sm text-muted-foreground">{t('no more relays')}</p> <p className="mt-3 text-center text-sm text-muted-foreground">{t('no more relays')}</p>
) : null} ) : null}
</> </>

Loading…
Cancel
Save