diff --git a/src/components/NoteList/index.tsx b/src/components/NoteList/index.tsx index 63bd515c..026a34c9 100644 --- a/src/components/NoteList/index.tsx +++ b/src/components/NoteList/index.tsx @@ -2054,8 +2054,15 @@ const NoteList = forwardRef( if (oneShotFetch || mappedSubRequests.length === 0) return if (isSpellPageLocalWarmup) return const diskReq = mappedSubRequests as Array<{ urls: string[]; filter: TSubRequestFilter }> + const strictSingleRelayShard = + mappedSubRequests.length === 1 && + mappedSubRequests[0]!.urls.length === 1 && + (hostPrimaryPageNameRef.current === 'relay' || + (allowKindlessRelayExploreRef.current && useFilterAsIsRef.current)) void client - .getLocalFeedEvents(diskReq) + .getLocalFeedEvents(diskReq, { + strictRelayShardSourcesOnly: strictSingleRelayShard + }) .then((diskRaw) => { if (!effectActive || timelineEffectStale()) return const diskNarrowed = narrowLiveBatch(diskRaw) @@ -2429,8 +2436,15 @@ const NoteList = forwardRef( urls: string[] filter: TSubRequestFilter }> + const strictSingleRelayShardOneShot = + mappedSubRequests.length === 1 && + mappedSubRequests[0]!.urls.length === 1 && + (hostPrimaryPageNameRef.current === 'relay' || + (allowKindlessRelayExploreRef.current && useFilterAsIsRef.current)) void client - .getLocalFeedEvents(diskReqOneShot) + .getLocalFeedEvents(diskReqOneShot, { + strictRelayShardSourcesOnly: strictSingleRelayShardOneShot + }) .then((diskRaw) => { if (!effectActive || timelineEffectStale()) return if (diskRaw.length === 0) return diff --git a/src/components/Relay/index.tsx b/src/components/Relay/index.tsx index ef53220d..79c96541 100644 --- a/src/components/Relay/index.tsx +++ b/src/components/Relay/index.tsx @@ -7,8 +7,10 @@ import type { TPrimaryPageName } from '@/PageManager' import { SINGLE_RELAY_KINDLESS_REQ_LIMIT } from '@/constants' import { normalizeAnyRelayUrl } from '@/lib/url' import { useCurrentRelays } from '@/providers/CurrentRelaysProvider' +import client from '@/services/client.service' import type { TFeedSubRequest } from '@/types' -import { forwardRef, useEffect, useMemo, useRef, useState } from 'react' +import type { Event } from 'nostr-tools' +import { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import NotFound from '../NotFound' @@ -77,6 +79,21 @@ const Relay = forwardRef< ] }, [normalizedUrl, debouncedInput]) + /** When we know delivery relays, drop rows that never arrived from this feed’s relay (stale cache / mis-tagged). */ + const relaySeenMatchKey = useMemo( + () => (normalizedUrl ? (normalizeAnyRelayUrl(normalizedUrl) || normalizedUrl).toLowerCase() : ''), + [normalizedUrl] + ) + const shouldHideEventNotFromThisRelay = useCallback( + (ev: Event) => { + if (!relaySeenMatchKey) return false + const seen = client.getSeenEventRelayUrls(ev.id) + if (seen.length === 0) return false + return !seen.some((u) => (normalizeAnyRelayUrl(u) || u).toLowerCase() === relaySeenMatchKey) + }, + [relaySeenMatchKey] + ) + if (!normalizedUrl) { return } @@ -101,6 +118,8 @@ const Relay = forwardRef< showAllKinds showFeedClientFilter hostPrimaryPageName={hostPrimaryPageName} + extraShouldHideEvent={shouldHideEventNotFromThisRelay} + extraShouldHideRepliesEvent={shouldHideEventNotFromThisRelay} /> ) diff --git a/src/components/Settings/SettingsMenuBody.tsx b/src/components/Settings/SettingsMenuBody.tsx index 582458a0..851c1ca9 100644 --- a/src/components/Settings/SettingsMenuBody.tsx +++ b/src/components/Settings/SettingsMenuBody.tsx @@ -144,10 +144,6 @@ export default function SettingsMenuBody({ className }: { className?: string }) -
-
Imwald
-
Im Wald
-
) } diff --git a/src/constants.ts b/src/constants.ts index 9014620b..fea18c64 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -73,7 +73,7 @@ export const DESKTOP_APP_DOWNLOAD_URL_DEFAULT = export const DEFAULT_FAVORITE_RELAYS = [ 'wss://theforest.nostr1.com', - 'wss://orly-relay.imwald.eu', + 'wss://christpill.nostr1.com', 'wss://nostr.land', 'wss://nostr21.com' ] diff --git a/src/services/client.service.ts b/src/services/client.service.ts index 30e741ea..d45757ce 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -1895,7 +1895,16 @@ class ClientService extends EventTarget { async getLocalFeedEvents( subRequests: { urls: string[]; filter: TSubRequestFilter }[], - options?: { maxRowsScanned?: number; maxMatches?: number } + options?: { + maxRowsScanned?: number + maxMatches?: number + /** + * When true, only load rows persisted for this feed’s timeline shard(s) — no global session scan or + * archive/publication sweeps. Used for single-relay explore so kindless `{ limit }` does not match every + * cached note while the UI is scoped to one relay. + */ + strictRelayShardSourcesOnly?: boolean + } ): Promise { if (!subRequests.length) return [] const filters = subRequests.map(({ filter }) => filter as Filter) @@ -1921,6 +1930,14 @@ class ClientService extends EventTarget { } } + if (options?.strictRelayShardSourcesOnly) { + const timelineRows = await this.getTimelineDiskSnapshotEvents(subRequests).catch(() => [] as NEvent[]) + add(timelineRows) + return [...byId.values()] + .sort((a, b) => b.created_at - a.created_at || b.id.localeCompare(a.id)) + .slice(0, maxMatches) + } + add(this.eventService.getSessionEventsMatchingFilters(filters, maxMatches)) const [timelineRows, archiveRows, publicationRows] = await Promise.all([ @@ -3285,6 +3302,7 @@ class ClientService extends EventTarget { }) let relayListResolved = 0 let contactsResolved = 0 + let profileResolved = 0 const chunkSize = 20 for (let i = 0; i * chunkSize < followings.length; i++) { if (signal.aborted) { @@ -3292,20 +3310,24 @@ class ClientService extends EventTarget { return } const chunk = followings.slice(i * chunkSize, (i + 1) * chunkSize) - const [relayListEvents, contactsEvents] = await Promise.all([ + const [relayListEvents, contactsEvents, metadataEvents] = await Promise.all([ this.replaceableEventService.fetchReplaceableEventsFromProfileFetchRelays(chunk, kinds.RelayList), this.replaceableEventService.fetchReplaceableEventsFromProfileFetchRelays(chunk, kinds.Contacts), - Promise.all(chunk.map((pk) => this.fetchProfileEvent(pk))) + this.replaceableEventService.fetchReplaceableEventsFromProfileFetchRelays(chunk, kinds.Metadata) ]) relayListResolved += relayListEvents.filter(Boolean).length contactsResolved += contactsEvents.filter(Boolean).length - await new Promise((resolve) => setTimeout(resolve, 1000)) + const profiles = metadataEvents.filter((ev): ev is NEvent => Boolean(ev)) + profileResolved += profiles.length + await Promise.allSettled(profiles.map((ev) => this.addUsernameToIndex(ev))) + profiles.forEach((ev) => this.updateProfileEventCache(ev)) } logger.info('[client] Prewarm: following profile + contacts + NIP-65 fetch finished', { pubkeySlice: pubkey.slice(0, 12), followingCount: followings.length, relayListEventsResolved: relayListResolved, - contactsEventsResolved: contactsResolved + contactsEventsResolved: contactsResolved, + profileEventsResolved: profileResolved }) }