diff --git a/src/components/Profile/ProfileMediaFeed.tsx b/src/components/Profile/ProfileMediaFeed.tsx index 5ce19d80..3856f342 100644 --- a/src/components/Profile/ProfileMediaFeed.tsx +++ b/src/components/Profile/ProfileMediaFeed.tsx @@ -1,5 +1,5 @@ import NoteList, { type TNoteListRef } from '@/components/NoteList' -import { buildProfilePageReadRelayUrls } from '@/lib/favorites-feed-relays' +import { buildAuthorInboxOutboxRelayUrls } from '@/lib/favorites-feed-relays' import logger from '@/lib/logger' import { normalizeHexPubkey } from '@/lib/pubkey' import { computeSpellSubRequestsIdentityKey } from '@/lib/spell-feed-request-identity' @@ -10,82 +10,67 @@ import client from '@/services/client.service' import { forwardRef, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -function relayListsContentKey(favoriteRelays: string[], blockedRelays: string[]): string { - const fav = [...favoriteRelays].map((u) => normalizeUrl(u) || u).filter(Boolean).sort().join('\u0001') - const blk = [...blockedRelays].map((u) => normalizeUrl(u) || u).filter(Boolean).sort().join('\u0001') - return `${fav}\u0000${blk}` +function blockedRelaysContentKey(blockedRelays: string[]): string { + return [...blockedRelays].map((u) => normalizeUrl(u) || u).filter(Boolean).sort().join('\u0001') } const MEDIA_LOG = '[ProfileMedia]' const ProfileMediaFeed = forwardRef(({ pubkey }, ref) => { const { t } = useTranslation() - const { favoriteRelays, blockedRelays } = useFavoriteRelays() - const relayListsKey = useMemo( - () => relayListsContentKey(favoriteRelays, blockedRelays), - [favoriteRelays, blockedRelays] - ) + const { blockedRelays } = useFavoriteRelays() + const blockedKey = useMemo(() => blockedRelaysContentKey(blockedRelays), [blockedRelays]) /** - * Start REQ immediately with the same stack as “no NIP-65 yet” (favorites + fast-read), then refine when - * {@link client.fetchRelayList} returns — avoids an empty/skeleton Medien tab while Posts already shows cache. + * Before NIP-65: empty author tier so REQ still uses read-only + fast-read; refine when + * {@link client.fetchRelayList} returns. */ - const provisionalProfileRelayUrls = useMemo(() => { + const provisionalAuthorRelayUrls = useMemo(() => { if (!pubkey?.trim()) return [] as string[] - return buildProfilePageReadRelayUrls( - favoriteRelays, - blockedRelays, - { read: [] as string[], write: [] as string[] }, - false - ) - }, [pubkey, relayListsKey, favoriteRelays, blockedRelays]) + return buildAuthorInboxOutboxRelayUrls({ read: [], write: [] }, blockedRelays) + }, [pubkey, blockedKey, blockedRelays]) - const [refinedProfileRelayUrls, setRefinedProfileRelayUrls] = useState(null) + const [refinedAuthorRelayUrls, setRefinedAuthorRelayUrls] = useState(null) useEffect(() => { const pk = pubkey?.trim() if (!pk) { logger.debug(`${MEDIA_LOG} empty pubkey — no relay resolution`) - setRefinedProfileRelayUrls([]) + setRefinedAuthorRelayUrls([]) return } let cancelled = false - setRefinedProfileRelayUrls(null) + setRefinedAuthorRelayUrls(null) void (async () => { const authorRl = await client.fetchRelayList(pk).catch(() => ({ read: [] as string[], write: [] as string[] })) if (cancelled) return - const profileStack = buildProfilePageReadRelayUrls( - favoriteRelays, - blockedRelays, - authorRl, - false - ) + const authorStack = buildAuthorInboxOutboxRelayUrls(authorRl, blockedRelays) const hexPk = normalizeHexPubkey(pk) - logger.debug(`${MEDIA_LOG} NIP-65 stack resolved for media tab`, { + logger.debug(`${MEDIA_LOG} NIP-65 author relays resolved for media tab`, { pubkey: hexPk.slice(0, 8), authorReadCount: authorRl.read?.length ?? 0, authorWriteCount: authorRl.write?.length ?? 0, - profileRelayCount: profileStack.length, - profileRelaysSample: profileStack.slice(0, 4) + authorRelayCount: authorStack.length, + authorRelaysSample: authorStack.slice(0, 4) }) - logger.debug(`${MEDIA_LOG} full profile relay stack`, { profileRelays: profileStack }) - setRefinedProfileRelayUrls(profileStack) + logger.debug(`${MEDIA_LOG} author inbox/outbox relay list`, { authorRelays: authorStack }) + setRefinedAuthorRelayUrls(authorStack) })() return () => { cancelled = true } - }, [pubkey, relayListsKey, favoriteRelays, blockedRelays]) + }, [pubkey, blockedKey, blockedRelays]) - const profileRelayUrls = refinedProfileRelayUrls ?? provisionalProfileRelayUrls + const authorRelayUrls = refinedAuthorRelayUrls ?? provisionalAuthorRelayUrls const subRequests = useMemo(() => { const pk = pubkey?.trim() if (!pk) return [] - return buildProfileMediaSubRequests(profileRelayUrls, blockedRelays, pk) - }, [pubkey, profileRelayUrls, blockedRelays]) + return buildProfileMediaSubRequests(authorRelayUrls, blockedRelays, pk) + }, [pubkey, authorRelayUrls, blockedRelays]) const feedSubscriptionKey = useMemo( () => computeSpellSubRequestsIdentityKey(subRequests), @@ -98,7 +83,7 @@ const ProfileMediaFeed = forwardRef(({ pubkey if (!subRequests.length) { logger.debug(`${MEDIA_LOG} buildProfileMediaSubRequests returned no URLs (blocked or empty stacks)`, { pubkey: normalizeHexPubkey(pk).slice(0, 8), - profileRelayCount: profileRelayUrls.length + authorRelayCount: authorRelayUrls.length }) return } @@ -112,7 +97,7 @@ const ProfileMediaFeed = forwardRef(({ pubkey filterLimit: sr.filter.limit }) logger.debug(`${MEDIA_LOG} augmented relay URLs`, { urls: sr.urls }) - }, [pubkey, profileRelayUrls, subRequests, feedSubscriptionKey, refinedProfileRelayUrls]) + }, [pubkey, authorRelayUrls, subRequests, feedSubscriptionKey, refinedAuthorRelayUrls]) const showKinds = useMemo(() => [...PROFILE_MEDIA_TAB_KINDS], []) @@ -141,8 +126,7 @@ const ProfileMediaFeed = forwardRef(({ pubkey showKinds={showKinds} useFilterAsIs /** - * Provisional relay stack (favorites + fast read) then NIP-65 refinement changes URLs without changing the - * REQ filter — merge so we do not wipe rows or re-enter a long loading state. + * Provisional author tier (empty) then NIP-65 inbox/outbox refinement; REQ filter unchanged — merge rows. */ preserveTimelineOnSubRequestsChange mergeTimelineWhenSubRequestFiltersMatch diff --git a/src/hooks/useProfilePins.tsx b/src/hooks/useProfilePins.tsx index 2b71b40c..fb5ba45f 100644 --- a/src/hooks/useProfilePins.tsx +++ b/src/hooks/useProfilePins.tsx @@ -1,7 +1,7 @@ import { Event } from 'nostr-tools' import { + buildAuthorInboxOutboxRelayUrls, buildProfileAugmentedReadRelayUrls, - buildProfilePageReadRelayUrls, PROFILE_PAGE_PINS_RESOLVE_LIMIT } from '@/lib/favorites-feed-relays' import { @@ -70,18 +70,13 @@ function orderPinEvents(pinList: Event, eventsById: Map): Event[] return ordered } -function relayListsContentKey(favoriteRelays: string[], blockedRelays: string[]): string { - const fav = [...favoriteRelays].map((u) => normalizeUrl(u) || u).filter(Boolean).sort().join('\u0001') - const blk = [...blockedRelays].map((u) => normalizeUrl(u) || u).filter(Boolean).sort().join('\u0001') - return `${fav}\u0000${blk}` +function blockedRelaysContentKey(blockedRelays: string[]): string { + return [...blockedRelays].map((u) => normalizeUrl(u) || u).filter(Boolean).sort().join('\u0001') } export function useProfilePins(pubkey: string | undefined) { - const { favoriteRelays, blockedRelays } = useFavoriteRelays() - const relayListsKey = useMemo( - () => relayListsContentKey(favoriteRelays, blockedRelays), - [favoriteRelays, blockedRelays] - ) + const { blockedRelays } = useFavoriteRelays() + const blockedKey = useMemo(() => blockedRelaysContentKey(blockedRelays), [blockedRelays]) const [pinEvents, setPinEvents] = useState([]) const [loadingPins, setLoadingPins] = useState(false) @@ -137,15 +132,8 @@ export function useProfilePins(pubkey: string | undefined) { })), client.fetchPinListEvent(pk).catch(() => undefined) ]) - // Same stack as profile feed: viewed npub NIP-65 read+write → your favorites → FAST_READ_RELAY_URLS, - // deduped, blocked stripped, max PROFILE_PAGE_FEED_MAX_RELAYS (6). Relays here accept `#d` on REQ. - const profileRelays = buildProfilePageReadRelayUrls( - favoriteRelays, - blockedRelays, - authorRl, - false - ) - const pinsResolveRelays = buildProfileAugmentedReadRelayUrls(profileRelays, blockedRelays) + const authorRelays = buildAuthorInboxOutboxRelayUrls(authorRl, blockedRelays) + const pinsResolveRelays = buildProfileAugmentedReadRelayUrls(authorRelays, blockedRelays) if (!pinsResolveRelays.length) { setPinEvents([]) return @@ -249,7 +237,7 @@ export function useProfilePins(pubkey: string | undefined) { setLoadingPins(false) } }, - [pubkey, relayListsKey, favoriteRelays, blockedRelays] + [pubkey, blockedKey, blockedRelays] ) useEffect(() => { diff --git a/src/lib/favorites-feed-relays.ts b/src/lib/favorites-feed-relays.ts index 7d0173f0..e520a194 100644 --- a/src/lib/favorites-feed-relays.ts +++ b/src/lib/favorites-feed-relays.ts @@ -68,19 +68,32 @@ export function mergeRelayUrlLayers(layers: string[][], blockedRelays: string[]) } /** - * Profile pins + media: prepend {@link READ_ONLY_RELAY_URLS} and {@link FAST_READ_RELAY_URLS} to the - * capped NIP-65 stack so REQ still hits aggregators when the author’s six relays fill the profile cap alone. + * Viewed author’s NIP-65 read list (inboxes), then write list (outboxes), each with LAN/local URLs first; blocked + * stripped. Used for profile pins + Medien before {@link buildProfileAugmentedReadRelayUrls}. + */ +export function buildAuthorInboxOutboxRelayUrls( + authorRelayList: { read: string[]; write: string[] }, + blockedRelays: string[] +): string[] { + const inboxLayer = relayUrlsLocalsFirst(authorRelayList.read ?? []) + const outboxLayer = relayUrlsLocalsFirst(authorRelayList.write ?? []) + return mergeRelayUrlLayers([inboxLayer, outboxLayer], blockedRelays) +} + +/** + * Profile pins + Medien: author NIP-65 tier (pass from {@link buildAuthorInboxOutboxRelayUrls}), then + * {@link READ_ONLY_RELAY_URLS}, then {@link FAST_READ_RELAY_URLS}; dedupe, blocked-stripped, capped. */ export const PROFILE_AUGMENTED_READ_MAX_RELAYS = 16 export function buildProfileAugmentedReadRelayUrls( - profileRelayUrls: string[], + authorRelayUrls: string[], blockedRelays: string[], maxRelays: number = PROFILE_AUGMENTED_READ_MAX_RELAYS ): string[] { const readOnlyLayer = READ_ONLY_RELAY_URLS.map((u) => normalizeUrl(u) || u).filter(Boolean) const fastReadLayer = FAST_READ_RELAY_URLS.map((u) => normalizeUrl(u) || u).filter(Boolean) - const merged = mergeRelayUrlLayers([readOnlyLayer, fastReadLayer, profileRelayUrls], blockedRelays) + const merged = mergeRelayUrlLayers([authorRelayUrls, readOnlyLayer, fastReadLayer], blockedRelays) return merged.slice(0, maxRelays) } diff --git a/src/pages/primary/SpellsPage/fauxSpellFeeds.ts b/src/pages/primary/SpellsPage/fauxSpellFeeds.ts index af4a7236..02f56a35 100644 --- a/src/pages/primary/SpellsPage/fauxSpellFeeds.ts +++ b/src/pages/primary/SpellsPage/fauxSpellFeeds.ts @@ -8,7 +8,7 @@ * topics go in a single `#t` filter (NIP-01 OR semantics). The notifications spell uses a narrow * kind list vs full profile kinds. */ -import { ExtendedKind, FAST_READ_RELAY_URLS, PROFILE_FEED_KINDS, READ_ONLY_RELAY_URLS } from '@/constants' +import { ExtendedKind, PROFILE_FEED_KINDS, READ_ONLY_RELAY_URLS } from '@/constants' import { buildProfileAugmentedReadRelayUrls } from '@/lib/favorites-feed-relays' import { normalizeTopic } from '@/lib/discussion-topics' import { userIdToPubkey } from '@/lib/pubkey' @@ -23,8 +23,8 @@ export const FAUX_SPELL_EVENT_LIMIT = 200 /** Profile Media tab: single REQ `limit` (matches merged cap in NoteList one-shot). */ export const PROFILE_MEDIA_REQ_LIMIT = 200 -/** Max relay URLs per Medien REQ after read-only + fast-read layers (see {@link buildProfileMediaSubRequests}). */ -export const PROFILE_MEDIA_MAX_RELAYS = 10 +/** Max relay URLs per Medien REQ (author stack + aggregators; see {@link buildProfileMediaSubRequests}). */ +export const PROFILE_MEDIA_MAX_RELAYS = 16 /** * Trim relay lists and filter limits (and bookmark `ids`) so faux feeds stay cheap to open. @@ -134,13 +134,16 @@ export function buildProfileMediaSpellFilter(pubkey: string): Filter { } } -/** Read-only + {@link FAST_READ_RELAY_URLS} before the author-only base stack; capped at {@link PROFILE_MEDIA_MAX_RELAYS}. */ +/** + * Author inboxes/outboxes + read-only + fast read (see {@link buildProfileAugmentedReadRelayUrls}), capped at + * {@link PROFILE_MEDIA_MAX_RELAYS}. + */ export function buildProfileMediaSubRequests( - profileRelayUrls: string[], + authorRelayUrls: string[], blockedRelays: string[], pubkey: string ): TFeedSubRequest[] { - const urls = buildProfileAugmentedReadRelayUrls(profileRelayUrls, blockedRelays, PROFILE_MEDIA_MAX_RELAYS) + const urls = buildProfileAugmentedReadRelayUrls(authorRelayUrls, blockedRelays, PROFILE_MEDIA_MAX_RELAYS) if (!urls.length) return [] return [{ urls, filter: buildProfileMediaSpellFilter(pubkey) }] }