Browse Source

bug-fix

imwald
Silberengel 2 weeks ago
parent
commit
74e1d4d77f
  1. 13
      src/components/Profile/ProfileFeedWithPins.tsx
  2. 22
      src/components/Profile/ProfileMediaFeed.tsx
  3. 18
      src/hooks/useProfilePins.tsx
  4. 20
      src/hooks/useProfileTimeline.tsx
  5. 32
      src/lib/favorites-feed-relays.ts
  6. 29
      src/lib/relay-list-sanitize.ts
  7. 16
      src/lib/relay-url-priority.test.ts
  8. 3
      src/services/media-upload.service.ts

13
src/components/Profile/ProfileFeedWithPins.tsx

@ -221,10 +221,12 @@ const ProfileFeedWithPins = forwardRef<{ refresh: () => void }, { pubkey: string @@ -221,10 +221,12 @@ const ProfileFeedWithPins = forwardRef<{ refresh: () => void }, { pubkey: string
return () => observer.disconnect()
}, [totalVisible, mergedDisplay.length])
const loading =
(loadingPins || loadingTimeline || loadingZapPollVotes) && mergedDisplay.length === 0
// Pins and zap-poll votes can take longer than the timeline; do not block the whole tab on them.
// Show posts as soon as the timeline has delivered anything (or finished empty).
const showFullSkeleton =
mergedDisplay.length === 0 && loadingTimeline && timelineEvents.length === 0
if (loading) {
if (showFullSkeleton) {
return (
<div className="mt-4 space-y-2 px-1">
<div className="flex flex-wrap items-center gap-2 px-2">
@ -301,6 +303,11 @@ const ProfileFeedWithPins = forwardRef<{ refresh: () => void }, { pubkey: string @@ -301,6 +303,11 @@ const ProfileFeedWithPins = forwardRef<{ refresh: () => void }, { pubkey: string
))}
</div>
)}
{mergedDisplay.length === 0 && (loadingPins || loadingZapPollVotes) && (
<div className="flex justify-center py-6 text-sm text-muted-foreground" role="status" aria-live="polite">
{t('Loading…')}
</div>
)}
{displayedPins.length > 0 && displayedFeed.length > 0 && (
<div className="text-xs text-muted-foreground px-2 py-1 border-t border-border/60 mt-2 pt-2">
{t('Feed')}

22
src/components/Profile/ProfileMediaFeed.tsx

@ -1,12 +1,13 @@ @@ -1,12 +1,13 @@
import NoteList, { type TNoteListRef } from '@/components/NoteList'
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'
import { PROFILE_MEDIA_TAB_KINDS } from '@/constants'
import { buildProfileMediaSubRequests } from '@/pages/primary/SpellsPage/fauxSpellFeeds'
import { normalizeUrl } from '@/lib/url'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useNostrOptional } from '@/providers/nostr-context'
import { hexPubkeysEqual, normalizeHexPubkey } from '@/lib/pubkey'
import client from '@/services/client.service'
import { forwardRef, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -19,8 +20,19 @@ const MEDIA_LOG = '[ProfileMedia]' @@ -19,8 +20,19 @@ const MEDIA_LOG = '[ProfileMedia]'
const ProfileMediaFeed = forwardRef<TNoteListRef, { pubkey: string }>(({ pubkey }, ref) => {
const { t } = useTranslation()
const nostr = useNostrOptional()
const { blockedRelays } = useFavoriteRelays()
const blockedKey = useMemo(() => blockedRelaysContentKey(blockedRelays), [blockedRelays])
const includeAuthorLocalRelays = useMemo(() => {
const me = nostr?.pubkey?.trim()
const pk = pubkey?.trim()
if (!me || !pk) return false
try {
return hexPubkeysEqual(normalizeHexPubkey(me), normalizeHexPubkey(pk))
} catch {
return false
}
}, [nostr?.pubkey, pubkey])
/**
* Before NIP-65: empty author tier so REQ still uses read-only + fast-read; refine when
@ -28,8 +40,8 @@ const ProfileMediaFeed = forwardRef<TNoteListRef, { pubkey: string }>(({ pubkey @@ -28,8 +40,8 @@ const ProfileMediaFeed = forwardRef<TNoteListRef, { pubkey: string }>(({ pubkey
*/
const provisionalAuthorRelayUrls = useMemo(() => {
if (!pubkey?.trim()) return [] as string[]
return buildAuthorInboxOutboxRelayUrls({ read: [], write: [] }, blockedRelays)
}, [pubkey, blockedKey, blockedRelays])
return buildAuthorInboxOutboxRelayUrls({ read: [], write: [] }, blockedRelays, includeAuthorLocalRelays)
}, [pubkey, blockedKey, blockedRelays, includeAuthorLocalRelays])
const [refinedAuthorRelayUrls, setRefinedAuthorRelayUrls] = useState<string[] | null>(null)
@ -48,7 +60,7 @@ const ProfileMediaFeed = forwardRef<TNoteListRef, { pubkey: string }>(({ pubkey @@ -48,7 +60,7 @@ const ProfileMediaFeed = forwardRef<TNoteListRef, { pubkey: string }>(({ pubkey
write: [] as string[]
}))
if (cancelled) return
const authorStack = buildAuthorInboxOutboxRelayUrls(authorRl, blockedRelays)
const authorStack = buildAuthorInboxOutboxRelayUrls(authorRl, blockedRelays, includeAuthorLocalRelays)
const hexPk = normalizeHexPubkey(pk)
logger.debug(`${MEDIA_LOG} NIP-65 author relays resolved for media tab`, {
pubkey: hexPk.slice(0, 8),
@ -63,7 +75,7 @@ const ProfileMediaFeed = forwardRef<TNoteListRef, { pubkey: string }>(({ pubkey @@ -63,7 +75,7 @@ const ProfileMediaFeed = forwardRef<TNoteListRef, { pubkey: string }>(({ pubkey
return () => {
cancelled = true
}
}, [pubkey, blockedKey, blockedRelays])
}, [pubkey, blockedKey, blockedRelays, includeAuthorLocalRelays])
const authorRelayUrls = refinedAuthorRelayUrls ?? provisionalAuthorRelayUrls

18
src/hooks/useProfilePins.tsx

@ -8,9 +8,10 @@ import { @@ -8,9 +8,10 @@ import {
METADATA_BATCH_QUERY_EOSE_TIMEOUT_MS,
METADATA_BATCH_QUERY_GLOBAL_TIMEOUT_MS
} from '@/constants'
import { normalizeHexPubkey } from '@/lib/pubkey'
import { hexPubkeysEqual, normalizeHexPubkey } from '@/lib/pubkey'
import { normalizeUrl } from '@/lib/url'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useNostrOptional } from '@/providers/nostr-context'
import client, { eventService, queryService } from '@/services/client.service'
import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react'
@ -75,8 +76,19 @@ function blockedRelaysContentKey(blockedRelays: string[]): string { @@ -75,8 +76,19 @@ function blockedRelaysContentKey(blockedRelays: string[]): string {
}
export function useProfilePins(pubkey: string | undefined) {
const nostr = useNostrOptional()
const { blockedRelays } = useFavoriteRelays()
const blockedKey = useMemo(() => blockedRelaysContentKey(blockedRelays), [blockedRelays])
const includeAuthorLocalRelays = useMemo(() => {
const me = nostr?.pubkey?.trim()
const pk = pubkey?.trim()
if (!me || !pk) return false
try {
return hexPubkeysEqual(normalizeHexPubkey(me), normalizeHexPubkey(pk))
} catch {
return false
}
}, [nostr?.pubkey, pubkey])
const [pinEvents, setPinEvents] = useState<Event[]>([])
const [loadingPins, setLoadingPins] = useState(false)
@ -132,7 +144,7 @@ export function useProfilePins(pubkey: string | undefined) { @@ -132,7 +144,7 @@ export function useProfilePins(pubkey: string | undefined) {
})),
client.fetchPinListEvent(pk).catch(() => undefined)
])
const authorRelays = buildAuthorInboxOutboxRelayUrls(authorRl, blockedRelays)
const authorRelays = buildAuthorInboxOutboxRelayUrls(authorRl, blockedRelays, includeAuthorLocalRelays)
const pinsResolveRelays = buildProfileAugmentedReadRelayUrls(authorRelays, blockedRelays)
if (!pinsResolveRelays.length) {
setPinEvents([])
@ -237,7 +249,7 @@ export function useProfilePins(pubkey: string | undefined) { @@ -237,7 +249,7 @@ export function useProfilePins(pubkey: string | undefined) {
setLoadingPins(false)
}
},
[pubkey, blockedKey, blockedRelays]
[pubkey, blockedKey, blockedRelays, includeAuthorLocalRelays]
)
useEffect(() => {

20
src/hooks/useProfileTimeline.tsx

@ -4,8 +4,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' @@ -4,8 +4,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Event } from 'nostr-tools'
import { CALENDAR_EVENT_KINDS, ExtendedKind, isSocialKindBlockedKind } from '@/constants'
import { buildProfilePageReadRelayUrls } from '@/lib/favorites-feed-relays'
import { hexPubkeysEqual, normalizeHexPubkey } from '@/lib/pubkey'
import { normalizeAnyRelayUrl, subtractNormalizedRelayUrls } from '@/lib/url'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useNostrOptional } from '@/providers/nostr-context'
type ProfileTimelineMemoryEntry = {
events: Event[]
@ -124,7 +126,17 @@ export function useProfileTimeline({ @@ -124,7 +126,17 @@ export function useProfileTimeline({
limit = 200,
filterPredicate
}: UseProfileTimelineOptions): UseProfileTimelineResult {
const nostr = useNostrOptional()
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const includeAuthorLocalRelays = useMemo(() => {
const me = nostr?.pubkey?.trim()
if (!me) return false
try {
return hexPubkeysEqual(normalizeHexPubkey(me), normalizeHexPubkey(pubkey))
} catch {
return false
}
}, [nostr?.pubkey, pubkey])
const relayListsKey = useMemo(
() => relayListsContentKey(favoriteRelays, blockedRelays),
[favoriteRelays, blockedRelays]
@ -208,7 +220,8 @@ export function useProfileTimeline({ @@ -208,7 +220,8 @@ export function useProfileTimeline({
favoriteRelays,
blockedRelays,
emptyAuthor,
socialKinds
socialKinds,
includeAuthorLocalRelays
)
const startWave = async (subRequests: ReturnType<typeof buildSubRequests>) => {
@ -259,7 +272,8 @@ export function useProfileTimeline({ @@ -259,7 +272,8 @@ export function useProfileTimeline({
favoriteRelays,
blockedRelays,
authorRl,
socialKinds
socialKinds,
includeAuthorLocalRelays
)
const deltaUrls = subtractNormalizedRelayUrls(fullFeedUrls, provisionalFeedUrls)
if (cancelled || deltaUrls.length === 0) return
@ -274,7 +288,7 @@ export function useProfileTimeline({ @@ -274,7 +288,7 @@ export function useProfileTimeline({
subscriptionRef.current()
subscriptionRef.current = () => {}
}
}, [pubkey, cacheKey, JSON.stringify(kinds), limit, refreshToken, relayListsKey])
}, [pubkey, cacheKey, JSON.stringify(kinds), limit, refreshToken, relayListsKey, includeAuthorLocalRelays])
const refresh = useCallback(() => {
subscriptionRef.current()

32
src/lib/favorites-feed-relays.ts

@ -14,6 +14,7 @@ import { @@ -14,6 +14,7 @@ import {
mergeRelayPriorityLayers,
relayUrlsLocalsFirst
} from '@/lib/relay-url-priority'
import { stripMailboxLocalUrlsForRemoteViewers } from '@/lib/relay-list-sanitize'
const blockedSet = (blockedRelays: string[]) =>
new Set(blockedRelays.map((b) => normalizeAnyRelayUrl(b) || b))
@ -77,19 +78,19 @@ export function mergeRelayUrlLayers(layers: string[][], blockedRelays: string[]) @@ -77,19 +78,19 @@ export function mergeRelayUrlLayers(layers: string[][], blockedRelays: string[])
/**
* Viewed authors 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}.
*
* @param includeAuthorLocalRelays When true (viewing your own profile), keep LAN hints so local cache/outbox works.
*/
export function buildAuthorInboxOutboxRelayUrls(
authorRelayList: { read: string[]; write: string[]; httpRead?: string[]; httpWrite?: string[] },
blockedRelays: string[]
blockedRelays: string[],
includeAuthorLocalRelays = false
): string[] {
const inboxLayer = relayUrlsLocalsFirst([
...(authorRelayList.httpRead ?? []),
...(authorRelayList.read ?? [])
])
const outboxLayer = relayUrlsLocalsFirst([
...(authorRelayList.httpWrite ?? []),
...(authorRelayList.write ?? [])
])
const list = includeAuthorLocalRelays
? authorRelayList
: stripMailboxLocalUrlsForRemoteViewers(authorRelayList)
const inboxLayer = relayUrlsLocalsFirst([...(list.httpRead ?? []), ...(list.read ?? [])])
const outboxLayer = relayUrlsLocalsFirst([...(list.httpWrite ?? []), ...(list.write ?? [])])
return mergeRelayUrlLayers([inboxLayer, outboxLayer], blockedRelays)
}
@ -159,7 +160,8 @@ export function getRelayUrlsWithFavoritesFastReadAndInbox( @@ -159,7 +160,8 @@ export function getRelayUrlsWithFavoritesFastReadAndInbox(
* Profile page pins + feed: viewed author's NIP-65 read + write (REQ tier 1), then logged-in user's favorites,
* then fast-read defaults from constants, deduped and blocked-stripped, capped at this count.
*/
const PROFILE_PAGE_FEED_MAX_RELAYS = 6
/** Profile REQ cap: too small waits on a few bad relays; larger spreads load across fast-read / favorites. */
const PROFILE_PAGE_FEED_MAX_RELAYS = 14
export const PROFILE_PAGE_PINS_RESOLVE_LIMIT = 10
@ -167,14 +169,18 @@ export function buildProfilePageReadRelayUrls( @@ -167,14 +169,18 @@ export function buildProfilePageReadRelayUrls(
favoriteRelays: string[],
blockedRelays: string[],
authorRelayList: { read: string[]; write: string[]; httpRead?: string[]; httpWrite?: string[] },
kindsIncludeSocialBlockedKind: boolean
kindsIncludeSocialBlockedKind: boolean,
includeAuthorLocalRelays = false
): string[] {
const list = includeAuthorLocalRelays
? authorRelayList
: stripMailboxLocalUrlsForRemoteViewers(authorRelayList)
return getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays,
blockedRelays,
[...(authorRelayList.httpRead ?? []), ...(authorRelayList.read ?? [])],
[...(list.httpRead ?? []), ...(list.read ?? [])],
{
userWriteRelays: [...(authorRelayList.httpWrite ?? []), ...(authorRelayList.write ?? [])],
userWriteRelays: [...(list.httpWrite ?? []), ...(list.write ?? [])],
authorWriteRelays: [],
maxRelays: PROFILE_PAGE_FEED_MAX_RELAYS,
applySocialKindBlockedFilter: kindsIncludeSocialBlockedKind

29
src/lib/relay-list-sanitize.ts

@ -1,6 +1,35 @@ @@ -1,6 +1,35 @@
import { isHttpRelayUrl, isLocalNetworkUrl, normalizeAnyRelayUrl, normalizeUrl } from '@/lib/url'
import type { TRelayList } from '@/types'
/** True if this URL is not loopback / LAN (safe to open from another user's browser as a REQ target). */
export function urlIsNonLocalForRemoteViewer(url: string): boolean {
const t = typeof url === 'string' ? url.trim() : ''
if (!t) return false
if (isLocalNetworkUrl(t)) return false
const n = normalizeAnyRelayUrl(t) || ''
if (n && isLocalNetworkUrl(n)) return false
return true
}
/**
* Drop LAN/loopback from NIP-65 + HTTP mailbox fields when resolving **another** author's data:
* the viewer cannot reach the author's `localhost` / `192.168.*` / etc., but we used to rank them first.
*/
export function stripMailboxLocalUrlsForRemoteViewers(list: {
read: string[]
write: string[]
httpRead?: string[]
httpWrite?: string[]
}): { read: string[]; write: string[]; httpRead: string[]; httpWrite: string[] } {
const f = (arr: string[] | undefined) => (arr ?? []).filter(urlIsNonLocalForRemoteViewer)
return {
read: f(list.read),
write: f(list.write),
httpRead: f(list.httpRead),
httpWrite: f(list.httpWrite)
}
}
/**
* Remove LAN / loopback relay URLs (e.g. ws://localhost:4869, 192.168.x.x).
* Apply to **kind 10002** (NIP-65): those URLs belong on kind 10432 (cache relays), not read/write outbox/inbox.

16
src/lib/relay-url-priority.test.ts

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest'
import { dedupeNormalizeRelayUrlsOrdered, filterContextAuthorReadRelaysForPublish } from '@/lib/relay-url-priority'
import { stripMailboxLocalUrlsForRemoteViewers } from '@/lib/relay-list-sanitize'
describe('filterContextAuthorReadRelaysForPublish', () => {
it('drops loopback, LAN, and .onion; keeps public relays', () => {
@ -23,3 +24,18 @@ describe('filterContextAuthorReadRelaysForPublish', () => { @@ -23,3 +24,18 @@ describe('filterContextAuthorReadRelaysForPublish', () => {
expect(b).toEqual(a)
})
})
describe('stripMailboxLocalUrlsForRemoteViewers', () => {
it('removes loopback and LAN from read/write/http fields', () => {
const out = stripMailboxLocalUrlsForRemoteViewers({
read: ['ws://localhost:4869/', 'wss://relay.example.com/'],
write: ['wss://192.168.1.1/', 'wss://author-outbox.example/'],
httpRead: ['http://127.0.0.1:8080/'],
httpWrite: []
})
expect(out.read).toEqual(['wss://relay.example.com/'])
expect(out.write).toEqual(['wss://author-outbox.example/'])
expect(out.httpRead).toEqual([])
expect(out.httpWrite).toEqual([])
})
})

3
src/services/media-upload.service.ts

@ -12,8 +12,9 @@ import { simplifyUrl } from '@/lib/url' @@ -12,8 +12,9 @@ import { simplifyUrl } from '@/lib/url'
import { TDraftEvent, TMediaUploadServiceConfig } from '@/types'
import { BlossomClient } from 'blossom-client-sdk'
import { z } from 'zod'
import client from './client.service'
/** Must run before `./client.service` — that graph can synchronously re-enter this module; `storage` must be bound first (constructor reads it at module bottom). */
import storage from './local-storage.service'
import client from './client.service'
type UploadOptions = {
onProgress?: (progressPercent: number) => void

Loading…
Cancel
Save