Browse Source

bug-fixes

imwald
Silberengel 1 month ago
parent
commit
4237090623
  1. 14
      src/components/NoteList/index.tsx
  2. 25
      src/components/ReplyNoteList/index.tsx
  3. 63
      src/services/client-replaceable-events.service.ts
  4. 40
      src/services/client.service.ts

14
src/components/NoteList/index.tsx

@ -24,7 +24,7 @@ import {
isSpellSubRequestsSameFiltersDifferentRelays isSpellSubRequestsSameFiltersDifferentRelays
} from '@/lib/spell-feed-request-identity' } from '@/lib/spell-feed-request-identity'
import logger from '@/lib/logger' import logger from '@/lib/logger'
import { isLocalNetworkUrl, normalizeUrl } from '@/lib/url' import { isLocalNetworkUrl, normalizeAnyRelayUrl, normalizeUrl } from '@/lib/url'
import { eventPassesNoteListKindPicker } from '@/lib/feed-kind-filter' import { eventPassesNoteListKindPicker } from '@/lib/feed-kind-filter'
import { collectLocalEventsForTextSearch } from '@/lib/local-nip50-search-merge' import { collectLocalEventsForTextSearch } from '@/lib/local-nip50-search-merge'
import { eventMatchesNip50LocalFullTextQuery } from '@/lib/nip50-local-text-match' import { eventMatchesNip50LocalFullTextQuery } from '@/lib/nip50-local-text-match'
@ -1596,12 +1596,22 @@ const NoteList = forwardRef(
void (async () => { void (async () => {
if (gen !== feedProfileBatchGenRef.current) return if (gen !== feedProfileBatchGenRef.current) return
const contextualReadRelays = Array.from(
new Set(
subRequestsRef.current
.flatMap((r) => r.urls)
.map((u) => normalizeAnyRelayUrl(u) || normalizeUrl(u) || u.trim())
.filter(Boolean)
)
)
const chunks: string[][] = [] const chunks: string[][] = []
for (let i = 0; i < need.length; i += FEED_PROFILE_CHUNK) { for (let i = 0; i < need.length; i += FEED_PROFILE_CHUNK) {
chunks.push(need.slice(i, i + FEED_PROFILE_CHUNK)) chunks.push(need.slice(i, i + FEED_PROFILE_CHUNK))
} }
const settled = await Promise.allSettled( const settled = await Promise.allSettled(
chunks.map((chunk) => client.fetchProfilesForPubkeys(chunk)) chunks.map((chunk) =>
client.fetchProfilesForPubkeys(chunk, { contextualReadRelays })
)
) )
if (gen !== feedProfileBatchGenRef.current) return if (gen !== feedProfileBatchGenRef.current) return

25
src/components/ReplyNoteList/index.tsx

@ -661,6 +661,20 @@ function ReplyNoteList({
return zapsThenTimeSorted(merged, 'desc') return zapsThenTimeSorted(merged, 'desc')
}, [replies, filteredQuoteEvents, showQuotes, sort, replyIdSet, rootInfo, event.kind]) }, [replies, filteredQuoteEvents, showQuotes, sort, replyIdSet, rootInfo, event.kind])
/** Relays that actually delivered thread rows — used to resolve kind-0 when profile mirrors do not replicate them. */
const threadProfileContextRelays = useMemo(() => {
const s = new Set<string>()
const addEv = (e: NEvent) => {
for (const u of client.getSeenEventRelayUrls(e.id)) {
const n = normalizeAnyRelayUrl(u) || u
if (n) s.add(n)
}
}
addEv(event)
for (const e of mergedFeed) addEv(e)
return [...s]
}, [event, mergedFeed])
useEffect(() => { useEffect(() => {
if (!rootInfo) return if (!rootInfo) return
const toAdd = filteredQuoteEvents.filter((evt) => const toAdd = filteredQuoteEvents.filter((evt) =>
@ -745,12 +759,13 @@ function ReplyNoteList({
}) })
void (async () => { void (async () => {
const contextualReadRelays = threadProfileContextRelays
const chunks: string[][] = [] const chunks: string[][] = []
for (let i = 0; i < need.length; i += THREAD_PROFILE_CHUNK) { for (let i = 0; i < need.length; i += THREAD_PROFILE_CHUNK) {
chunks.push(need.slice(i, i + THREAD_PROFILE_CHUNK)) chunks.push(need.slice(i, i + THREAD_PROFILE_CHUNK))
} }
const settled = await Promise.allSettled( const settled = await Promise.allSettled(
chunks.map((chunk) => client.fetchProfilesForPubkeys(chunk)) chunks.map((chunk) => client.fetchProfilesForPubkeys(chunk, { contextualReadRelays }))
) )
if (gen !== threadProfileBatchGenRef.current) return if (gen !== threadProfileBatchGenRef.current) return
@ -788,7 +803,13 @@ function ReplyNoteList({
})() })()
}, THREAD_PROFILE_BATCH_DEBOUNCE_MS) }, THREAD_PROFILE_BATCH_DEBOUNCE_MS)
return () => window.clearTimeout(handle) return () => window.clearTimeout(handle)
}, [event, mergedFeed, parentNoteFeed?.profiles, parentNoteFeed?.pendingPubkeys]) }, [
event,
mergedFeed,
parentNoteFeed?.profiles,
parentNoteFeed?.pendingPubkeys,
threadProfileContextRelays
])
const [timelineKey] = useState<string | undefined>(undefined) const [timelineKey] = useState<string | undefined>(undefined)
const [until, setUntil] = useState<number | undefined>(undefined) const [until, setUntil] = useState<number | undefined>(undefined)

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

@ -14,7 +14,7 @@ import {
import { kinds, nip19 } from 'nostr-tools' import { kinds, nip19 } from 'nostr-tools'
import type { Event as NEvent, Filter } from 'nostr-tools' import type { Event as NEvent, Filter } from 'nostr-tools'
import DataLoader from 'dataloader' import DataLoader from 'dataloader'
import { isWebsocketUrl, normalizeAnyRelayUrl, normalizeHttpUrl, normalizeUrl } from '@/lib/url' import { isHttpRelayUrl, isWebsocketUrl, normalizeAnyRelayUrl, normalizeHttpUrl, normalizeUrl } from '@/lib/url'
import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata' import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata'
import { formatPubkey, pubkeyToNpub, userIdToPubkey } from '@/lib/pubkey' import { formatPubkey, pubkeyToNpub, userIdToPubkey } from '@/lib/pubkey'
import { getPubkeysFromPTags, getServersFromServerTags } from '@/lib/tag' import { getPubkeysFromPTags, getServersFromServerTags } from '@/lib/tag'
@ -1044,13 +1044,18 @@ export class ReplaceableEventService {
/** /**
* Fetch profiles for multiple pubkeys * Fetch profiles for multiple pubkeys
* @param contextualReadRelays Optional relays used for the surrounding feed/thread REQ queried for kind-0
* when default profile mirrors miss (e.g. metadata only on a community relay).
*/ */
async fetchProfilesForPubkeys(pubkeys: string[]): Promise<TProfile[]> { async fetchProfilesForPubkeys(
pubkeys: string[],
options?: { contextualReadRelays?: string[] }
): Promise<TProfile[]> {
const deduped = Array.from(new Set(pubkeys.filter((p) => p && p.length === 64))) const deduped = Array.from(new Set(pubkeys.filter((p) => p && p.length === 64)))
if (deduped.length === 0) return [] if (deduped.length === 0) return []
try { try {
return await racePromiseWithTimeout( return await racePromiseWithTimeout(
this.fetchProfilesForPubkeysBody(deduped), this.fetchProfilesForPubkeysBody(deduped, options),
FEED_PROFILE_BATCH_FETCH_TIMEOUT_MS, FEED_PROFILE_BATCH_FETCH_TIMEOUT_MS,
'fetchProfilesForPubkeys' 'fetchProfilesForPubkeys'
) )
@ -1105,7 +1110,10 @@ export class ReplaceableEventService {
return profiles return profiles
} }
private async fetchProfilesForPubkeysBody(deduped: string[]): Promise<TProfile[]> { private async fetchProfilesForPubkeysBody(
deduped: string[],
options?: { contextualReadRelays?: string[] }
): Promise<TProfile[]> {
let events = await this.fetchReplaceableEventsFromProfileFetchRelays(deduped, kinds.Metadata) let events = await this.fetchReplaceableEventsFromProfileFetchRelays(deduped, kinds.Metadata)
const gapIdx: number[] = [] const gapIdx: number[] = []
for (let i = 0; i < deduped.length; i++) { for (let i = 0; i < deduped.length; i++) {
@ -1158,6 +1166,53 @@ export class ReplaceableEventService {
) )
} }
const stillMissingIdx: number[] = []
for (let i = 0; i < deduped.length; i++) {
if (!events[i]) stillMissingIdx.push(i)
}
if (stillMissingIdx.length > 0 && options?.contextualReadRelays?.length) {
const urls = Array.from(
new Set(
options.contextualReadRelays
.map((u) => normalizeAnyRelayUrl(u) || normalizeUrl(u) || u.trim())
.filter((u): u is string => !!u && !isHttpRelayUrl(u))
)
)
if (urls.length > 0) {
const authors = stillMissingIdx.map((i) => deduped[i]!)
try {
const sanitizedUrls = stripLocalNetworkRelaysForWssReq(urls)
const withAggr = prependAggrNostrLandIfViewerEligible(sanitizedUrls)
if (withAggr.length > 0) {
const evs = await this.queryService.query(
withAggr,
{
kinds: [kinds.Metadata],
authors,
limit: Math.min(Math.max(authors.length * 2, authors.length), 500)
} as Filter,
undefined,
{
firstRelayResultGraceMs: false,
globalTimeout: METADATA_BATCH_QUERY_GLOBAL_TIMEOUT_MS,
eoseTimeout: METADATA_BATCH_QUERY_EOSE_TIMEOUT_MS,
replaceableRace: false
}
)
for (const ev of evs) {
if (ev.kind !== kinds.Metadata || shouldDropEventOnIngest(ev)) continue
const ix = deduped.findIndex((p) => p.toLowerCase() === ev.pubkey.toLowerCase())
if (ix >= 0 && !events[ix]) {
events[ix] = ev
}
}
}
} catch {
/* best-effort */
}
}
}
return this.profilesFromMetadataEvents(deduped, events) return this.profilesFromMetadataEvents(deduped, events)
} }

40
src/services/client.service.ts

@ -98,7 +98,12 @@ function canonicalSeenOnEventId(eventId: string): string {
return /^[0-9a-f]{64}$/i.test(t) ? t.toLowerCase() : t return /^[0-9a-f]{64}$/i.test(t) ? t.toLowerCase() : t
} }
import { shouldDropEventOnIngest, type ShouldDropEventOnIngestOptions } from '@/lib/event-ingest-filter' import { shouldDropEventOnIngest, type ShouldDropEventOnIngestOptions } from '@/lib/event-ingest-filter'
import { getHttpRelayListFromEvent, getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata' import {
getHttpRelayListFromEvent,
getProfileFromEvent,
getRelayListFromEvent,
getRelaySetFromEvent
} from '@/lib/event-metadata'
import logger from '@/lib/logger' import logger from '@/lib/logger'
import { patchPoolRelayAuthRaceAndFeedback } from '@/lib/nostr-relay-auth-patch' import { patchPoolRelayAuthRaceAndFeedback } from '@/lib/nostr-relay-auth-patch'
import { queueRelayAuthSign } from '@/lib/relay-auth-sign-queue' import { queueRelayAuthSign } from '@/lib/relay-auth-sign-queue'
@ -3463,15 +3468,41 @@ class ClientService extends EventTarget {
if (!favoriteRelaysEvent) return [] if (!favoriteRelaysEvent) return []
const relays: string[] = [] const relays: string[] = []
const relaySetIds: string[] = []
favoriteRelaysEvent.tags.forEach(([tagName, tagValue]) => { favoriteRelaysEvent.tags.forEach(([tagName, tagValue]) => {
if (tagName === 'relay' && tagValue) { if (tagName === 'relay' && tagValue) {
const normalized = normalizeUrl(tagValue) const normalized = normalizeUrl(tagValue)
if (normalized) { if (normalized) {
relays.push(normalized) relays.push(normalized)
} }
} else if (tagName === 'a' && tagValue) {
const [kindStr, author, d] = tagValue.split(':')
if (
kindStr === String(kinds.Relaysets) &&
author === pubkey &&
d &&
!relaySetIds.includes(d)
) {
relaySetIds.push(d)
}
} }
}) })
// NIP-51 relay sets on kind 10012: same expansion as {@link FavoriteRelaysProvider} (not only `relay` tags).
for (const id of relaySetIds) {
try {
const ev = await indexedDb.getReplaceableEvent(pubkey, kinds.Relaysets, id)
if (!ev || shouldDropEventOnIngest(ev)) continue
const set = getRelaySetFromEvent(ev)
for (const u of set.relayUrls) {
const n = normalizeUrl(u) || normalizeAnyRelayUrl(u)
if (n && !relays.includes(n)) relays.push(n)
}
} catch {
/* ignore */
}
}
return Array.from(new Set(relays)) return Array.from(new Set(relays))
} catch { } catch {
return [] return []
@ -4044,8 +4075,11 @@ class ClientService extends EventTarget {
return this.replaceableEventService.fetchProfile(id, skipCache) return this.replaceableEventService.fetchProfile(id, skipCache)
} }
async fetchProfilesForPubkeys(pubkeys: string[]): Promise<TProfile[]> { async fetchProfilesForPubkeys(
return this.replaceableEventService.fetchProfilesForPubkeys(pubkeys) pubkeys: string[],
options?: { contextualReadRelays?: string[] }
): Promise<TProfile[]> {
return this.replaceableEventService.fetchProfilesForPubkeys(pubkeys, options)
} }
async getProfileFromIndexedDB(id: string): Promise<TProfile | undefined> { async getProfileFromIndexedDB(id: string): Promise<TProfile | undefined> {

Loading…
Cancel
Save