Browse Source

bug-fixes

imwald
Silberengel 1 month ago
parent
commit
b34adbb553
  1. 1
      nip66-cron/index.mjs
  2. 18
      src/components/Explore/ExploreFavoriteRelays.tsx
  3. 23
      src/components/Explore/ExploreRelayReviews.tsx
  4. 35
      src/components/NoteList/index.tsx
  5. 5
      src/components/NoteStats/index.tsx
  6. 1
      src/constants.ts
  7. 27
      src/lib/relay-list-builder.ts
  8. 13
      src/pages/primary/SpellsPage/fauxSpellFeeds.ts
  9. 6
      src/services/client-replaceable-events.service.ts

1
nip66-cron/index.mjs

@ -60,7 +60,6 @@ const DEFAULT_RELAYS_TO_MONITOR = [
'wss://relay.noswhere.com', 'wss://relay.noswhere.com',
'wss://relay.wikifreedia.xyz', 'wss://relay.wikifreedia.xyz',
'wss://nostr.einundzwanzig.space', 'wss://nostr.einundzwanzig.space',
'wss://relay.lumina.rocks',
'wss://nostrelites.org', 'wss://nostrelites.org',
'wss://relay.nsec.app', 'wss://relay.nsec.app',
'wss://bucket.coracle.social', 'wss://bucket.coracle.social',

18
src/components/Explore/ExploreFavoriteRelays.tsx

@ -2,12 +2,12 @@ import RelaySimpleInfo, { RelaySimpleInfoSkeleton } from '@/components/RelaySimp
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { DEFAULT_FAVORITE_RELAYS } from '@/constants' import { DEFAULT_FAVORITE_RELAYS } from '@/constants'
import { useFetchRelayInfo } from '@/hooks' import { useFetchRelayInfo } from '@/hooks'
import { toRelay } from '@/lib/link' import { toRelay, toRelaySettings } from '@/lib/link'
import { normalizeUrl, simplifyUrl } from '@/lib/url' import { normalizeUrl, simplifyUrl } from '@/lib/url'
import { usePrimaryPage, useSmartRelayNavigation } from '@/PageManager' import { usePrimaryPage, useSecondaryPage, useSmartRelayNavigation } from '@/PageManager'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Newspaper } from 'lucide-react' import { Newspaper, Settings } from 'lucide-react'
import { useMemo } from 'react' import { useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -58,6 +58,7 @@ function FavoriteRelayCard({ url }: { url: string }) {
export default function ExploreFavoriteRelays() { export default function ExploreFavoriteRelays() {
const { t } = useTranslation() const { t } = useTranslation()
const { navigate } = usePrimaryPage() const { navigate } = usePrimaryPage()
const { push } = useSecondaryPage()
const { favoriteRelays, blockedRelays } = useFavoriteRelays() const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const blockedSet = useMemo( const blockedSet = useMemo(
@ -100,6 +101,17 @@ export default function ExploreFavoriteRelays() {
<Newspaper className="size-4 shrink-0" strokeWidth={2.5} /> <Newspaper className="size-4 shrink-0" strokeWidth={2.5} />
<span>{t('Favorites Feed')}</span> <span>{t('Favorites Feed')}</span>
</Button> </Button>
<Button
type="button"
variant="outline"
size="icon"
className="h-8 w-8 shrink-0"
aria-label={t('Relays and Storage Settings')}
title={t('Relays and Storage Settings')}
onClick={() => push(toRelaySettings('favorite-relays'))}
>
<Settings className="size-4 shrink-0" strokeWidth={2.5} />
</Button>
</div> </div>
{usingDefaults ? ( {usingDefaults ? (
<span className="text-xs text-muted-foreground">{t('Using app default relays')}</span> <span className="text-xs text-muted-foreground">{t('Using app default relays')}</span>

23
src/components/Explore/ExploreRelayReviews.tsx

@ -1,13 +1,30 @@
import NoteList from '@/components/NoteList' import NoteList from '@/components/NoteList'
import { ExtendedKind, FAST_READ_RELAY_URLS } from '@/constants' import { ExtendedKind, PROFILE_FETCH_RELAY_URLS } from '@/constants'
import { import {
getRelayUrlFromRelayReviewEvent, getRelayUrlFromRelayReviewEvent,
getStarsFromRelayReviewEvent getStarsFromRelayReviewEvent
} from '@/lib/event-metadata' } from '@/lib/event-metadata'
import { buildExploreProfileAndUserRelayList } from '@/lib/relay-list-builder'
import { useNostr } from '@/providers/NostrProvider'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useCallback } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
export default function ExploreRelayReviews() { export default function ExploreRelayReviews() {
const { pubkey } = useNostr()
const [relayUrls, setRelayUrls] = useState<string[]>(() => [...PROFILE_FETCH_RELAY_URLS])
useEffect(() => {
let cancelled = false
buildExploreProfileAndUserRelayList(pubkey ?? null).then((urls) => {
if (!cancelled) setRelayUrls(urls)
})
return () => {
cancelled = true
}
}, [pubkey])
const subRequests = useMemo(() => [{ urls: relayUrls, filter: {} }], [relayUrls])
const extraShouldHideEvent = useCallback((evt: Event) => { const extraShouldHideEvent = useCallback((evt: Event) => {
if (evt.kind !== ExtendedKind.RELAY_REVIEW) return false if (evt.kind !== ExtendedKind.RELAY_REVIEW) return false
if (!getRelayUrlFromRelayReviewEvent(evt)) return true if (!getRelayUrlFromRelayReviewEvent(evt)) return true
@ -18,7 +35,7 @@ export default function ExploreRelayReviews() {
<div className="min-w-0 pt-1"> <div className="min-w-0 pt-1">
<NoteList <NoteList
showKinds={[ExtendedKind.RELAY_REVIEW]} showKinds={[ExtendedKind.RELAY_REVIEW]}
subRequests={[{ urls: [...FAST_READ_RELAY_URLS], filter: {} }]} subRequests={subRequests}
showKind1OPs={false} showKind1OPs={false}
showKind1Replies={false} showKind1Replies={false}
showKind1111={false} showKind1111={false}

35
src/components/NoteList/index.tsx

@ -237,6 +237,9 @@ const NoteList = forwardRef(
return () => {} return () => {}
} }
/** False after cleanup so stale timeline callbacks cannot overwrite state after switching feeds (e.g. Spells discussions → notifications). */
let effectActive = true
async function init() { async function init() {
setLoading(true) setLoading(true)
setEvents([]) setEvents([])
@ -281,7 +284,10 @@ const NoteList = forwardRef(
let closer: (() => void) | undefined let closer: (() => void) | undefined
let timelineKey: string | undefined let timelineKey: string | undefined
let timelineSubscribePromise:
| Promise<{ closer: () => void; timelineKey: string }>
| undefined
try { try {
// Add timeout wrapper to prevent subscribeTimeline from hanging indefinitely // Add timeout wrapper to prevent subscribeTimeline from hanging indefinitely
const timeoutPromise = new Promise<never>((_, reject) => { const timeoutPromise = new Promise<never>((_, reject) => {
@ -290,11 +296,11 @@ const NoteList = forwardRef(
}, 5000) // 5 second timeout }, 5000) // 5 second timeout
}) })
const result = await Promise.race([ timelineSubscribePromise = client.subscribeTimeline(
client.subscribeTimeline(
mappedSubRequests, mappedSubRequests,
{ {
onEvents: (events: Event[], eosed: boolean) => { onEvents: (events: Event[], eosed: boolean) => {
if (!effectActive) return
if (events.length > 0) { if (events.length > 0) {
setEvents(events) setEvents(events)
// Do not wait for full EOSE across many relays — otherwise loading/skeleton stays up for 10–30s+ // Do not wait for full EOSE across many relays — otherwise loading/skeleton stays up for 10–30s+
@ -314,6 +320,7 @@ const NoteList = forwardRef(
pubkeysToFetch.forEach((p) => prefetchedPubkeysRef.current.add(p)) pubkeysToFetch.forEach((p) => prefetchedPubkeysRef.current.add(p))
// Batch fetch in background (non-blocking) with delay to not block initial render // Batch fetch in background (non-blocking) with delay to not block initial render
setTimeout(() => { setTimeout(() => {
if (!effectActive) return
client.fetchProfilesForPubkeys(pubkeysToFetch).catch(() => { client.fetchProfilesForPubkeys(pubkeysToFetch).catch(() => {
// On error, remove from prefetched set so we can retry later // On error, remove from prefetched set so we can retry later
pubkeysToFetch.forEach((p) => prefetchedPubkeysRef.current.delete(p)) pubkeysToFetch.forEach((p) => prefetchedPubkeysRef.current.delete(p))
@ -337,6 +344,7 @@ const NoteList = forwardRef(
eventIdsToFetch.forEach((id) => prefetchedEventIdsRef.current.add(id)) eventIdsToFetch.forEach((id) => prefetchedEventIdsRef.current.add(id))
// Batch fetch embedded events in background (non-blocking) with delay // Batch fetch embedded events in background (non-blocking) with delay
setTimeout(() => { setTimeout(() => {
if (!effectActive) return
Promise.all(eventIdsToFetch.map((id) => client.fetchEvent(id))).catch(() => { Promise.all(eventIdsToFetch.map((id) => client.fetchEvent(id))).catch(() => {
// On error, remove from prefetched set so we can retry later // On error, remove from prefetched set so we can retry later
eventIdsToFetch.forEach((id) => prefetchedEventIdsRef.current.delete(id)) eventIdsToFetch.forEach((id) => prefetchedEventIdsRef.current.delete(id))
@ -369,6 +377,7 @@ const NoteList = forwardRef(
} }
}, },
onNew: (event: Event) => { onNew: (event: Event) => {
if (!effectActive) return
if (!useFilterAsIs && !showKinds.includes(event.kind)) return if (!useFilterAsIs && !showKinds.includes(event.kind)) return
if (event.kind === kinds.ShortTextNote) { if (event.kind === kinds.ShortTextNote) {
const isReply = isReplyNoteEvent(event) const isReply = isReplyNoteEvent(event)
@ -395,22 +404,34 @@ const NoteList = forwardRef(
needSort: !areAlgoRelays, needSort: !areAlgoRelays,
useCache: false // Main feeds should always fetch fresh from relays, not use cache useCache: false // Main feeds should always fetch fresh from relays, not use cache
} }
), )
timeoutPromise
]) const result = await Promise.race([timelineSubscribePromise, timeoutPromise])
if (!effectActive) {
result.closer()
return () => {}
}
closer = result.closer closer = result.closer
timelineKey = result.timelineKey timelineKey = result.timelineKey
setTimelineKey(timelineKey) setTimelineKey(timelineKey)
return closer return closer
} catch (_error) { } catch (_error) {
setLoading(false) setLoading(false)
// Return a no-op closer function instead of throwing - allows cleanup to work // Race timeout or subscribe failure: if the timeline promise later resolves, close or subs leak (relay slots + stale setEvents).
if (timelineSubscribePromise) {
void timelineSubscribePromise
.then((r) => {
r.closer()
})
.catch(() => {})
}
return () => {} return () => {}
} }
} }
const promise = init() const promise = init()
return () => { return () => {
effectActive = false
promise.then((closer) => closer?.()) promise.then((closer) => closer?.())
} }
}, [ }, [

5
src/components/NoteStats/index.tsx

@ -74,7 +74,8 @@ export default function NoteStats({
{displayTopZapsAndLikes && ( {displayTopZapsAndLikes && (
<> <>
<TopZaps event={event} /> <TopZaps event={event} />
<Likes event={event} /> {/* Kind 11: LikeButton already shows ⬆/⬇; Likes row would duplicate those pills */}
{!isDiscussion && <Likes event={event} />}
</> </>
)} )}
<div <div
@ -100,7 +101,7 @@ export default function NoteStats({
{displayTopZapsAndLikes && ( {displayTopZapsAndLikes && (
<> <>
<TopZaps event={event} /> <TopZaps event={event} />
<Likes event={event} /> {!isDiscussion && <Likes event={event} />}
</> </>
)} )}
<div className="flex justify-between h-5 [&_svg]:size-4"> <div className="flex justify-between h-5 [&_svg]:size-4">

1
src/constants.ts

@ -179,7 +179,6 @@ export const SEARCHABLE_RELAY_URLS = [
'wss://relay.noswhere.com', 'wss://relay.noswhere.com',
'wss://relay.wikifreedia.xyz', 'wss://relay.wikifreedia.xyz',
'wss://nostr.einundzwanzig.space', 'wss://nostr.einundzwanzig.space',
'wss://relay.lumina.rocks',
'wss://nostrelites.org', 'wss://nostrelites.org',
'wss://relay.nsec.app', 'wss://relay.nsec.app',
'wss://bucket.coracle.social', 'wss://bucket.coracle.social',

27
src/lib/relay-list-builder.ts

@ -230,6 +230,33 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio
return Array.from(relayUrls) return Array.from(relayUrls)
} }
/**
* Explore: Following's Favorites (kind 10012 batch) and Relay reviews tab.
* PROFILE_FETCH_RELAY_URLS plus the viewer's read/write and cache (10432) relays no FAST_READ.
*/
export async function buildExploreProfileAndUserRelayList(
userPubkey: string | null | undefined
): Promise<string[]> {
if (!userPubkey) {
return Array.from(new Set([...PROFILE_FETCH_RELAY_URLS]))
}
try {
const built = await buildComprehensiveRelayList({
userPubkey,
includeUserOwnRelays: true,
includeProfileFetchRelays: true,
includeFastReadRelays: false,
includeFavoriteRelays: false,
includeLocalRelays: true,
includeFastWriteRelays: false,
includeSearchableRelays: false
})
return built.length > 0 ? built : Array.from(new Set([...PROFILE_FETCH_RELAY_URLS]))
} catch {
return Array.from(new Set([...PROFILE_FETCH_RELAY_URLS]))
}
}
/** /**
* Build relay list for reading replies/comments * Build relay list for reading replies/comments
* READ from: FAST_READ_RELAY_URLS + user's inboxes + local relays + OP author's outboxes * READ from: FAST_READ_RELAY_URLS + user's inboxes + local relays + OP author's outboxes

13
src/pages/primary/SpellsPage/fauxSpellFeeds.ts

@ -17,6 +17,13 @@ const NOTIFICATION_LIMIT = 500
const DISCUSSION_LIMIT = 500 const DISCUSSION_LIMIT = 500
const MAX_BOOKMARK_IDS = 250 const MAX_BOOKMARK_IDS = 250
/**
* Spells Discussions uses NoteList subscribeTimeline one live REQ per relay.
* The same merged list as DiscussionsPages one-shot query would open 80+ sockets and exhaust
* subscription slots; cap keeps first paint fast. Full coverage remains on /discussions.
*/
const DISCUSSION_FAUX_SPELL_MAX_RELAYS = 32
export const MEDIA_SPELL_KINDS = [ export const MEDIA_SPELL_KINDS = [
ExtendedKind.PICTURE, ExtendedKind.PICTURE,
ExtendedKind.VIDEO, ExtendedKind.VIDEO,
@ -66,7 +73,10 @@ export function buildMentionsSpellFilter(pubkey: string): Filter {
} }
} }
/** Relay set for discussion threads (kind 11), aligned with DiscussionsPage’s merged list (sync). */ /**
* Relay set for Spells Discussions (kind 11): same merge order as DiscussionsPage, but capped
* for subscription-based loading (see DISCUSSION_FAUX_SPELL_MAX_RELAYS).
*/
export function discussionRelayUrls( export function discussionRelayUrls(
relayList: TRelayList | null | undefined, relayList: TRelayList | null | undefined,
favoriteRelays: string[], favoriteRelays: string[],
@ -83,6 +93,7 @@ export function discussionRelayUrls(
if (!k || seen.has(k) || blocked.has(k)) continue if (!k || seen.has(k) || blocked.has(k)) continue
seen.add(k) seen.add(k)
out.push(k) out.push(k)
if (out.length >= DISCUSSION_FAUX_SPELL_MAX_RELAYS) break
} }
return out return out
} }

6
src/services/client-replaceable-events.service.ts

@ -18,7 +18,7 @@ import indexedDb from './indexed-db.service'
import type { QueryService } from './client-query.service' import type { QueryService } from './client-query.service'
import logger from '@/lib/logger' import logger from '@/lib/logger'
import client from './client.service' import client from './client.service'
import { buildComprehensiveRelayList } from '@/lib/relay-list-builder' import { buildComprehensiveRelayList, buildExploreProfileAndUserRelayList } from '@/lib/relay-list-builder'
export class ReplaceableEventService { export class ReplaceableEventService {
private queryService: QueryService private queryService: QueryService
@ -436,6 +436,8 @@ export class ReplaceableEventService {
// For metadata with a logged-in user, merge defaults with {@link buildComprehensiveRelayList}: inboxes (read), // For metadata with a logged-in user, merge defaults with {@link buildComprehensiveRelayList}: inboxes (read),
// local/cache relays (10432), favorite relays (10012), plus profile + fast read — same idea as favorites feed // local/cache relays (10432), favorite relays (10012), plus profile + fast read — same idea as favorites feed
// / inbox-scoped discovery without per-author relay list fetches. // / inbox-scoped discovery without per-author relay list fetches.
// Following's Favorites (Explore): kind 10012 batch uses PROFILE_FETCH_RELAY_URLS + viewer's own relays only
// (no FAST_READ), so outbox data is queried where the user actually reads + profile-index relays.
let relayUrls: string[] let relayUrls: string[]
if (kind === kinds.Metadata) { if (kind === kinds.Metadata) {
const userPk = client.pubkey const userPk = client.pubkey
@ -457,6 +459,8 @@ export class ReplaceableEventService {
} else { } else {
relayUrls = Array.from(new Set([...PROFILE_FETCH_RELAY_URLS, ...FAST_READ_RELAY_URLS])) relayUrls = Array.from(new Set([...PROFILE_FETCH_RELAY_URLS, ...FAST_READ_RELAY_URLS]))
} }
} else if (kind === ExtendedKind.FAVORITE_RELAYS) {
relayUrls = await buildExploreProfileAndUserRelayList(client.pubkey)
} else { } else {
relayUrls = [...FAST_READ_RELAY_URLS] relayUrls = [...FAST_READ_RELAY_URLS]
} }

Loading…
Cancel
Save