Browse Source

integrate http relays into inboxes and outboxes

imwald
Silberengel 3 weeks ago
parent
commit
c1f952ed46
  1. 11
      src/components/Explore/ExploreRelayDirectory.tsx
  2. 22
      src/components/Explore/ExploreRelayReviews.tsx
  3. 8
      src/components/GifPicker/index.tsx
  4. 10
      src/components/MemePicker/index.tsx
  5. 49
      src/components/PostEditor/PostRelaySelector.tsx
  6. 9
      src/components/RelayInfo/RelayReviewsPreview.tsx
  7. 11
      src/components/RssArticleWebBookmarks/index.tsx
  8. 10
      src/components/SearchResult/index.tsx
  9. 8
      src/components/Sidebar/SidebarCalendarWeekWidget.tsx
  10. 4
      src/features/feed/relay-policy.ts
  11. 22
      src/hooks/useFetchCalendarRsvps.tsx
  12. 21
      src/hooks/useUserMailboxRelayUrls.ts
  13. 7
      src/hooks/useViewerInboxRelayUrls.ts
  14. 14
      src/lib/account-list-relay-urls.ts
  15. 6
      src/lib/draft-event.ts
  16. 44
      src/lib/favorites-feed-relays.ts
  17. 76
      src/lib/private-relays.ts
  18. 3
      src/lib/profile-reports-relays.ts
  19. 21
      src/lib/public-message-publish-relays.ts
  20. 23
      src/lib/tombstone-events.ts
  21. 54
      src/lib/viewer-read-inboxes.test.ts
  22. 88
      src/lib/viewer-read-inboxes.ts
  23. 44
      src/lib/viewer-write-outboxes.test.ts
  24. 77
      src/lib/viewer-write-outboxes.ts
  25. 8
      src/pages/primary/CalendarPrimaryPage.tsx
  26. 8
      src/pages/primary/SpellsPage/CreateSpellDialog.tsx
  27. 11
      src/pages/primary/SpellsPage/ProfileInteractionsMap.tsx
  28. 8
      src/pages/primary/SpellsPage/RelayThreadHeatMap.tsx
  29. 8
      src/pages/primary/SpellsPage/TopicKeywordHeatMap.tsx
  30. 12
      src/pages/primary/SpellsPage/index.tsx
  31. 36
      src/pages/primary/SpellsPage/useSpellsPageFeed.ts
  32. 13
      src/pages/secondary/EmojiSetsSettingsPage/index.tsx
  33. 11
      src/pages/secondary/FollowSetsSettingsPage/index.tsx
  34. 21
      src/pages/secondary/NoteListPage/index.tsx
  35. 11
      src/pages/secondary/RelayReviewsPage/index.tsx
  36. 42
      src/providers/FeedProvider.tsx
  37. 14
      src/providers/LiveActivitiesProvider.tsx
  38. 1
      src/providers/NostrProvider/index.tsx
  39. 179
      src/services/client.service.ts
  40. 101
      src/services/relay-selection.service.ts

11
src/components/Explore/ExploreRelayDirectory.tsx

@ -15,10 +15,7 @@ import { @@ -15,10 +15,7 @@ import {
loadCachedRelayReviews
} from '@/lib/explore-relay-reviews'
import { getRelayUrlFromRelayReviewEvent } from '@/lib/event-metadata'
import {
getRelayUrlsWithFavoritesFastReadAndInbox,
userReadRelaysWithHttp
} from '@/lib/favorites-feed-relays'
import { getRelayUrlsWithFavoritesFastReadAndInbox, userReadInboxUrls, userWriteOutboxUrls } from '@/lib/favorites-feed-relays'
import { toRelay } from '@/lib/link'
import { normalizeAnyRelayUrl } from '@/lib/url'
import { appendCuratedReadOnlyRelays } from '@/pages/primary/SpellsPage/fauxSpellFeeds'
@ -114,7 +111,7 @@ function ExploreRelayDirectoryCard({ entry }: { entry: ExploreRelayEntry }) { @@ -114,7 +111,7 @@ function ExploreRelayDirectoryCard({ entry }: { entry: ExploreRelayEntry }) {
export default function ExploreRelayDirectory({ listFilter = '' }: { listFilter?: string }) {
const { t } = useTranslation()
const { pubkey, relayList } = useNostr()
const { pubkey, relayList, cacheRelayListEvent } = useNostr()
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const relayInputsKey = useMemo(
@ -127,9 +124,9 @@ export default function ExploreRelayDirectory({ listFilter = '' }: { listFilter? @@ -127,9 +124,9 @@ export default function ExploreRelayDirectory({ listFilter = '' }: { listFilter?
getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays,
blockedRelays,
userReadRelaysWithHttp(relayList),
userReadInboxUrls(relayList, cacheRelayListEvent),
{
userWriteRelays: relayList?.write ?? [],
userWriteRelays: userWriteOutboxUrls(relayList, cacheRelayListEvent),
maxRelays: EXPLORE_REVIEWS_MAX_RELAYS,
applySocialKindBlockedFilter: false
}

22
src/components/Explore/ExploreRelayReviews.tsx

@ -8,10 +8,7 @@ import { @@ -8,10 +8,7 @@ import {
dedupeRelayReviewsNewestFirst,
loadCachedRelayReviews
} from '@/lib/explore-relay-reviews'
import {
getRelayUrlsWithFavoritesFastReadAndInbox,
userReadRelaysWithHttp
} from '@/lib/favorites-feed-relays'
import { getRelayUrlsWithFavoritesFastReadAndInbox, userReadInboxUrls, userWriteOutboxUrls } from '@/lib/favorites-feed-relays'
import { toRelay } from '@/lib/link'
import { isExploreBrowsableRelayUrl } from '@/lib/explore-popular-relays'
import { normalizeAnyRelayUrl } from '@/lib/url'
@ -59,7 +56,8 @@ const EXPLORE_REVIEWS_EOSE_TAIL_MS = 4500 @@ -59,7 +56,8 @@ const EXPLORE_REVIEWS_EOSE_TAIL_MS = 4500
function stableRelayInputsKey(
favoriteRelays: string[],
blockedRelays: string[],
relayList: { read?: string[]; write?: string[]; httpRead?: string[] } | null | undefined
relayList: { read?: string[]; write?: string[]; httpRead?: string[] } | null | undefined,
cacheRelayListEvent: Event | null | undefined
): string {
const normSortJoin = (urls: string[]) =>
[...urls]
@ -70,19 +68,19 @@ function stableRelayInputsKey( @@ -70,19 +68,19 @@ function stableRelayInputsKey(
return [
normSortJoin(favoriteRelays),
normSortJoin(blockedRelays),
normSortJoin([...(relayList?.httpRead ?? []), ...(relayList?.read ?? [])]),
normSortJoin(relayList?.write ?? [])
normSortJoin(userReadInboxUrls(relayList, cacheRelayListEvent)),
normSortJoin(userWriteOutboxUrls(relayList, cacheRelayListEvent))
].join('::')
}
export default function ExploreRelayReviews() {
const { t } = useTranslation()
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const { relayList } = useNostr()
const { relayList, cacheRelayListEvent } = useNostr()
const relayInputsKey = useMemo(
() => stableRelayInputsKey(favoriteRelays, blockedRelays, relayList),
[favoriteRelays, blockedRelays, relayList]
() => stableRelayInputsKey(favoriteRelays, blockedRelays, relayList, cacheRelayListEvent),
[favoriteRelays, blockedRelays, relayList, cacheRelayListEvent]
)
const relayUrls = useMemo(() => {
@ -90,9 +88,9 @@ export default function ExploreRelayReviews() { @@ -90,9 +88,9 @@ export default function ExploreRelayReviews() {
getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays,
blockedRelays,
userReadRelaysWithHttp(relayList),
userReadInboxUrls(relayList, cacheRelayListEvent),
{
userWriteRelays: relayList?.write ?? [],
userWriteRelays: userWriteOutboxUrls(relayList, cacheRelayListEvent),
maxRelays: EXPLORE_REVIEWS_MAX_RELAYS,
applySocialKindBlockedFilter: false
}

8
src/components/GifPicker/index.tsx

@ -10,7 +10,7 @@ import { Label } from '@/components/ui/label' @@ -10,7 +10,7 @@ import { Label } from '@/components/ui/label'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Skeleton } from '@/components/ui/skeleton'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import { useUserReadInboxUrls, useUserWriteOutboxUrls } from '@/hooks/useUserMailboxRelayUrls'
import { useNostr } from '@/providers/NostrProvider'
import { ExtendedKind, FAST_WRITE_RELAY_URLS, GIF_RELAY_URLS } from '@/constants'
import { cn } from '@/lib/utils'
@ -50,7 +50,7 @@ export default function GifPicker({ @@ -50,7 +50,7 @@ export default function GifPicker({
}) {
const { t } = useTranslation()
const { isSmallScreen } = useScreenSize()
const { publish, pubkey, relayList } = useNostr()
const { publish, pubkey } = useNostr()
const [open, setOpen] = useState(false)
const [searchInput, setSearchInput] = useState('')
// Initialise from the module-level session cache so re-opens are instant
@ -70,8 +70,8 @@ export default function GifPicker({ @@ -70,8 +70,8 @@ export default function GifPicker({
const fileInputRef = useRef<HTMLInputElement | null>(null)
const gifbuddyPopupRef = useRef<Window | null>(null)
const userReadRelays = useMemo(() => userReadRelaysWithHttp(relayList), [relayList])
const userWriteRelays = relayList?.write ?? []
const userReadRelays = useUserReadInboxUrls()
const userWriteRelays = useUserWriteOutboxUrls()
/** Paste / upload: GIF discovery relays + user writes (unchanged). */
const gifPublishRelayUrls = useMemo(() => {

10
src/components/MemePicker/index.tsx

@ -10,7 +10,7 @@ import { Label } from '@/components/ui/label' @@ -10,7 +10,7 @@ import { Label } from '@/components/ui/label'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Skeleton } from '@/components/ui/skeleton'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import { useUserReadInboxUrls, useUserWriteOutboxUrls } from '@/hooks/useUserMailboxRelayUrls'
import { useNostr } from '@/providers/NostrProvider'
import { ExtendedKind, GIF_RELAY_URLS } from '@/constants'
import { normalizeUrl } from '@/lib/url'
@ -24,7 +24,7 @@ import { @@ -24,7 +24,7 @@ import {
} from '@/services/meme.service'
import mediaUpload from '@/services/media-upload.service'
import { ExternalLink, X } from 'lucide-react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
/** In-session cache: survives Drawer/Dropdown open↔close without a relay re-fetch. */
let _sessionMemes: MemeMetadata[] = []
@ -68,7 +68,7 @@ export default function MemePicker({ @@ -68,7 +68,7 @@ export default function MemePicker({
}) {
const { t } = useTranslation()
const { isSmallScreen } = useScreenSize()
const { publish, pubkey, relayList } = useNostr()
const { publish, pubkey } = useNostr()
const [open, setOpen] = useState(false)
const [query, setQuery] = useState('')
const [searchInput, setSearchInput] = useState('')
@ -86,8 +86,8 @@ export default function MemePicker({ @@ -86,8 +86,8 @@ export default function MemePicker({
const fileInputRef = useRef<HTMLInputElement | null>(null)
const memeamigoPopupRef = useRef<Window | null>(null)
const userReadRelays = useMemo(() => userReadRelaysWithHttp(relayList), [relayList])
const userWriteRelays = relayList?.write ?? []
const userReadRelays = useUserReadInboxUrls()
const userWriteRelays = useUserWriteOutboxUrls()
/** Keep memesRef, session cache, and React state in sync. */
const setMemes = useCallback((newMemes: MemeMetadata[], isSearch = false) => {

49
src/components/PostEditor/PostRelaySelector.tsx

@ -1,13 +1,12 @@ @@ -1,13 +1,12 @@
import { ExtendedKind, isSocialKindBlockedKind, MAX_PUBLISH_RELAYS, SOCIAL_KIND_BLOCKED_RELAY_URLS } from '@/constants'
import { isSocialKindBlockedKind, MAX_PUBLISH_RELAYS, SOCIAL_KIND_BLOCKED_RELAY_URLS } from '@/constants'
import { NOSTR_URI_FOR_REPLY_PUBKEYS_REGEX } from '@/lib/content-patterns'
import { simplifyUrl, isLocalNetworkUrl, normalizeAnyRelayUrl, normalizeUrl } from '@/lib/url'
import { simplifyUrl, isLocalNetworkUrl, normalizeRelayUrlByScheme } from '@/lib/url'
import { collectViewerWriteOutboxUrls } from '@/lib/viewer-write-outboxes'
import { useCurrentRelays } from '@/providers/CurrentRelaysProvider'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { useNostr } from '@/providers/NostrProvider'
import { getRelayListFromEvent } from '@/lib/event-metadata'
import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import indexedDb from '@/services/indexed-db.service'
import { userReadInboxUrls } from '@/lib/favorites-feed-relays'
import { Check, ChevronDown, Server } from 'lucide-react'
import { NostrEvent } from 'nostr-tools'
import { Dispatch, SetStateAction, useCallback, useEffect, useState, useMemo, useRef } from 'react'
@ -25,7 +24,7 @@ const NO_MENTIONS: string[] = [] @@ -25,7 +24,7 @@ const NO_MENTIONS: string[] = []
/** Keep auto-selection within {@link MAX_PUBLISH_RELAYS}, preserving {@link selectableRelaysOrder} (top of list first). */
function capAutoSelectedRelays(selectableRelaysOrder: string[], selectedWithCache: string[]): string[] {
const norm = (u: string) => normalizeAnyRelayUrl(u) || u
const norm = (u: string) => normalizeRelayUrlByScheme(u) || u
const selectedNormSet = new Set(selectedWithCache.map(norm))
const ordered: string[] = []
for (const url of selectableRelaysOrder) {
@ -71,8 +70,11 @@ export default function PostRelaySelector({ @@ -71,8 +70,11 @@ export default function PostRelaySelector({
const { isSmallScreen } = useScreenSize()
useCurrentRelays() // Keep this hook call for any side effects
const { relaySets, favoriteRelays, blockedRelays } = useFavoriteRelays()
const { pubkey, relayList } = useNostr()
const userReadRelaysForSelection = useMemo(() => userReadRelaysWithHttp(relayList), [relayList])
const { pubkey, relayList, cacheRelayListEvent } = useNostr()
const userReadRelaysForSelection = useMemo(
() => userReadInboxUrls(relayList, cacheRelayListEvent),
[relayList, cacheRelayListEvent]
)
const [selectedRelayUrls, setSelectedRelayUrls] = useState<string[]>([])
const [selectableRelays, setSelectableRelays] = useState<string[]>([])
const [relayTypes, setRelayTypes] = useState<Record<string, RelaySourceType>>({})
@ -165,32 +167,9 @@ export default function PostRelaySelector({ @@ -165,32 +167,9 @@ export default function PostRelaySelector({
const updateRelaySelection = async () => {
setIsLoading(true)
try {
let userWriteRelays = relayList?.write || []
if (pubkey) {
try {
const cacheRelayListEvent = await indexedDb.getReplaceableEvent(pubkey, ExtendedKind.CACHE_RELAYS)
if (cacheRelayListEvent) {
const cacheRelayList = getRelayListFromEvent(cacheRelayListEvent)
const cacheRelays = [
...cacheRelayList.write,
...cacheRelayList.originalRelays
.filter(relay => (relay.scope === 'both' || relay.scope === 'write') && isLocalNetworkUrl(relay.url))
.map(relay => relay.url)
].filter(url => {
if (!url || typeof url !== 'string' || url.trim() === '' || url === 'ws://' || url === 'wss://') return false
return isLocalNetworkUrl(url)
})
const existingUrls = new Set(userWriteRelays.map(url => normalizeUrl(url) || url))
const newCacheRelays = cacheRelays
.map(url => normalizeUrl(url) || url)
.filter((url): url is string => !!url && !existingUrls.has(url))
if (newCacheRelays.length > 0) {
userWriteRelays = [...newCacheRelays, ...userWriteRelays]
}
}
} catch (error) {
logger.warn('Failed to get cache relays from IndexedDB', { error, pubkey })
}
let userWriteRelays: string[] = []
if (pubkey && relayList) {
userWriteRelays = await collectViewerWriteOutboxUrls(pubkey, relayList)
}
const result = await relaySelectionService.selectRelays({
@ -269,7 +248,7 @@ export default function PostRelaySelector({ @@ -269,7 +248,7 @@ export default function PostRelaySelector({
useEffect(() => {
// An event is "protected" if we have selected relays that aren't the default user write relays
const defaultUserWriteRelays = [...(relayList?.httpWrite ?? []), ...(relayList?.write || [])]
const normW = (u: string) => normalizeAnyRelayUrl(u) || u
const normW = (u: string) => normalizeRelayUrlByScheme(u) || u
const defaultNorm = new Set(defaultUserWriteRelays.map(normW))
const isProtectedEvent =
selectedRelayUrls.length > 0 &&

9
src/components/RelayInfo/RelayReviewsPreview.tsx

@ -8,10 +8,7 @@ import { @@ -8,10 +8,7 @@ import {
CarouselPrevious
} from '@/components/ui/carousel'
import { ExtendedKind } from '@/constants'
import {
getRelayUrlsWithFavoritesFastReadAndInbox,
userReadRelaysWithHttp
} from '@/lib/favorites-feed-relays'
import { getRelayUrlsWithFavoritesFastReadAndInbox, userReadInboxUrls } from '@/lib/favorites-feed-relays'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { compareEvents } from '@/lib/event'
import { getStarsFromRelayReviewEvent } from '@/lib/event-metadata'
@ -39,7 +36,7 @@ import ReviewEditor from './ReviewEditor' @@ -39,7 +36,7 @@ import ReviewEditor from './ReviewEditor'
export default function RelayReviewsPreview({ relayUrl }: { relayUrl: string }) {
const { t } = useTranslation()
const { push } = useSecondaryPage()
const { pubkey, checkLogin, relayList } = useNostr()
const { pubkey, checkLogin, relayList, cacheRelayListEvent } = useNostr()
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const { mutePubkeySet } = useMuteList()
const [showEditor, setShowEditor] = useState(false)
@ -117,7 +114,7 @@ export default function RelayReviewsPreview({ relayUrl }: { relayUrl: string }) @@ -117,7 +114,7 @@ export default function RelayReviewsPreview({ relayUrl }: { relayUrl: string })
const base = getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays,
blockedRelays,
userReadRelaysWithHttp(relayList)
userReadInboxUrls(relayList, cacheRelayListEvent)
)
const uniqueUrls = [...new Set([normalizedTarget, ...base])]

11
src/components/RssArticleWebBookmarks/index.tsx

@ -5,10 +5,7 @@ import { Separator } from '@/components/ui/separator' @@ -5,10 +5,7 @@ import { Separator } from '@/components/ui/separator'
import { Textarea } from '@/components/ui/textarea'
import { ExtendedKind } from '@/constants'
import { createWebBookmarkDraftEvent } from '@/lib/draft-event'
import {
getRelayUrlsWithFavoritesFastReadAndInbox,
userReadRelaysWithHttp
} from '@/lib/favorites-feed-relays'
import { getRelayUrlsWithFavoritesFastReadAndInbox, userReadInboxUrls } from '@/lib/favorites-feed-relays'
import logger from '@/lib/logger'
import { showPublishingError } from '@/lib/publishing-feedback'
import {
@ -34,7 +31,7 @@ import { useTranslation } from 'react-i18next' @@ -34,7 +31,7 @@ import { useTranslation } from 'react-i18next'
export default function RssArticleWebBookmarks({ articleUrl }: { articleUrl: string }) {
const { t } = useTranslation()
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const { pubkey, publish, attemptDelete, relayList, account } = useNostr()
const { pubkey, publish, attemptDelete, relayList, cacheRelayListEvent, account } = useNostr()
const canonical = useMemo(() => canonicalizeRssArticleUrl(articleUrl), [articleUrl])
const iVals = useMemo(() => {
@ -43,11 +40,11 @@ export default function RssArticleWebBookmarks({ articleUrl }: { articleUrl: str @@ -43,11 +40,11 @@ export default function RssArticleWebBookmarks({ articleUrl }: { articleUrl: str
}, [canonical])
const relayUrls = useMemo(() => {
const read = userReadRelaysWithHttp(relayList)
const read = userReadInboxUrls(relayList, cacheRelayListEvent)
const base = getRelayUrlsWithFavoritesFastReadAndInbox(favoriteRelays, blockedRelays, read, {})
if (!base.length) return []
return appendCuratedReadOnlyRelays(base, blockedRelays)
}, [favoriteRelays, blockedRelays, relayList])
}, [favoriteRelays, blockedRelays, relayList, cacheRelayListEvent])
const [mine, setMine] = useState<Event[]>([])
const [loading, setLoading] = useState(false)

10
src/components/SearchResult/index.tsx

@ -8,6 +8,7 @@ import Relay from '../Relay' @@ -8,6 +8,7 @@ import Relay from '../Relay'
import { useNostr } from '@/providers/NostrProvider'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import client from '@/services/client.service'
import { userReadInboxUrls, userWriteOutboxUrls } from '@/lib/favorites-feed-relays'
import { normalizeUrl } from '@/lib/url'
import { buildAlexandriaEventsSearchUrlForTSearchParams } from '@/lib/alexandria-events-search-url'
import { useLayoutEffect, useMemo } from 'react'
@ -17,7 +18,7 @@ function relayDedupeKey(url: string): string { @@ -17,7 +18,7 @@ function relayDedupeKey(url: string): string {
}
export default function SearchResult({ searchParams }: { searchParams: TSearchParams | null }) {
const { pubkey, relayList } = useNostr()
const { relayList, cacheRelayListEvent } = useNostr()
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
/**
@ -54,7 +55,10 @@ export default function SearchResult({ searchParams }: { searchParams: TSearchPa @@ -54,7 +55,10 @@ export default function SearchResult({ searchParams }: { searchParams: TSearchPa
const relays: string[] = []
if (relayList) {
relays.push(...(relayList.read || []), ...(relayList.write || []))
relays.push(
...userReadInboxUrls(relayList, cacheRelayListEvent),
...userWriteOutboxUrls(relayList, cacheRelayListEvent)
)
}
relays.push(...(favoriteRelays || []))
@ -75,7 +79,7 @@ export default function SearchResult({ searchParams }: { searchParams: TSearchPa @@ -75,7 +79,7 @@ export default function SearchResult({ searchParams }: { searchParams: TSearchPa
const n = normalizeUrl(relay) || relay
return !blockedSet.has(n)
})
}, [pubkey, relayList, favoriteRelays, blockedRelays])
}, [relayList, cacheRelayListEvent, favoriteRelays, blockedRelays])
const nonSearchableRelays = useMemo(
() => combinedRelays.filter((u) => !searchableKeySet.has(relayDedupeKey(u))),

8
src/components/Sidebar/SidebarCalendarWeekWidget.tsx

@ -7,7 +7,7 @@ import { @@ -7,7 +7,7 @@ import {
getCalendarOccurrenceWindowMs,
getLocalMondayWeekBounds
} from '@/lib/calendar-event'
import { getRelayUrlsWithFavoritesFastReadAndInbox, userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import { getRelayUrlsWithFavoritesFastReadAndInbox, userReadInboxUrls, userWriteOutboxUrls } from '@/lib/favorites-feed-relays'
import { replaceableEventDedupeKey } from '@/lib/event'
import { toNote } from '@/lib/link'
import { cn } from '@/lib/utils'
@ -41,7 +41,7 @@ const SESSION_CALENDAR_MERGE_CAP = 1200 @@ -41,7 +41,7 @@ const SESSION_CALENDAR_MERGE_CAP = 1200
export default function SidebarCalendarWeekWidget() {
const { t } = useTranslation()
const { relayList, pubkey } = useNostr()
const { relayList, cacheRelayListEvent, pubkey } = useNostr()
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const followList = useFollowListOptional()
const { navigateToNote } = useSmartNoteNavigation()
@ -62,9 +62,9 @@ export default function SidebarCalendarWeekWidget() { @@ -62,9 +62,9 @@ export default function SidebarCalendarWeekWidget() {
const base = getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays,
blockedRelays,
userReadRelaysWithHttp(relayList),
userReadInboxUrls(relayList, cacheRelayListEvent),
{
userWriteRelays: relayList?.write ?? [],
userWriteRelays: userWriteOutboxUrls(relayList, cacheRelayListEvent),
applySocialKindBlockedFilter: false
}
)

4
src/features/feed/relay-policy.ts

@ -11,7 +11,7 @@ import { @@ -11,7 +11,7 @@ import {
relayUrlsStripExtendedTagReqBlocked
} from '@/lib/relay-extended-tag-req-blocks'
import { isRelayBlockedByUser } from '@/lib/relay-blocked'
import { isLocalNetworkUrl, normalizeAnyRelayUrl, normalizeHttpRelayUrl } from '@/lib/url'
import { isLocalNetworkUrl, normalizeHttpRelayUrl, normalizeRelayUrlByScheme } from '@/lib/url'
import type { TSubRequestFilter } from '@/types'
export type FeedRelayOperation = 'read' | 'write' | 'publish-picker' | 'favorites-feed'
@ -98,7 +98,7 @@ function canonicalRelayUrl(url: string | undefined | null, layerSource?: FeedRel @@ -98,7 +98,7 @@ function canonicalRelayUrl(url: string | undefined | null, layerSource?: FeedRel
function normalizedRelayUrl(url: string, layerSource?: FeedRelayLayerSource | string): string {
if (layerSource === 'http-index') return normalizeHttpRelayUrl(url) || url.trim()
return normalizeAnyRelayUrl(url) || url.trim()
return normalizeRelayUrlByScheme(url) || url.trim()
}
function normalizedSet(urls: readonly string[] | undefined): Set<string> {

22
src/hooks/useFetchCalendarRsvps.tsx

@ -12,23 +12,9 @@ import { Event } from 'nostr-tools' @@ -12,23 +12,9 @@ import { Event } from 'nostr-tools'
import { useEffect, useState } from 'react'
import { normalizeAnyRelayUrl } from '@/lib/url'
import { FAST_READ_RELAY_URLS } from '@/constants'
import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import { userReadInboxUrls, userWriteOutboxUrls } from '@/lib/favorites-feed-relays'
import { tagNameEquals } from '@/lib/tag'
/** NIP-65 inboxes only — calendar RSVPs are published to the author’s outboxes, so REQ must include those too. */
function userWriteRelaysForQuery(
relayList: { write?: string[]; httpWrite?: string[] } | null | undefined
): string[] {
if (!relayList) return []
const ws = (relayList.write ?? [])
.map((url) => normalizeAnyRelayUrl(url) || url)
.filter(Boolean) as string[]
const http = (relayList.httpWrite ?? [])
.map((url) => normalizeAnyRelayUrl(url) || url)
.filter(Boolean) as string[]
return [...http, ...ws]
}
function getRsvpStatus(rsvp: Event): 'accepted' | 'tentative' | 'declined' | undefined {
const status = rsvp.tags.find(tagNameEquals('status'))?.[1]
if (status === 'accepted' || status === 'tentative' || status === 'declined') return status
@ -53,7 +39,7 @@ function mergeRsvpList(events: Event[]): Event[] { @@ -53,7 +39,7 @@ function mergeRsvpList(events: Event[]): Event[] {
}
export function useFetchCalendarRsvps(calendarEvent: Event | undefined) {
const { relayList } = useNostr()
const { relayList, cacheRelayListEvent } = useNostr()
const [rsvps, setRsvps] = useState<Event[]>([])
const [isFetching, setIsFetching] = useState(false)
@ -69,8 +55,8 @@ export function useFetchCalendarRsvps(calendarEvent: Event | undefined) { @@ -69,8 +55,8 @@ export function useFetchCalendarRsvps(calendarEvent: Event | undefined) {
const coordinate = normalizeReplaceableCoordinateString(
getReplaceableCoordinateFromEvent(calendarEvent)
)
const userRead = userReadRelaysWithHttp(relayList)
const userWrite = userWriteRelaysForQuery(relayList)
const userRead = userReadInboxUrls(relayList, cacheRelayListEvent)
const userWrite = userWriteOutboxUrls(relayList, cacheRelayListEvent)
void (async () => {
const fromSession = client.getSessionCalendarRsvpsForCalendarEvent(calendarEvent)

21
src/hooks/useUserMailboxRelayUrls.ts

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
import { userReadInboxUrls, userWriteOutboxUrls } from '@/lib/favorites-feed-relays'
import { useNostrOptional } from '@/providers/nostr-context'
import { useMemo } from 'react'
/** Viewer read inbox: kind 10432 cache + kind 10243 HTTP + kind 10002 WS. */
export function useUserReadInboxUrls(): string[] {
const nostr = useNostrOptional()
return useMemo(
() => userReadInboxUrls(nostr?.relayList, nostr?.cacheRelayListEvent),
[nostr?.relayList, nostr?.cacheRelayListEvent]
)
}
/** Viewer write outbox: kind 10432 cache + kind 10243 HTTP + kind 10002 WS. */
export function useUserWriteOutboxUrls(): string[] {
const nostr = useNostrOptional()
return useMemo(
() => userWriteOutboxUrls(nostr?.relayList, nostr?.cacheRelayListEvent),
[nostr?.relayList, nostr?.cacheRelayListEvent]
)
}

7
src/hooks/useViewerInboxRelayUrls.ts

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import { collectViewerReadInboxUrls } from '@/lib/viewer-read-inboxes'
import { useNostrOptional } from '@/providers/nostr-context'
import client from '@/services/client.service'
import { useEffect, useState } from 'react'
@ -19,7 +19,10 @@ export function useViewerInboxRelayUrls(): { @@ -19,7 +19,10 @@ export function useViewerInboxRelayUrls(): {
let cancelled = false
void client.peekRelayListFromStorage(pk).then((rl) => {
if (cancelled) return
setInboxRelayUrls(userReadRelaysWithHttp(rl).slice(0, 14))
void collectViewerReadInboxUrls(pk, rl).then((urls) => {
if (cancelled) return
setInboxRelayUrls(urls.slice(0, 14))
})
})
return () => {
cancelled = true

14
src/lib/account-list-relay-urls.ts

@ -1,6 +1,8 @@ @@ -1,6 +1,8 @@
import { getFavoritesFeedRelayUrls } from '@/lib/favorites-feed-relays'
import { buildPrioritizedReadRelayUrls, buildPrioritizedWriteRelayUrls } from '@/lib/relay-url-priority'
import { normalizeAnyRelayUrl } from '@/lib/url'
import { normalizeRelayUrlByScheme } from '@/lib/url'
import { collectViewerReadInboxUrls } from '@/lib/viewer-read-inboxes'
import { collectViewerWriteOutboxUrls } from '@/lib/viewer-write-outboxes'
import { viewerUsesGlobalRelayDefaults } from '@/lib/viewer-relay-defaults'
import client from '@/services/client.service'
@ -21,9 +23,11 @@ export async function buildAccountListRelayUrlsForMerge(options: { @@ -21,9 +23,11 @@ export async function buildAccountListRelayUrlsForMerge(options: {
relayList: myRelayList
})
const favoritesTier = getFavoritesFeedRelayUrls(favoriteRelays ?? [], blockedRelays, useGlobal)
const writeOutboxes = await collectViewerWriteOutboxUrls(accountPubkey, myRelayList)
const readInboxes = await collectViewerReadInboxUrls(accountPubkey, myRelayList)
const read = buildPrioritizedReadRelayUrls({
userReadRelays: myRelayList.read ?? [],
userWriteRelays: myRelayList.write ?? [],
userReadRelays: readInboxes,
userWriteRelays: writeOutboxes,
favoriteRelays: favoritesTier,
blockedRelays,
maxRelays: 100,
@ -31,7 +35,7 @@ export async function buildAccountListRelayUrlsForMerge(options: { @@ -31,7 +35,7 @@ export async function buildAccountListRelayUrlsForMerge(options: {
includeGlobalFastRead: useGlobal
})
const write = buildPrioritizedWriteRelayUrls({
userWriteRelays: myRelayList.write ?? [],
userWriteRelays: writeOutboxes,
favoriteRelays: favoritesTier,
blockedRelays,
maxRelays: 100,
@ -39,5 +43,5 @@ export async function buildAccountListRelayUrlsForMerge(options: { @@ -39,5 +43,5 @@ export async function buildAccountListRelayUrlsForMerge(options: {
includeGlobalFastWriteReadTails: useGlobal
})
const merged = [...read, ...write]
return [...new Set(merged.map((u) => normalizeAnyRelayUrl(u) || u).filter(Boolean))]
return [...new Set(merged.map((u) => normalizeRelayUrlByScheme(u) || u).filter(Boolean))]
}

6
src/lib/draft-event.ts

@ -33,6 +33,7 @@ import { @@ -33,6 +33,7 @@ import {
} from '@/lib/rss-article'
import { EMOJI_SHORT_CODE_REGEX } from '@/lib/content-patterns'
import { blossomSha256FromBlobUrl, cleanUrl, isBlossomBudBlobUrl } from '@/lib/url'
import { collectReadInboxUrlsFromRelayList } from '@/lib/viewer-read-inboxes'
import { urlToWebBookmarkDTag } from '@/lib/web-bookmark-nip'
import { randomString } from './random'
import { generateBech32IdFromETag, getImetaInfoFromImetaTag, tagNameEquals } from './tag'
@ -1169,10 +1170,7 @@ export async function createPollDraftEvent( @@ -1169,10 +1170,7 @@ export async function createPollDraftEvent(
relays.forEach((relay) => tags.push(buildRelayTag(relay)))
} else {
const relayList = await client.fetchRelayList(author)
const readHints = [
...(relayList.httpRead || []).slice(0, 4),
...(relayList.read || []).slice(0, 4)
].slice(0, 4)
const readHints = collectReadInboxUrlsFromRelayList(relayList).slice(0, 4)
readHints.forEach((relay) => {
tags.push(buildRelayTag(relay))
})

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

@ -20,6 +20,10 @@ import { feedRelayPolicyUrls, type FeedRelayLayer } from '@/features/feed/relay- @@ -20,6 +20,10 @@ import { feedRelayPolicyUrls, type FeedRelayLayer } from '@/features/feed/relay-
import { stripMailboxLocalUrlsForRemoteViewers } from '@/lib/relay-list-sanitize'
import { relaySessionStrikes } from '@/lib/relay-strikes'
import { profileFetchRelayUrlsWithoutFastReadLayer } from '@/lib/viewer-relay-defaults'
import { getCacheRelayUrlsFromEvent } from '@/lib/private-relays'
import { collectUserReadInboxUrls } from '@/lib/viewer-read-inboxes'
import { collectUserWriteOutboxUrls } from '@/lib/viewer-write-outboxes'
import type { Event } from 'nostr-tools'
function isBlockedRelay(url: string, blockedRelays: string[]): boolean {
return isRelayBlockedByUser(url, blockedRelays)
@ -32,14 +36,36 @@ function isBlockedRelay(url: string, blockedRelays: string[]): boolean { @@ -32,14 +36,36 @@ function isBlockedRelay(url: string, blockedRelays: string[]): boolean {
* Same list drives the favorites tier in REQ/publish prioritization and the all-favorites home feed.
*/
/**
* NIP-65 `read` plus HTTP index inboxes (kind 10243) for feed REQ / query URL lists.
* Logged-in user's read inbox: kind 10432 cache + kind 10243 HTTP + kind 10002 WS.
* Pass `cacheRelayListEvent` (or `cacheUrls`) when kind 10432 is not merged into `relayList.read`.
*/
export function userReadInboxUrls(
relayList: { read?: string[]; httpRead?: string[] } | undefined | null,
cacheRelayListEvent?: Event | null,
cacheUrls?: readonly string[]
): string[] {
const cache = cacheUrls ?? getCacheRelayUrlsFromEvent(cacheRelayListEvent)
return collectUserReadInboxUrls(relayList, cache)
}
/**
* Logged-in user's write outbox: kind 10432 cache + kind 10243 HTTP + kind 10002 WS.
*/
export function userWriteOutboxUrls(
relayList: { write?: string[]; httpWrite?: string[] } | undefined | null,
cacheRelayListEvent?: Event | null,
cacheUrls?: readonly string[]
): string[] {
const cache = cacheUrls ?? getCacheRelayUrlsFromEvent(cacheRelayListEvent)
return collectUserWriteOutboxUrls(relayList, cache)
}
/** @deprecated use {@link userReadInboxUrls} */
export function userReadRelaysWithHttp(
relayList: { read?: string[]; httpRead?: string[] } | undefined | null
relayList: { read?: string[]; httpRead?: string[] } | undefined | null,
cacheRelayListEvent?: Event | null
): string[] {
const http = relayList?.httpRead ?? []
const read = relayList?.read ?? []
return dedupeNormalizeRelayUrlsOrdered([...http, ...read])
return userReadInboxUrls(relayList, cacheRelayListEvent)
}
export function getFavoritesFeedRelayUrls(
@ -98,8 +124,8 @@ export function buildAuthorInboxOutboxRelayUrls( @@ -98,8 +124,8 @@ export function buildAuthorInboxOutboxRelayUrls(
const list = includeAuthorLocalRelays
? authorRelayList
: stripMailboxLocalUrlsForRemoteViewers(authorRelayList)
const inboxLayer = relayUrlsLocalsFirst([...(list.httpRead ?? []), ...(list.read ?? [])])
const outboxLayer = relayUrlsLocalsFirst([...(list.httpWrite ?? []), ...(list.write ?? [])])
const inboxLayer = relayUrlsLocalsFirst(collectUserReadInboxUrls(list))
const outboxLayer = relayUrlsLocalsFirst(collectUserWriteOutboxUrls(list))
return mergeRelayUrlLayers([inboxLayer, outboxLayer], blockedRelays)
}
@ -213,8 +239,8 @@ export function buildProfilePageReadRelayUrls( @@ -213,8 +239,8 @@ export function buildProfilePageReadRelayUrls(
const list = includeAuthorLocalRelays
? authorRelayList
: stripMailboxLocalUrlsForRemoteViewers(authorRelayList)
const authorRead = [...(list.httpRead ?? []), ...(list.read ?? [])]
const authorWrite = [...(list.httpWrite ?? []), ...(list.write ?? [])]
const authorRead = collectUserReadInboxUrls(list)
const authorWrite = collectUserWriteOutboxUrls(list)
const authorHasNoNip65 = authorRead.length === 0 && authorWrite.length === 0
const favorites = getFavoritesFeedRelayUrls(favoriteRelays, blockedRelays, useGlobal)

76
src/lib/private-relays.ts

@ -1,78 +1,46 @@ @@ -1,78 +1,46 @@
import client from '@/services/client.service'
import {
collectViewerWriteOutboxUrls,
viewerHasWriteOutboxes
} from '@/lib/viewer-write-outboxes'
import indexedDb from '@/services/indexed-db.service'
import { ExtendedKind } from '@/constants'
import type { Event } from 'nostr-tools'
/** Kind 10432 relay tag URLs from an in-memory event (sync). */
export function getCacheRelayUrlsFromEvent(event: Event | null | undefined): string[] {
if (!event) return []
const relayUrls: string[] = []
event.tags.forEach((tag) => {
if (tag[0] === 'relay' && tag[1]) {
relayUrls.push(tag[1])
}
})
return Array.from(new Set(relayUrls))
}
/**
* Check if user has private relays available (outbox relays or cache relays)
* @param pubkey - User's public key
* @returns Promise<boolean> - true if user has at least one private relay available
*/
export async function hasPrivateRelays(pubkey: string): Promise<boolean> {
// Check for outbox relays (kind 10002) — IndexedDB merge only; no network wait.
const relayList = await client.peekRelayListFromStorage(pubkey)
if (relayList.write && relayList.write.length > 0) {
return true
}
// Check for cache relays (kind 10432)
const cacheRelayEvent = await indexedDb.getReplaceableEvent(pubkey, ExtendedKind.CACHE_RELAYS)
if (cacheRelayEvent) {
// Check if cache relay event has any relays
const hasRelays = cacheRelayEvent.tags.some(tag => tag[0] === 'relay' && tag[1])
if (hasRelays) {
return true
}
}
return false
return viewerHasWriteOutboxes(pubkey, relayList)
}
/**
* Get private relay URLs (outbox + cache relays)
* @param pubkey - User's public key
* @returns Promise<string[]> - Array of relay URLs
* Get private relay URLs (kind 10002 WS + kind 10243 HTTP + kind 10432 cache write outboxes)
*/
export async function getPrivateRelayUrls(pubkey: string): Promise<string[]> {
const relayUrls: string[] = []
// Get outbox relays (kind 10002) — storage-first; cache rows below still augment.
const relayList = await client.peekRelayListFromStorage(pubkey)
if (relayList.write) {
relayUrls.push(...relayList.write)
}
// Get cache relays (kind 10432)
const cacheRelayEvent = await indexedDb.getReplaceableEvent(pubkey, ExtendedKind.CACHE_RELAYS)
if (cacheRelayEvent) {
cacheRelayEvent.tags.forEach(tag => {
if (tag[0] === 'relay' && tag[1]) {
relayUrls.push(tag[1])
}
})
}
// Deduplicate
return Array.from(new Set(relayUrls))
return collectViewerWriteOutboxUrls(pubkey, relayList)
}
/**
* Get cache relay URLs only
* Get cache relay URLs only (kind 10432)
* @param pubkey - User's public key
* @returns Promise<string[]> - Array of cache relay URLs
*/
export async function getCacheRelayUrls(pubkey: string): Promise<string[]> {
const relayUrls: string[] = []
// Get cache relays (kind 10432)
const cacheRelayEvent = await indexedDb.getReplaceableEvent(pubkey, ExtendedKind.CACHE_RELAYS)
if (cacheRelayEvent) {
cacheRelayEvent.tags.forEach(tag => {
if (tag[0] === 'relay' && tag[1]) {
relayUrls.push(tag[1])
}
})
}
return Array.from(new Set(relayUrls))
return getCacheRelayUrlsFromEvent(cacheRelayEvent)
}

3
src/lib/profile-reports-relays.ts

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
import { feedRelayPolicyUrls } from '@/features/feed/relay-policy'
import { relayUrlsLocalsFirst } from '@/lib/relay-url-priority'
import { collectReadInboxUrlsFromRelayList } from '@/lib/viewer-read-inboxes'
import { stripMailboxLocalUrlsForRemoteViewers } from '@/lib/relay-list-sanitize'
import { normalizeAnyRelayUrl } from '@/lib/url'
@ -36,7 +37,7 @@ export function buildProfileReportsRelayUrls( @@ -36,7 +37,7 @@ export function buildProfileReportsRelayUrls(
const list = options.includeAuthorLocalRelays
? mailboxList
: stripMailboxLocalUrlsForRemoteViewers(mailboxList)
const inboxLayer = relayUrlsLocalsFirst([...(list.httpRead ?? []), ...(list.read ?? [])])
const inboxLayer = relayUrlsLocalsFirst(collectReadInboxUrlsFromRelayList(list))
const cacheLayer = relayUrlsLocalsFirst(
(options.cacheRelayUrls ?? []).filter((u) => {
const k = normalizeAnyRelayUrl(u) || u.trim()

21
src/lib/public-message-publish-relays.ts

@ -5,32 +5,21 @@ import { @@ -5,32 +5,21 @@ import {
} from '@/constants'
import { feedRelayPolicyUrls } from '@/features/feed/relay-policy'
import { dedupeNormalizeRelayUrlsOrdered, relayUrlsLocalsFirst } from '@/lib/relay-url-priority'
import { isLocalNetworkUrl, normalizeHttpRelayUrl, normalizeUrl } from '@/lib/url'
import { collectRemoteReadInboxUrlsFromRelayList } from '@/lib/viewer-read-inboxes'
import { collectWriteOutboxUrlsFromRelayList } from '@/lib/viewer-write-outboxes'
import type { TRelayList } from '@/types'
/** NIP-65 / 10243 outbox URLs for the sender (includes viewer-local outboxes). */
/** NIP-65 / 10243 outbox URLs for the sender (includes viewer-local outboxes when present on `write`). */
export function collectSenderOutboxUrls(
relayList: TRelayList | null | undefined,
extraWriteUrls: readonly string[] = []
): string[] {
const http = (relayList?.httpWrite ?? [])
.map((u) => normalizeHttpRelayUrl(u) || u)
.filter((u): u is string => !!u)
const ws = (relayList?.write ?? [])
.map((u) => normalizeUrl(u) || u)
.filter((u): u is string => !!u)
return dedupeNormalizeRelayUrlsOrdered([...http, ...ws, ...extraWriteUrls])
return collectWriteOutboxUrlsFromRelayList(relayList, extraWriteUrls)
}
/** NIP-65 / 10243 inbox URLs for a recipient (drops other people's LAN/loopback). */
export function collectRecipientInboxUrls(relayList: TRelayList | null | undefined): string[] {
const http = (relayList?.httpRead ?? [])
.map((u) => normalizeHttpRelayUrl(u) || u)
.filter((u): u is string => !!u && !isLocalNetworkUrl(u))
const ws = (relayList?.read ?? [])
.map((u) => normalizeUrl(u) || u)
.filter((u): u is string => !!u && !isLocalNetworkUrl(u))
return dedupeNormalizeRelayUrlsOrdered([...http, ...ws])
return collectRemoteReadInboxUrlsFromRelayList(relayList)
}
/**

23
src/lib/tombstone-events.ts

@ -1,5 +1,7 @@ @@ -1,5 +1,7 @@
import { PROFILE_RELAY_URLS } from '@/constants'
import { normalizeAnyRelayUrl, normalizeHttpRelayUrl, normalizeUrl } from '@/lib/url'
import { normalizeAnyRelayUrl, normalizeUrl } from '@/lib/url'
import { collectUserReadInboxUrls } from '@/lib/viewer-read-inboxes'
import { collectUserWriteOutboxUrls } from '@/lib/viewer-write-outboxes'
import type { TRelayList } from '@/types'
/** Dispatched after tombstones in IndexedDB change (kind-5 sync or local apply). */
@ -11,22 +13,21 @@ export function dispatchTombstonesUpdated(): void { @@ -11,22 +13,21 @@ export function dispatchTombstonesUpdated(): void {
}
/** Relay set for querying the current user's kind-5 events (aligned with login sync). */
export function buildDeletionRelayUrls(relayList: TRelayList | null | undefined): string[] {
const httpR = relayList?.httpRead ?? []
const httpW = relayList?.httpWrite ?? []
if (!relayList?.read?.length && !relayList?.write?.length && !httpR.length && !httpW.length) {
export function buildDeletionRelayUrls(
relayList: TRelayList | null | undefined,
cacheUrls: readonly string[] = []
): string[] {
const readInboxes = collectUserReadInboxUrls(relayList, cacheUrls)
const writeOutboxes = collectUserWriteOutboxUrls(relayList, cacheUrls)
if (readInboxes.length === 0 && writeOutboxes.length === 0) {
return Array.from(
new Set(PROFILE_RELAY_URLS.map((url) => normalizeUrl(url) || url).filter(Boolean))
).slice(0, 20)
}
const ws = relayList?.write ?? []
const rs = relayList?.read ?? []
return Array.from(
new Set([
...ws.map((url: string) => normalizeUrl(url) || url),
...rs.slice(0, 8).map((url: string) => normalizeUrl(url) || url),
...httpW.map((url: string) => normalizeHttpRelayUrl(url) || url),
...httpR.slice(0, 8).map((url: string) => normalizeHttpRelayUrl(url) || url),
...writeOutboxes,
...readInboxes.slice(0, 8),
...PROFILE_RELAY_URLS.map((url: string) => normalizeAnyRelayUrl(url) || url)
])
).slice(0, 20)

54
src/lib/viewer-read-inboxes.test.ts

@ -0,0 +1,54 @@ @@ -0,0 +1,54 @@
import { describe, expect, it, vi } from 'vitest'
import {
collectReadInboxUrlsFromRelayList,
collectUserReadInboxUrls,
collectRemoteReadInboxUrlsFromRelayList,
collectViewerReadInboxUrls
} from '@/lib/viewer-read-inboxes'
vi.mock('@/lib/private-relays', () => ({
getCacheRelayUrls: vi.fn(async () => ['ws://localhost:4869/'])
}))
describe('viewer read inboxes', () => {
const relayList = {
write: ['wss://outbox.example/'],
read: ['wss://inbox.example/'],
httpWrite: [],
httpRead: ['https://http-in.example/'],
originalRelays: [],
httpOriginalRelays: []
}
it('collectReadInboxUrlsFromRelayList merges http before ws (no cache layer)', () => {
expect(collectReadInboxUrlsFromRelayList(relayList)).toEqual([
'https://http-in.example/',
'wss://inbox.example/'
])
})
it('collectUserReadInboxUrls orders cache before http before ws', () => {
expect(collectUserReadInboxUrls(relayList, ['ws://127.0.0.1:4869'])).toEqual([
'ws://127.0.0.1:4869/',
'https://http-in.example/',
'wss://inbox.example/'
])
})
it('collectRemoteReadInboxUrlsFromRelayList drops LAN urls', () => {
expect(
collectRemoteReadInboxUrlsFromRelayList({
...relayList,
read: ['wss://inbox.example/', 'ws://127.0.0.1:4869/']
})
).toEqual(['https://http-in.example/', 'wss://inbox.example/'])
})
it('collectViewerReadInboxUrls loads cache from kind 10432', async () => {
await expect(collectViewerReadInboxUrls('ab'.repeat(32), relayList)).resolves.toEqual([
'ws://localhost:4869/',
'https://http-in.example/',
'wss://inbox.example/'
])
})
})

88
src/lib/viewer-read-inboxes.ts

@ -0,0 +1,88 @@ @@ -0,0 +1,88 @@
import { getCacheRelayUrls } from '@/lib/private-relays'
import { dedupeNormalizeRelayUrlsOrdered } from '@/lib/relay-url-priority'
import { isLocalNetworkUrl, normalizeAnyRelayUrl, normalizeHttpRelayUrl, normalizeUrl } from '@/lib/url'
import type { TRelayList } from '@/types'
export type ReadInboxSource = Pick<TRelayList, 'read' | 'httpRead'> | {
read?: string[]
httpRead?: string[]
}
function relayKey(url: string): string {
return (normalizeAnyRelayUrl(url) || url.trim()).toLowerCase()
}
/**
* Logged-in user's read inbox: kind 10432 cache, then kind 10243 HTTP read, then kind 10002 WS read.
* Pass `cacheUrls` when kind 10432 is stored separately (e.g. from {@link cacheRelayListEvent});
* those URLs are stripped from the WS layer so each relay appears once with cache-first ordering.
*/
export function collectUserReadInboxUrls(
relayList: ReadInboxSource | null | undefined,
cacheUrls: readonly string[] = [],
extraReadUrls: readonly string[] = []
): string[] {
const cache = cacheUrls
.map((u) => normalizeUrl(u) || u.trim())
.filter(Boolean)
const cacheKeys = new Set(cache.map(relayKey))
const http = (relayList?.httpRead ?? [])
.map((u) => normalizeHttpRelayUrl(u) || u)
.filter((u): u is string => !!u)
const ws = (relayList?.read ?? [])
.map((u) => normalizeUrl(u) || u)
.filter((u): u is string => !!u)
.filter((u) => cacheKeys.size === 0 || !cacheKeys.has(relayKey(u)))
return dedupeNormalizeRelayUrlsOrdered([...cache, ...http, ...ws, ...extraReadUrls])
}
/**
* Kind 10243 HTTP + kind 10002 WS read fields from a mailbox list (no kind 10432).
* Use for third-party authors; for the viewer prefer {@link collectUserReadInboxUrls}.
*/
export function collectReadInboxUrlsFromRelayList(
relayList: ReadInboxSource | null | undefined,
extraReadUrls: readonly string[] = []
): string[] {
return collectUserReadInboxUrls(relayList, [], extraReadUrls)
}
/** NIP-65 / 10243 inbox URLs for a recipient (drops other people's LAN/loopback). */
export function collectRemoteReadInboxUrlsFromRelayList(
relayList: ReadInboxSource | null | undefined,
extraReadUrls: readonly string[] = []
): string[] {
return collectReadInboxUrlsFromRelayList(relayList, extraReadUrls).filter(
(u) => !isLocalNetworkUrl(u)
)
}
/** @deprecated use {@link collectUserReadInboxUrls} */
export function collectReadInboxUrlsWithExtraCache(
relayList: ReadInboxSource | null | undefined,
cacheUrls: readonly string[],
extraReadUrls: readonly string[] = []
): string[] {
return collectUserReadInboxUrls(relayList, cacheUrls, extraReadUrls)
}
/**
* Full viewer read inbox: kind 10432 cache (IndexedDB) + kind 10243 HTTP + kind 10002 WS.
*/
export async function collectViewerReadInboxUrls(
pubkey: string,
relayList: ReadInboxSource | null | undefined,
extraReadUrls: readonly string[] = []
): Promise<string[]> {
const cache = await getCacheRelayUrls(pubkey)
return collectUserReadInboxUrls(relayList, cache, extraReadUrls)
}
/** True when the viewer has any configured read inbox (10002, 10243, or 10432 cache). */
export async function viewerHasReadInboxes(
pubkey: string,
relayList: ReadInboxSource | null | undefined
): Promise<boolean> {
const urls = await collectViewerReadInboxUrls(pubkey, relayList)
return urls.length > 0
}

44
src/lib/viewer-write-outboxes.test.ts

@ -0,0 +1,44 @@ @@ -0,0 +1,44 @@
import { describe, expect, it, vi } from 'vitest'
import {
collectUserWriteOutboxUrls,
collectViewerWriteOutboxUrls,
collectWriteOutboxUrlsFromRelayList
} from '@/lib/viewer-write-outboxes'
vi.mock('@/lib/private-relays', () => ({
getCacheRelayUrls: vi.fn(async () => ['ws://localhost:4869/'])
}))
describe('viewer write outboxes', () => {
const relayList = {
write: ['wss://nip65.example/'],
read: ['wss://inbox.example/'],
httpWrite: ['https://http-out.example/'],
httpRead: [],
originalRelays: [],
httpOriginalRelays: []
}
it('collectWriteOutboxUrlsFromRelayList merges http before ws (no cache layer)', () => {
expect(collectWriteOutboxUrlsFromRelayList(relayList)).toEqual([
'https://http-out.example/',
'wss://nip65.example/'
])
})
it('collectUserWriteOutboxUrls orders cache before http before ws', () => {
expect(collectUserWriteOutboxUrls(relayList, ['ws://127.0.0.1:4869'])).toEqual([
'ws://127.0.0.1:4869/',
'https://http-out.example/',
'wss://nip65.example/'
])
})
it('collectViewerWriteOutboxUrls loads cache from kind 10432', async () => {
await expect(collectViewerWriteOutboxUrls('ab'.repeat(32), relayList)).resolves.toEqual([
'ws://localhost:4869/',
'https://http-out.example/',
'wss://nip65.example/'
])
})
})

77
src/lib/viewer-write-outboxes.ts

@ -0,0 +1,77 @@ @@ -0,0 +1,77 @@
import { getCacheRelayUrls } from '@/lib/private-relays'
import { dedupeNormalizeRelayUrlsOrdered } from '@/lib/relay-url-priority'
import { normalizeAnyRelayUrl, normalizeHttpRelayUrl, normalizeUrl } from '@/lib/url'
import type { TRelayList } from '@/types'
export type WriteOutboxSource = Pick<TRelayList, 'write' | 'httpWrite'> | {
write?: string[]
httpWrite?: string[]
}
function relayKey(url: string): string {
return (normalizeAnyRelayUrl(url) || url.trim()).toLowerCase()
}
/**
* Logged-in user's write outbox: kind 10432 cache, then kind 10243 HTTP write, then kind 10002 WS write.
* Pass `cacheUrls` when kind 10432 is stored separately; those URLs are stripped from the WS layer.
*/
export function collectUserWriteOutboxUrls(
relayList: WriteOutboxSource | null | undefined,
cacheUrls: readonly string[] = [],
extraWriteUrls: readonly string[] = []
): string[] {
const cache = cacheUrls
.map((u) => normalizeUrl(u) || u.trim())
.filter(Boolean)
const cacheKeys = new Set(cache.map(relayKey))
const http = (relayList?.httpWrite ?? [])
.map((u) => normalizeHttpRelayUrl(u) || u)
.filter((u): u is string => !!u)
const ws = (relayList?.write ?? [])
.map((u) => normalizeUrl(u) || u)
.filter((u): u is string => !!u)
.filter((u) => cacheKeys.size === 0 || !cacheKeys.has(relayKey(u)))
return dedupeNormalizeRelayUrlsOrdered([...cache, ...http, ...ws, ...extraWriteUrls])
}
/**
* Kind 10243 HTTP + kind 10002 WS write fields from a mailbox list (no kind 10432).
* Use for third-party authors; for the viewer prefer {@link collectUserWriteOutboxUrls}.
*/
export function collectWriteOutboxUrlsFromRelayList(
relayList: WriteOutboxSource | null | undefined,
extraWriteUrls: readonly string[] = []
): string[] {
return collectUserWriteOutboxUrls(relayList, [], extraWriteUrls)
}
/** @deprecated use {@link collectUserWriteOutboxUrls} */
export function collectWriteOutboxUrlsWithExtraCache(
relayList: WriteOutboxSource | null | undefined,
cacheUrls: readonly string[],
extraWriteUrls: readonly string[] = []
): string[] {
return collectUserWriteOutboxUrls(relayList, cacheUrls, extraWriteUrls)
}
/**
* Full viewer publish outbox: kind 10432 cache (IndexedDB) + kind 10243 HTTP + kind 10002 WS.
*/
export async function collectViewerWriteOutboxUrls(
pubkey: string,
relayList: WriteOutboxSource | null | undefined,
extraWriteUrls: readonly string[] = []
): Promise<string[]> {
const cache = await getCacheRelayUrls(pubkey)
return collectUserWriteOutboxUrls(relayList, cache, extraWriteUrls)
}
/** True when the viewer has any configured write outbox (10002, 10243, or 10432 cache). */
export async function viewerHasWriteOutboxes(
pubkey: string,
relayList: WriteOutboxSource | null | undefined
): Promise<boolean> {
const urls = await collectViewerWriteOutboxUrls(pubkey, relayList)
return urls.length > 0
}

8
src/pages/primary/CalendarPrimaryPage.tsx

@ -8,7 +8,7 @@ import { @@ -8,7 +8,7 @@ import {
getLocalMonthRangeMs
} from '@/lib/calendar-event'
import { replaceableEventDedupeKey } from '@/lib/event'
import { getRelayUrlsWithFavoritesFastReadAndInbox, userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import { getRelayUrlsWithFavoritesFastReadAndInbox, userReadInboxUrls, userWriteOutboxUrls } from '@/lib/favorites-feed-relays'
import { setCalendarDayPanelEvents } from '@/lib/calendar-day-panel-cache'
import { toNote } from '@/lib/link'
import { cn } from '@/lib/utils'
@ -72,7 +72,7 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct @@ -72,7 +72,7 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct
ref
) {
const { t, i18n } = useTranslation()
const { relayList, pubkey } = useNostr()
const { relayList, cacheRelayListEvent, pubkey } = useNostr()
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const followList = useFollowListOptional()
const { navigateToNote } = useSmartNoteNavigation()
@ -121,9 +121,9 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct @@ -121,9 +121,9 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct
const base = getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays,
blockedRelays,
userReadRelaysWithHttp(relayList),
userReadInboxUrls(relayList, cacheRelayListEvent),
{
userWriteRelays: relayList?.write ?? [],
userWriteRelays: userWriteOutboxUrls(relayList, cacheRelayListEvent),
applySocialKindBlockedFilter: false
}
)

8
src/pages/primary/SpellsPage/CreateSpellDialog.tsx

@ -25,7 +25,7 @@ import { useNostr } from '@/providers/NostrProvider' @@ -25,7 +25,7 @@ import { useNostr } from '@/providers/NostrProvider'
import { showPublishingError, showSimplePublishSuccess } from '@/lib/publishing-feedback'
import { eventService } from '@/services/client.service'
import indexedDb from '@/services/indexed-db.service'
import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import { userReadInboxUrls, userWriteOutboxUrls } from '@/lib/favorites-feed-relays'
import { getRelaysForSpellCatalogSync } from '@/services/spell.service'
import { Info, Minus, Plus, X } from 'lucide-react'
import { useTranslation } from 'react-i18next'
@ -292,7 +292,7 @@ export default function CreateSpellDialog({ @@ -292,7 +292,7 @@ export default function CreateSpellDialog({
spellToClone?: NostrEvent | null
}) {
const { t } = useTranslation()
const { pubkey, publish, checkLogin, relayList } = useNostr()
const { pubkey, publish, checkLogin, relayList, cacheRelayListEvent } = useNostr()
const { addBookmark, removeBookmark } = useBookmarks()
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const useGlobalRelayBootstrap = useGlobalRelayBootstrapDefaults()
@ -326,8 +326,8 @@ export default function CreateSpellDialog({ @@ -326,8 +326,8 @@ export default function CreateSpellDialog({
const { draft, notices, pendingATags } = applyListEventToSpellDraft(base, ev)
setForm(draft)
setListImportNotices(notices)
const urls = getRelaysForSpellCatalogSync(favoriteRelays, blockedRelays, userReadRelaysWithHttp(relayList), {
userWriteRelays: relayList?.write ?? [],
const urls = getRelaysForSpellCatalogSync(favoriteRelays, blockedRelays, userReadInboxUrls(relayList, cacheRelayListEvent), {
userWriteRelays: userWriteOutboxUrls(relayList, cacheRelayListEvent),
useGlobalRelayBootstrap
})
if (pendingATags.length === 0) return

11
src/pages/primary/SpellsPage/ProfileInteractionsMap.tsx

@ -4,10 +4,7 @@ import { Button } from '@/components/ui/button' @@ -4,10 +4,7 @@ import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { Skeleton } from '@/components/ui/skeleton'
import { ExtendedKind } from '@/constants'
import {
getRelayUrlsWithFavoritesFastReadAndInbox,
userReadRelaysWithHttp
} from '@/lib/favorites-feed-relays'
import { getRelayUrlsWithFavoritesFastReadAndInbox, userReadInboxUrls, userWriteOutboxUrls } from '@/lib/favorites-feed-relays'
import { toProfile } from '@/lib/link'
import { formatPubkey } from '@/lib/pubkey'
import { cn } from '@/lib/utils'
@ -114,7 +111,7 @@ function compactCount(n: number): string { @@ -114,7 +111,7 @@ function compactCount(n: number): string {
export default function ProfileInteractionsMap({ pubkey, refreshKey }: Props) {
const { t } = useTranslation()
const { push } = useSecondaryPage()
const { relayList } = useNostr()
const { relayList, cacheRelayListEvent } = useNostr()
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const [cards, setCards] = useState<InteractionCard[]>([])
const [loading, setLoading] = useState(true)
@ -126,9 +123,9 @@ export default function ProfileInteractionsMap({ pubkey, refreshKey }: Props) { @@ -126,9 +123,9 @@ export default function ProfileInteractionsMap({ pubkey, refreshKey }: Props) {
getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays,
blockedRelays,
userReadRelaysWithHttp(relayList),
userReadInboxUrls(relayList, cacheRelayListEvent),
{
userWriteRelays: relayList?.write ?? [],
userWriteRelays: userWriteOutboxUrls(relayList, cacheRelayListEvent),
applySocialKindBlockedFilter: false
}
),

8
src/pages/primary/SpellsPage/RelayThreadHeatMap.tsx

@ -4,7 +4,7 @@ import { SimpleUserAvatar } from '@/components/UserAvatar' @@ -4,7 +4,7 @@ import { SimpleUserAvatar } from '@/components/UserAvatar'
import { ExtendedKind } from '@/constants'
import { eventPassesNoteListKindPicker } from '@/lib/feed-kind-filter'
import { filterEventsExcludingTombstones } from '@/lib/event'
import { getRelayUrlsWithFavoritesFastReadAndInbox, userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import { getRelayUrlsWithFavoritesFastReadAndInbox, userReadInboxUrls, userWriteOutboxUrls } from '@/lib/favorites-feed-relays'
import { toNote } from '@/lib/link'
import logger from '@/lib/logger'
import {
@ -100,7 +100,7 @@ export default function RelayThreadHeatMap({ followPubkeys, refreshKey }: Props) @@ -100,7 +100,7 @@ export default function RelayThreadHeatMap({ followPubkeys, refreshKey }: Props)
const { t } = useTranslation()
const { navigate: navigatePrimary } = usePrimaryPage()
const { navigateToNote } = useSmartNoteNavigation()
const { pubkey, relayList } = useNostr()
const { pubkey, relayList, cacheRelayListEvent } = useNostr()
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const { showKinds, showKind1OPs, showKind1Replies, showKind1111 } = useKindFilterOrDefaults()
@ -125,9 +125,9 @@ export default function RelayThreadHeatMap({ followPubkeys, refreshKey }: Props) @@ -125,9 +125,9 @@ export default function RelayThreadHeatMap({ followPubkeys, refreshKey }: Props)
getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays,
blockedRelays,
userReadRelaysWithHttp(relayList),
userReadInboxUrls(relayList, cacheRelayListEvent),
{
userWriteRelays: relayList?.write ?? [],
userWriteRelays: userWriteOutboxUrls(relayList, cacheRelayListEvent),
applySocialKindBlockedFilter: false
}
),

8
src/pages/primary/SpellsPage/TopicKeywordHeatMap.tsx

@ -4,7 +4,7 @@ import { ExtendedKind } from '@/constants' @@ -4,7 +4,7 @@ import { ExtendedKind } from '@/constants'
import { eventPassesNoteListKindPicker } from '@/lib/feed-kind-filter'
import { filterEventsExcludingTombstones } from '@/lib/event'
import { extractHashtagsFromContent, normalizeTopic } from '@/lib/discussion-topics'
import { getRelayUrlsWithFavoritesFastReadAndInbox, userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import { getRelayUrlsWithFavoritesFastReadAndInbox, userReadInboxUrls, userWriteOutboxUrls } from '@/lib/favorites-feed-relays'
import { toNoteList } from '@/lib/link'
import logger from '@/lib/logger'
import { useSmartHashtagNavigation } from '@/PageManager'
@ -115,7 +115,7 @@ type Props = { @@ -115,7 +115,7 @@ type Props = {
export default function TopicKeywordHeatMap({ refreshKey }: Props) {
const { t } = useTranslation()
const { navigateToHashtag } = useSmartHashtagNavigation()
const { relayList } = useNostr()
const { relayList, cacheRelayListEvent } = useNostr()
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const { showKinds, showKind1OPs, showKind1Replies, showKind1111 } = useKindFilterOrDefaults()
@ -124,9 +124,9 @@ export default function TopicKeywordHeatMap({ refreshKey }: Props) { @@ -124,9 +124,9 @@ export default function TopicKeywordHeatMap({ refreshKey }: Props) {
getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays,
blockedRelays,
userReadRelaysWithHttp(relayList),
userReadInboxUrls(relayList, cacheRelayListEvent),
{
userWriteRelays: relayList?.write ?? [],
userWriteRelays: userWriteOutboxUrls(relayList, cacheRelayListEvent),
applySocialKindBlockedFilter: false
}
),

12
src/pages/primary/SpellsPage/index.tsx

@ -34,7 +34,7 @@ import indexedDb from '@/services/indexed-db.service' @@ -34,7 +34,7 @@ import indexedDb from '@/services/indexed-db.service'
import { ExtendedKind, FIRST_RELAY_RESULT_GRACE_MS } from '@/constants'
import { filterEventsExcludingTombstones } from '@/lib/event'
import { normalizeHexPubkey } from '@/lib/pubkey'
import { getRelayUrlsWithFavoritesFastReadAndInbox, userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import { getRelayUrlsWithFavoritesFastReadAndInbox, userReadInboxUrls, userWriteOutboxUrls } from '@/lib/favorites-feed-relays'
import { TOMBSTONES_UPDATED_EVENT } from '@/lib/tombstone-events'
import {
buildSpellCatalogAuthors,
@ -80,6 +80,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -80,6 +80,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
pubkey,
account,
relayList,
cacheRelayListEvent,
attemptDelete,
bookmarkListEvent,
interestListEvent,
@ -236,6 +237,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -236,6 +237,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
selectedSpell,
pubkey,
relayList,
cacheRelayListEvent,
favoriteRelays,
blockedRelays,
notificationsFeedPubkey,
@ -264,8 +266,8 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -264,8 +266,8 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
const feedUrls = getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays,
blockedRelays,
userReadRelaysWithHttp(relayList),
{ userWriteRelays: relayList?.write ?? [] }
userReadInboxUrls(relayList, cacheRelayListEvent),
{ userWriteRelays: userWriteOutboxUrls(relayList, cacheRelayListEvent) }
)
if (!feedUrls.length) {
if (!cancelled) setFollowSetListEvents([])
@ -378,8 +380,8 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -378,8 +380,8 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
}
}
const urls = getRelaysForSpellCatalogSync(favoriteRelays, blockedRelays, userReadRelaysWithHttp(relayList), {
userWriteRelays: relayList?.write ?? [],
const urls = getRelaysForSpellCatalogSync(favoriteRelays, blockedRelays, userReadInboxUrls(relayList, cacheRelayListEvent), {
userWriteRelays: userWriteOutboxUrls(relayList, cacheRelayListEvent),
useGlobalRelayBootstrap
})
const catalogAuthors = buildSpellCatalogAuthors(pubkey, contacts)

36
src/pages/primary/SpellsPage/useSpellsPageFeed.ts

@ -8,7 +8,8 @@ import { normalizeUrl } from '@/lib/url' @@ -8,7 +8,8 @@ import { normalizeUrl } from '@/lib/url'
import {
augmentSubRequestsWithFavoritesFastReadAndInbox,
getRelayUrlsWithFavoritesFastReadAndInbox,
userReadRelaysWithHttp
userReadInboxUrls,
userWriteOutboxUrls
} from '@/lib/favorites-feed-relays'
import { stableSpellFeedFilterKey } from '@/lib/spell-feed-request-identity'
import { isUserInEventMentions } from '@/lib/event'
@ -73,15 +74,16 @@ function buildInboxShardFollowingSubRequests(args: { @@ -73,15 +74,16 @@ function buildInboxShardFollowingSubRequests(args: {
authors: string[]
favoriteRelays: string[]
blockedRelays: string[]
relayList: { read: string[]; write: string[] } | null | undefined
relayList: { read: string[]; write: string[]; httpRead?: string[]; httpWrite?: string[] } | null | undefined
cacheRelayListEvent?: Event | null
augment: (raw: TFeedSubRequest[]) => TFeedSubRequest[]
}): TFeedSubRequest[] {
const { authors, favoriteRelays, blockedRelays, relayList, augment } = args
const { authors, favoriteRelays, blockedRelays, relayList, cacheRelayListEvent, augment } = args
const feedUrls = getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays,
blockedRelays,
userReadRelaysWithHttp(relayList),
{ userWriteRelays: relayList?.write ?? [] }
userReadInboxUrls(relayList, cacheRelayListEvent),
{ userWriteRelays: userWriteOutboxUrls(relayList, cacheRelayListEvent) }
)
if (!feedUrls.length) return []
const capped = authors.slice(0, FOLLOWING_INBOX_SHARD_AUTHOR_CAP)
@ -113,7 +115,8 @@ export type UseSpellsPageFeedArgs = { @@ -113,7 +115,8 @@ export type UseSpellsPageFeedArgs = {
selectedFauxSpell: string | null
selectedSpell: Event | null
pubkey: string | null | undefined
relayList: { read: string[]; write: string[] } | null | undefined
relayList: { read: string[]; write: string[]; httpRead?: string[]; httpWrite?: string[] } | null | undefined
cacheRelayListEvent?: Event | null
favoriteRelays: string[]
blockedRelays: string[]
notificationsFeedPubkey: string | null
@ -135,6 +138,7 @@ export function useSpellsPageFeed(a: UseSpellsPageFeedArgs) { @@ -135,6 +138,7 @@ export function useSpellsPageFeed(a: UseSpellsPageFeedArgs) {
selectedSpell,
pubkey,
relayList,
cacheRelayListEvent,
favoriteRelays,
blockedRelays,
notificationsFeedPubkey,
@ -153,11 +157,9 @@ export function useSpellsPageFeed(a: UseSpellsPageFeedArgs) { @@ -153,11 +157,9 @@ export function useSpellsPageFeed(a: UseSpellsPageFeedArgs) {
const hideRepliesFollowing = useNoteListHideReplies()
const [followingSubRequests, setFollowingSubRequests] = useState<TFeedSubRequest[]>([])
const normalizedReadSorted = relayList
? [...relayList.read].map((u) => normalizeUrl(u) || u).filter(Boolean).sort()
: []
const normalizedReadSorted = relayList ? [...userReadInboxUrls(relayList, cacheRelayListEvent)].sort() : []
const normalizedWriteSorted = relayList
? [...relayList.write].map((u) => normalizeUrl(u) || u).filter(Boolean).sort()
? [...userWriteOutboxUrls(relayList, cacheRelayListEvent)].sort()
: []
const relayMailboxStableKey =
@ -220,8 +222,8 @@ export function useSpellsPageFeed(a: UseSpellsPageFeedArgs) { @@ -220,8 +222,8 @@ export function useSpellsPageFeed(a: UseSpellsPageFeedArgs) {
raw,
favoriteRelays,
blockedRelays,
userReadRelaysWithHttp(relayList),
{ userWriteRelays: relayList?.write ?? [] }
userReadInboxUrls(relayList, cacheRelayListEvent),
{ userWriteRelays: userWriteOutboxUrls(relayList, cacheRelayListEvent) }
)
try {
if (selectedFauxSpell === 'following') {
@ -232,6 +234,7 @@ export function useSpellsPageFeed(a: UseSpellsPageFeedArgs) { @@ -232,6 +234,7 @@ export function useSpellsPageFeed(a: UseSpellsPageFeedArgs) {
favoriteRelays,
blockedRelays,
relayList,
cacheRelayListEvent,
augment
}
const syncProvisional = buildInboxShardFollowingSubRequests({
@ -294,6 +297,7 @@ export function useSpellsPageFeed(a: UseSpellsPageFeedArgs) { @@ -294,6 +297,7 @@ export function useSpellsPageFeed(a: UseSpellsPageFeedArgs) {
favoriteRelays,
blockedRelays,
relayList,
cacheRelayListEvent,
augment
})
if (!cancelled && syncReq.length > 0) setFollowingSubRequests(syncReq)
@ -310,6 +314,7 @@ export function useSpellsPageFeed(a: UseSpellsPageFeedArgs) { @@ -310,6 +314,7 @@ export function useSpellsPageFeed(a: UseSpellsPageFeedArgs) {
favoriteRelays,
blockedRelays,
relayList,
cacheRelayListEvent,
augment
})
if (!cancelled) setFollowingSubRequests(req)
@ -329,6 +334,7 @@ export function useSpellsPageFeed(a: UseSpellsPageFeedArgs) { @@ -329,6 +334,7 @@ export function useSpellsPageFeed(a: UseSpellsPageFeedArgs) {
sortedFavoriteRelaysKey,
sortedBlockedRelaysKey,
relayMailboxStableKey,
cacheRelayListEvent,
followSetCatalogLoading,
followSetListStableKey,
followListEvent?.id,
@ -399,9 +405,9 @@ export function useSpellsPageFeed(a: UseSpellsPageFeedArgs) { @@ -399,9 +405,9 @@ export function useSpellsPageFeed(a: UseSpellsPageFeedArgs) {
getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays,
blockedRelays,
userReadRelaysWithHttp(relayList),
userReadInboxUrls(relayList, cacheRelayListEvent),
{
userWriteRelays: relayList?.write ?? [],
userWriteRelays: userWriteOutboxUrls(relayList, cacheRelayListEvent),
applySocialKindBlockedFilter: fauxSpellSkipSocialKindBlocked ? false : undefined
}
)
@ -478,7 +484,7 @@ export function useSpellsPageFeed(a: UseSpellsPageFeedArgs) { @@ -478,7 +484,7 @@ export function useSpellsPageFeed(a: UseSpellsPageFeedArgs) {
const spellSubRequests = useMemo<TFeedSubRequest[]>(() => {
if (!selectedSpell) return []
const relayListWrite = relayList?.write ?? []
const relayListWrite = userWriteOutboxUrls(relayList, cacheRelayListEvent)
const ctx = { pubkey: pubkey ?? null, contacts }
const filter = spellEventToFilter(selectedSpell, ctx)
if (!filter) return []

13
src/pages/secondary/EmojiSetsSettingsPage/index.tsx

@ -32,10 +32,7 @@ import { randomString } from '@/lib/random' @@ -32,10 +32,7 @@ import { randomString } from '@/lib/random'
import { showPublishingError } from '@/lib/publishing-feedback'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { usePrimaryNoteView } from '@/contexts/primary-note-view-context'
import {
getRelayUrlsWithFavoritesFastReadAndInbox,
userReadRelaysWithHttp
} from '@/lib/favorites-feed-relays'
import { getRelayUrlsWithFavoritesFastReadAndInbox, userReadInboxUrls, userWriteOutboxUrls } from '@/lib/favorites-feed-relays'
import { createEmojiSetDraftEvent } from '@/lib/draft-event'
import { filterEventsExcludingTombstones } from '@/lib/event'
import logger from '@/lib/logger'
@ -63,7 +60,7 @@ const EMOJI_SET_FETCH_OPTS = { @@ -63,7 +60,7 @@ const EMOJI_SET_FETCH_OPTS = {
const EmojiSetsSettingsPage = forwardRef(
({ index, hideTitlebar = false }: { index?: number; hideTitlebar?: boolean }, ref) => {
const { t } = useTranslation()
const { pubkey, account, publish, attemptDelete, checkLogin, relayList, userEmojiListEvent, profileEvent } =
const { pubkey, account, publish, attemptDelete, checkLogin, relayList, cacheRelayListEvent, userEmojiListEvent, profileEvent } =
useNostr()
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const [lists, setLists] = useState<Event[]>([])
@ -91,11 +88,11 @@ const EmojiSetsSettingsPage = forwardRef( @@ -91,11 +88,11 @@ const EmojiSetsSettingsPage = forwardRef(
const feedUrls = getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays,
blockedRelays,
userReadRelaysWithHttp(relayList),
{ userWriteRelays: relayList?.write ?? [] }
userReadInboxUrls(relayList, cacheRelayListEvent),
{ userWriteRelays: userWriteOutboxUrls(relayList, cacheRelayListEvent) }
)
return appendCuratedReadOnlyRelays(feedUrls, blockedRelays)
}, [favoriteRelays, blockedRelays, relayList])
}, [favoriteRelays, blockedRelays, relayList, cacheRelayListEvent])
const loadLists = useCallback(async () => {
if (!pubkey) {

11
src/pages/secondary/FollowSetsSettingsPage/index.tsx

@ -34,10 +34,7 @@ import { randomString } from '@/lib/random' @@ -34,10 +34,7 @@ import { randomString } from '@/lib/random'
import { showPublishingError } from '@/lib/publishing-feedback'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { usePrimaryNoteView } from '@/contexts/primary-note-view-context'
import {
getRelayUrlsWithFavoritesFastReadAndInbox,
userReadRelaysWithHttp
} from '@/lib/favorites-feed-relays'
import { getRelayUrlsWithFavoritesFastReadAndInbox, userReadInboxUrls, userWriteOutboxUrls } from '@/lib/favorites-feed-relays'
import { createFollowSetDraftEvent } from '@/lib/draft-event'
import { filterEventsExcludingTombstones } from '@/lib/event'
import logger from '@/lib/logger'
@ -62,7 +59,7 @@ const FOLLOW_SET_FETCH_OPTS = { @@ -62,7 +59,7 @@ const FOLLOW_SET_FETCH_OPTS = {
const FollowSetsSettingsPage = forwardRef(
({ index, hideTitlebar = false }: { index?: number; hideTitlebar?: boolean }, ref) => {
const { t } = useTranslation()
const { pubkey, account, publish, attemptDelete, checkLogin, relayList } = useNostr()
const { pubkey, account, publish, attemptDelete, checkLogin, relayList, cacheRelayListEvent } = useNostr()
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const [lists, setLists] = useState<Event[]>([])
const [loading, setLoading] = useState(true)
@ -87,8 +84,8 @@ const FollowSetsSettingsPage = forwardRef( @@ -87,8 +84,8 @@ const FollowSetsSettingsPage = forwardRef(
const feedUrls = getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays,
blockedRelays,
userReadRelaysWithHttp(relayList),
{ userWriteRelays: relayList?.write ?? [] }
userReadInboxUrls(relayList, cacheRelayListEvent),
{ userWriteRelays: userWriteOutboxUrls(relayList, cacheRelayListEvent) }
)
return appendCuratedReadOnlyRelays(feedUrls, blockedRelays)
}, [favoriteRelays, blockedRelays, relayList])

21
src/pages/secondary/NoteListPage/index.tsx

@ -13,7 +13,8 @@ import { @@ -13,7 +13,8 @@ import {
import {
augmentSubRequestsWithFavoritesFastReadAndInbox,
getRelayUrlsWithFavoritesFastReadAndInbox,
userReadRelaysWithHttp
userReadInboxUrls,
userWriteOutboxUrls
} from '@/lib/favorites-feed-relays'
import { useGlobalRelayBootstrapDefaults } from '@/hooks/use-global-relay-bootstrap-defaults'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
@ -47,7 +48,7 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid @@ -47,7 +48,7 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid
const feedRef = useRef<TNoteListRef>(null)
const bumpFeed = useCallback(() => feedRef.current?.refresh(), [])
const { push } = useSecondaryPage()
const { relayList, pubkey } = useNostr()
const { relayList, cacheRelayListEvent, pubkey } = useNostr()
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const useGlobalRelayBootstrap = useGlobalRelayBootstrapDefaults()
const interestList = useInterestListOptional()
@ -111,7 +112,7 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid @@ -111,7 +112,7 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid
.map((k) => parseInt(k))
.filter((k) => !isNaN(k))
const readUrlOpts = {
userWriteRelays: relayList?.write ?? [],
userWriteRelays: userWriteOutboxUrls(relayList, cacheRelayListEvent),
applySocialKindBlockedFilter: kinds.length === 0 || kinds.some(isSocialKindBlockedKind),
useGlobalFavoriteDefaults: useGlobalRelayBootstrap,
includeGlobalFastRead: useGlobalRelayBootstrap
@ -124,7 +125,7 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid @@ -124,7 +125,7 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid
const relayUrls = getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays,
blockedRelays,
userReadRelaysWithHttp(relayList),
userReadInboxUrls(relayList, cacheRelayListEvent),
readUrlOpts
)
const mergedSearchKinds = Array.from(
@ -166,7 +167,7 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid @@ -166,7 +167,7 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid
urls: getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays,
blockedRelays,
userReadRelaysWithHttp(relayList),
userReadInboxUrls(relayList, cacheRelayListEvent),
readUrlOpts
)
}
@ -209,8 +210,8 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid @@ -209,8 +210,8 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid
urls: getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays,
blockedRelays,
userReadRelaysWithHttp(relayList),
{ userWriteRelays: relayList?.write ?? [], useGlobalFavoriteDefaults: useGlobalRelayBootstrap, includeGlobalFastRead: useGlobalRelayBootstrap }
userReadInboxUrls(relayList, cacheRelayListEvent),
{ userWriteRelays: userWriteOutboxUrls(relayList, cacheRelayListEvent), useGlobalFavoriteDefaults: useGlobalRelayBootstrap, includeGlobalFastRead: useGlobalRelayBootstrap }
)
}
])
@ -241,9 +242,9 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid @@ -241,9 +242,9 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid
raw,
favoriteRelays,
blockedRelays,
userReadRelaysWithHttp(relayList),
userReadInboxUrls(relayList, cacheRelayListEvent),
{
userWriteRelays: relayList?.write ?? [],
userWriteRelays: userWriteOutboxUrls(relayList, cacheRelayListEvent),
useGlobalFavoriteDefaults: useGlobalRelayBootstrap,
includeGlobalFastRead: useGlobalRelayBootstrap
}
@ -272,7 +273,7 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid @@ -272,7 +273,7 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid
const relayUrls = getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays,
blockedRelays,
userReadRelaysWithHttp(relayList),
userReadInboxUrls(relayList, cacheRelayListEvent),
readUrlOpts
)
const mergedReqKinds = Array.from(

11
src/pages/secondary/RelayReviewsPage/index.tsx

@ -4,10 +4,7 @@ import { RefreshButton } from '@/components/RefreshButton' @@ -4,10 +4,7 @@ import { RefreshButton } from '@/components/RefreshButton'
import { ExtendedKind } from '@/constants'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { usePrimaryNoteView } from '@/contexts/primary-note-view-context'
import {
getRelayUrlsWithFavoritesFastReadAndInbox,
userReadRelaysWithHttp
} from '@/lib/favorites-feed-relays'
import { getRelayUrlsWithFavoritesFastReadAndInbox, userReadInboxUrls } from '@/lib/favorites-feed-relays'
import { relayReviewDTagsForRelayUrl, relayReviewsFeedSnapshotKey } from '@/lib/relay-review-feed'
import { normalizeRelayUrlForPage, simplifyUrl } from '@/lib/url'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
@ -24,7 +21,7 @@ const RelayReviewsPage = forwardRef(({ url, index, hideTitlebar = false }: { url @@ -24,7 +21,7 @@ const RelayReviewsPage = forwardRef(({ url, index, hideTitlebar = false }: { url
const { registerPrimaryPanelRefresh } = usePrimaryNoteView()
const feedRef = useRef<TNoteListRef>(null)
const bumpFeed = useCallback(() => feedRef.current?.refresh(), [])
const { relayList } = useNostr()
const { relayList, cacheRelayListEvent } = useNostr()
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
useEffect(() => {
@ -52,10 +49,10 @@ const RelayReviewsPage = forwardRef(({ url, index, hideTitlebar = false }: { url @@ -52,10 +49,10 @@ const RelayReviewsPage = forwardRef(({ url, index, hideTitlebar = false }: { url
const base = getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays,
blockedRelays,
userReadRelaysWithHttp(relayList)
userReadInboxUrls(relayList, cacheRelayListEvent)
)
return [...new Set([normalizedUrl, ...base])]
}, [normalizedUrl, favoriteRelays, blockedRelays, relayList])
}, [normalizedUrl, favoriteRelays, blockedRelays, relayList, cacheRelayListEvent])
const reviewsSubRequests = useMemo<TFeedSubRequest[]>(() => {
if (!normalizedUrl || relayReviewDTags.length === 0) return []
return [

42
src/providers/FeedProvider.tsx

@ -1,12 +1,14 @@ @@ -1,12 +1,14 @@
import { DEFAULT_FAVORITE_RELAYS } from '@/constants'
import { feedRelayPolicyUrls } from '@/features/feed/relay-policy'
import { getRelayListFromEvent, getHttpRelayListFromEvent } from '@/lib/event-metadata'
import { buildAllFavoritesFeedRelayUrls, stripNostrLandAggrFromRelayUrls } from '@/lib/home-feed-relays'
import logger from '@/lib/logger'
import {
syncViewerRelayStackNostrLandAggrEligible,
urlsForViewerNostrLandAggrEligibilitySync
} from '@/lib/nostr-land-relay-eligibility'
import { collectUserReadInboxUrls } from '@/lib/viewer-read-inboxes'
import { collectUserWriteOutboxUrls } from '@/lib/viewer-write-outboxes'
import { getCacheRelayUrlsFromEvent } from '@/lib/private-relays'
import { normalizeAnyRelayUrl } from '@/lib/url'
import { viewerUsesGlobalRelayDefaults } from '@/lib/viewer-relay-defaults'
import { buildWispTrendingNotesRelayUrl } from '@/lib/wisp-trending-relay'
@ -54,7 +56,7 @@ function buildHomeReplyFeedRelayUrls( @@ -54,7 +56,7 @@ function buildHomeReplyFeedRelayUrls(
}
export function FeedProvider({ children }: { children: ReactNode }) {
const { isInitialized, relayList, cacheRelayListEvent, httpRelayListEvent, pubkey } = useNostr()
const { isInitialized, relayList, cacheRelayListEvent, pubkey } = useNostr()
const { favoriteRelays, blockedRelays, relaySets } = useFavoriteRelays()
const useGlobalRelayDefaults = useMemo(
@ -80,35 +82,33 @@ export function FeedProvider({ children }: { children: ReactNode }) { @@ -80,35 +82,33 @@ export function FeedProvider({ children }: { children: ReactNode }) {
/** Read-side layers merged into {@link replyRelayUrls}; {@link outboxRelayUrls} is only for aggr eligibility sync. */
const replyExtraRelayLayers = useMemo(() => {
const cacheRelayUrls: string[] = []
if (cacheRelayListEvent) {
const list = getRelayListFromEvent(cacheRelayListEvent, blockedRelays, {
globalReadWriteFallback: useGlobalRelayDefaults
})
cacheRelayUrls.push(...list.read)
}
const cacheRelayUrls = getCacheRelayUrlsFromEvent(cacheRelayListEvent)
const httpRelayUrls: string[] = [...(relayList?.httpRead ?? [])]
if (httpRelayListEvent) {
const list = getHttpRelayListFromEvent(httpRelayListEvent, blockedRelays)
httpRelayUrls.push(...list.httpRead)
}
const hasReadMailbox =
cacheRelayUrls.length > 0 ||
(relayList?.read?.length ?? 0) > 0 ||
(relayList?.httpRead?.length ?? 0) > 0
const hasWriteMailbox =
cacheRelayUrls.length > 0 ||
(relayList?.write?.length ?? 0) > 0 ||
(relayList?.httpWrite?.length ?? 0) > 0
return {
inboxRelayUrls: relayList?.read?.length
? relayList.read
inboxRelayUrls: hasReadMailbox
? collectUserReadInboxUrls(relayList, cacheRelayUrls)
: useGlobalRelayDefaults
? DEFAULT_FAVORITE_RELAYS
: [],
outboxRelayUrls: relayList?.write?.length
? relayList.write
outboxRelayUrls: hasWriteMailbox
? collectUserWriteOutboxUrls(relayList, cacheRelayUrls)
: useGlobalRelayDefaults
? DEFAULT_FAVORITE_RELAYS
: [],
cacheRelayUrls,
httpRelayUrls
/** Kept for feed-layer identity / aggr sync; URLs are merged into inbox/outbox above. */
cacheRelayUrls: [] as string[],
httpRelayUrls: [] as string[]
}
}, [relayList, cacheRelayListEvent, httpRelayListEvent, blockedRelays, useGlobalRelayDefaults])
}, [relayList, cacheRelayListEvent, useGlobalRelayDefaults])
/** Default relays immediately so feeds / sidebar REQ never wait on Nostr session restore. */
const [relayUrls, setRelayUrls] = useState<string[]>(() =>

14
src/providers/LiveActivitiesProvider.tsx

@ -8,7 +8,7 @@ import { @@ -8,7 +8,7 @@ import {
resolveParentSpacesForLiveActivities,
type TLiveActivityItem
} from '@/lib/live-activities'
import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import { userReadInboxUrls, userWriteOutboxUrls } from '@/lib/favorites-feed-relays'
import logger from '@/lib/logger'
import { viewerUsesGlobalRelayDefaults } from '@/lib/viewer-relay-defaults'
import client from '@/services/client.service'
@ -22,7 +22,7 @@ import { useNostr } from './NostrProvider' @@ -22,7 +22,7 @@ import { useNostr } from './NostrProvider'
import { useUserPreferencesOptional } from './UserPreferencesProvider'
export function LiveActivitiesProvider({ children }: { children: React.ReactNode }) {
const { pubkey, relayList, isInitialized, isAccountSessionHydrating } = useNostr()
const { pubkey, relayList, cacheRelayListEvent, isInitialized, isAccountSessionHydrating } = useNostr()
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const followListCtx = useFollowListOptional()
const followings = followListCtx?.followings ?? []
@ -50,8 +50,14 @@ export function LiveActivitiesProvider({ children }: { children: React.ReactNode @@ -50,8 +50,14 @@ export function LiveActivitiesProvider({ children }: { children: React.ReactNode
/** Collapse boot + session-prewarm + StrictMode into one network pass. */
const LIVE_ACTIVITIES_MIN_REFRESH_GAP_MS = 8_000
const relayRead = useMemo(() => userReadRelaysWithHttp(relayList), [relayList])
const relayWrite = relayList?.write ?? []
const relayRead = useMemo(
() => userReadInboxUrls(relayList, cacheRelayListEvent),
[relayList, cacheRelayListEvent]
)
const relayWrite = useMemo(
() => userWriteOutboxUrls(relayList, cacheRelayListEvent),
[relayList, cacheRelayListEvent]
)
const refresh = useCallback(async () => {
if (!showLiveActivitiesBanner) {

1
src/providers/NostrProvider/index.tsx

@ -1844,6 +1844,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -1844,6 +1844,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
await indexedDb.putReplaceableEvent(httpRelayEvent)
if (account?.pubkey) {
client.clearRelayListCache(account.pubkey)
await client.syncViewerPersonalRelayKeys(account.pubkey)
}
setHttpRelayListEvent(httpRelayEvent)
const mergedRelayList = await client.fetchRelayList(account?.pubkey || '')

179
src/services/client.service.ts

@ -36,6 +36,17 @@ import { @@ -36,6 +36,17 @@ import {
} from '@/constants'
import { getCacheRelayUrls } from '@/lib/private-relays'
import {
collectReadInboxUrlsFromRelayList,
collectRemoteReadInboxUrlsFromRelayList,
collectUserReadInboxUrls,
collectViewerReadInboxUrls
} from '@/lib/viewer-read-inboxes'
import {
collectUserWriteOutboxUrls,
collectViewerWriteOutboxUrls,
collectWriteOutboxUrlsFromRelayList
} from '@/lib/viewer-write-outboxes'
import {
buildPersonalRelayKeySet,
sanitizeRelayUrlsForFetch,
@ -141,8 +152,7 @@ import { filterRelaysForEventPublish } from '@/lib/relay-publish-filter' @@ -141,8 +152,7 @@ import { filterRelaysForEventPublish } from '@/lib/relay-publish-filter'
import { getPaymentAttestationTargetId } from '@/lib/superchat'
import {
buildPublicMessagePublishRelayUrls,
collectRecipientInboxUrls,
collectSenderOutboxUrls
collectRecipientInboxUrls
} from '@/lib/public-message-publish-relays'
import { buildPrioritizedWriteRelayUrls, dedupeNormalizeRelayUrlsOrdered } from '@/lib/relay-url-priority'
import {
@ -173,6 +183,7 @@ import { @@ -173,6 +183,7 @@ import {
isWebsocketUrl,
normalizeAnyRelayUrl,
normalizeHttpRelayUrl,
normalizeRelayUrlByScheme,
normalizeUrl,
simplifyUrl,
urlMatchesConfiguredHttpIndexRelay
@ -626,12 +637,11 @@ class ClientService extends EventTarget { @@ -626,12 +637,11 @@ class ClientService extends EventTarget {
const extra: string[] = []
if (pubkey) {
const rl = await this.peekRelayListFromStorage(pubkey)
extra.push(
...(rl.read ?? []),
...(rl.write ?? []),
...(rl.httpRead ?? []),
...(rl.httpWrite ?? [])
)
const [readInboxes, writeOutboxes] = await Promise.all([
collectViewerReadInboxUrls(pubkey, rl),
collectViewerWriteOutboxUrls(pubkey, rl)
])
extra.push(...readInboxes, ...writeOutboxes)
}
await preloadGifsIntoIdbCache(pubkey, extra)
}
@ -752,6 +762,35 @@ class ClientService extends EventTarget { @@ -752,6 +762,35 @@ class ClientService extends EventTarget {
return { all, httpIndexBases, cacheRelayEvent }
}
/**
* Kind 10243 bases used to route publish targets through the HTTP index API (not WebSocket).
* Refreshes from IndexedDB when the batch includes https targets so reactions/posts work right
* after saving kind 10243 without waiting for a full account re-sync.
*/
private async resolveViewerHttpIndexBasesForPublish(
publishTargetUrls: readonly string[]
): Promise<string[]> {
const hasHttpTarget = publishTargetUrls.some((u) => isKind10243HttpRelayTagUrl(u.trim()))
if (!hasHttpTarget) return this.viewerHttpIndexRelayBases
const pk = this.pubkey?.trim()
if (!pk) return this.viewerHttpIndexRelayBases
try {
const rl = await this.peekRelayListFromStorage(pk)
const fresh = [...(rl.httpRead ?? []), ...(rl.httpWrite ?? [])]
.map((u) => normalizeHttpRelayUrl(u) || u)
.filter(Boolean)
if (fresh.length > 0) {
this.viewerHttpIndexRelayBases = fresh
return fresh
}
} catch {
/* keep session cache */
}
return this.viewerHttpIndexRelayBases
}
/** Kind 10012 + embedded NIP-51 relay sets from IndexedDB only (no network). */
async fetchFavoriteRelaysFromStorage(pubkey: string): Promise<string[]> {
try {
@ -858,48 +897,20 @@ class ClientService extends EventTarget { @@ -858,48 +897,20 @@ class ClientService extends EventTarget {
const socialKindBlockedSet = new Set(SOCIAL_KIND_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u))
return dedupeNormalizeRelayUrlsOrdered(
filterRelaysForEventPublish(relays, event.kind).filter((url) => {
const n = normalizeAnyRelayUrl(url) || url
const n = normalizeRelayUrlByScheme(url) || url
if (isSocialKindBlockedKind(event.kind) && socialKindBlockedSet.has(n)) return false
return true
})
)
}
/**
* Author kind 0 / 10133 publish: NIP-65 WS outbox + HTTP write (10243) + cache relays (10432).
* {@link fetchRelayList} usually merges cache into `write`; this also appends 10432 tags when missing.
*/
private async resolveFullMailboxWriteUrlsForPublish(
pubkey: string,
relayList: TRelayList
): Promise<string[]> {
const ws = (relayList.write ?? [])
.map((u) => normalizeUrl(u) || u)
.filter((u): u is string => !!u)
const http = (relayList.httpWrite ?? [])
.map((u) => normalizeHttpRelayUrl(u) || u)
.filter((u): u is string => !!u)
let merged = dedupeNormalizeRelayUrlsOrdered([...http, ...ws])
try {
const cache = await getCacheRelayUrls(pubkey)
if (cache.length > 0) {
merged = dedupeNormalizeRelayUrlsOrdered([...merged, ...cache])
}
} catch {
/* ignore */
}
return merged
}
private relayListHasWriteUrls(relayList: TRelayList): boolean {
return (relayList.write?.length ?? 0) > 0 || (relayList.httpWrite?.length ?? 0) > 0
return collectUserWriteOutboxUrls(relayList).length > 0
}
private relayListUsableForInboxOrdering(relayList: TRelayList): boolean {
return (
this.relayListHasWriteUrls(relayList) ||
(relayList.read?.length ?? 0) > 0 ||
(relayList.httpRead?.length ?? 0) > 0
this.relayListHasWriteUrls(relayList) || collectUserReadInboxUrls(relayList).length > 0
)
}
@ -914,23 +925,12 @@ class ClientService extends EventTarget { @@ -914,23 +925,12 @@ class ClientService extends EventTarget {
return this.fetchRelayListWithPublishTimeout(pubkey)
}
/** NIP-65 `write` URLs for `event.pubkey`, filtered for publish (no read-only / social-kind blocks). */
/** NIP-65 / 10243 / 10432 write outboxes for `event.pubkey`, filtered for publish. */
private async getUserOutboxRelayUrlsForPublish(event: NEvent): Promise<string[]> {
try {
const relayList = await this.peekOrFetchRelayListForPublish(event.pubkey)
if (!this.relayListHasWriteUrls(relayList)) {
return []
}
const raw = isAuthorProfileMetadataPublishKind(event.kind)
? await this.resolveFullMailboxWriteUrlsForPublish(event.pubkey, relayList)
: dedupeNormalizeRelayUrlsOrdered([
...(relayList.httpWrite ?? [])
.map((u) => normalizeHttpRelayUrl(u) || u)
.filter((u): u is string => !!u),
...(relayList.write ?? [])
.map((u) => normalizeUrl(u) || u)
.filter((u): u is string => !!u)
])
const raw = await collectViewerWriteOutboxUrls(event.pubkey, relayList)
if (raw.length === 0) return []
return this.filterPublishingRelays(raw, event)
} catch {
return []
@ -942,7 +942,7 @@ class ClientService extends EventTarget { @@ -942,7 +942,7 @@ class ClientService extends EventTarget {
userOutboxUrls: string[],
relayStatuses: { url: string; success: boolean; error?: string }[]
): Promise<void> {
const norm = (u: string) => normalizeAnyRelayUrl(u) || u
const norm = (u: string) => normalizeRelayUrlByScheme(u) || u
const hadSuccess = new Set<string>()
for (const r of relayStatuses) {
if (r.success) hadSuccess.add(norm(r.url))
@ -1191,15 +1191,8 @@ class ClientService extends EventTarget { @@ -1191,15 +1191,8 @@ class ClientService extends EventTarget {
}
// For Report events, always include user's write relays first, then add seen relays if they're write-capable
if (event.kind === kinds.Report) {
// Start with user's write relays (outboxes) - these are the primary targets for reports
const relayList = await this.fetchRelayListWithPublishTimeout(event.pubkey)
const reportHttpWrites = (relayList?.httpWrite ?? [])
.map((url) => normalizeHttpRelayUrl(url) || url)
.filter((u): u is string => !!u)
const reportWsWrites = (relayList?.write ?? [])
.map((url) => normalizeUrl(url) || url)
.filter((u): u is string => !!u)
const userWriteRelays = dedupeNormalizeRelayUrlsOrdered([...reportHttpWrites, ...reportWsWrites])
const userWriteRelays = await collectViewerWriteOutboxUrls(event.pubkey, relayList)
// Get seen relays where the reported event was found
const targetEventId = event.tags.find(tagNameEquals('e'))?.[1]
@ -1209,9 +1202,9 @@ class ClientService extends EventTarget { @@ -1209,9 +1202,9 @@ class ClientService extends EventTarget {
const allSeenRelays = this.getSeenEventRelayUrls(targetEventId)
// Filter seen relays: only include those that are in user's write list
// This ensures we don't try to publish to read-only relays
const userWriteRelaySet = new Set(userWriteRelays.map(url => normalizeAnyRelayUrl(url) || url))
const userWriteRelaySet = new Set(userWriteRelays.map((url) => normalizeRelayUrlByScheme(url) || url))
seenRelays.push(...allSeenRelays.filter(url => {
const normalized = normalizeAnyRelayUrl(url) || url
const normalized = normalizeRelayUrlByScheme(url) || url
return userWriteRelaySet.has(normalized)
}))
}
@ -1273,7 +1266,7 @@ class ClientService extends EventTarget { @@ -1273,7 +1266,7 @@ class ClientService extends EventTarget {
this.fetchRelayListWithPublishTimeout(event.pubkey),
recipientListsPromise
])
const authorWrite = collectSenderOutboxUrls(authorRelayList)
const authorWrite = await collectViewerWriteOutboxUrls(event.pubkey, authorRelayList)
const recipientRead = dedupeNormalizeRelayUrlsOrdered(
recipientRelayLists.flatMap((rl) => collectRecipientInboxUrls(rl))
)
@ -1303,7 +1296,7 @@ class ClientService extends EventTarget { @@ -1303,7 +1296,7 @@ class ClientService extends EventTarget {
? this.fetchRelayListsWithPublishTimeout(senderPubkeys)
: Promise.resolve([] as TRelayList[])
])
const authorWrite = collectSenderOutboxUrls(authorRelayList)
const authorWrite = await collectViewerWriteOutboxUrls(event.pubkey, authorRelayList)
const authorRead = collectRecipientInboxUrls(authorRelayList)
const senderInboxes = dedupeNormalizeRelayUrlsOrdered(
senderRelayLists.flatMap((rl) => collectRecipientInboxUrls(rl))
@ -1351,16 +1344,10 @@ class ClientService extends EventTarget { @@ -1351,16 +1344,10 @@ class ClientService extends EventTarget {
})
spellRelayList = this.emptyRelayListForPublish()
}
const spellHttpWrites = (spellRelayList?.httpWrite ?? [])
.map((url) => normalizeHttpRelayUrl(url))
.filter((url): url is string => !!url)
const spellWsWrites = (spellRelayList?.write ?? [])
.map((url) => normalizeUrl(url))
.filter((url): url is string => !!url)
const normalizedWrite = dedupeNormalizeRelayUrlsOrdered([...spellHttpWrites, ...spellWsWrites])
const spellWriteFilteredRaw = await collectViewerWriteOutboxUrls(event.pubkey, spellRelayList)
const readOnlySet = new Set(READ_ONLY_RELAY_URLS.map((u) => normalizeUrl(u) || u))
const spellWriteFiltered = normalizedWrite.filter((url) => {
const n = normalizeAnyRelayUrl(url) || url
const spellWriteFiltered = spellWriteFilteredRaw.filter((url) => {
const n = normalizeRelayUrlByScheme(url) || url
return !readOnlySet.has(n)
})
return this.filterPublishingRelays(
@ -1395,14 +1382,7 @@ class ClientService extends EventTarget { @@ -1395,14 +1382,7 @@ class ClientService extends EventTarget {
const relayListPromise = this.fetchRelayListWithPublishTimeout(event.pubkey)
const [relayLists, relayList] = await Promise.all([relayListsPromise, relayListPromise])
relayLists.forEach((rl) => {
for (const u of rl.httpRead ?? []) {
const n = normalizeHttpRelayUrl(u) || u
if (n) authorInboxFromContext.push(n)
}
for (const u of rl.read ?? []) {
const n = normalizeUrl(u) || u
if (n) authorInboxFromContext.push(n)
}
authorInboxFromContext.push(...collectRemoteReadInboxUrlsFromRelayList(rl))
})
if (
isAuthorProfileMetadataPublishKind(event.kind) ||
@ -1461,15 +1441,10 @@ class ClientService extends EventTarget { @@ -1461,15 +1441,10 @@ class ClientService extends EventTarget {
writeRelays: relayList?.write?.slice(0, MAX_PUBLISH_RELAYS) ?? []
})
}
const wsWrites = (relayList?.write ?? [])
.map((u) => normalizeUrl(u) || u)
.filter((u): u is string => !!u)
const httpWrites = (relayList?.httpWrite ?? [])
.map((u) => normalizeHttpRelayUrl(u) || u)
.filter((u): u is string => !!u)
const userWritesOrdered = isAuthorProfileMetadataPublishKind(event.kind)
? await this.resolveFullMailboxWriteUrlsForPublish(event.pubkey, relayList ?? this.emptyRelayListForPublish())
: dedupeNormalizeRelayUrlsOrdered([...httpWrites, ...wsWrites])
const userWritesOrdered = await collectViewerWriteOutboxUrls(
event.pubkey,
relayList ?? this.emptyRelayListForPublish()
)
relays = this.filterPublishingRelays(
buildPrioritizedWriteRelayUrls({
userWriteRelays: userWritesOrdered,
@ -1668,7 +1643,7 @@ class ClientService extends EventTarget { @@ -1668,7 +1643,7 @@ class ClientService extends EventTarget {
const socialKindBlockedSet = new Set(SOCIAL_KIND_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u))
let filtered = filterRelaysForEventPublish(mergedRelayUrls, event.kind).filter((url) => {
const n = normalizeAnyRelayUrl(url) || url
const n = normalizeRelayUrlByScheme(url) || url
if (isSocialKindBlockedKind(event.kind) && socialKindBlockedSet.has(n)) return false
return true
})
@ -1762,6 +1737,8 @@ class ClientService extends EventTarget { @@ -1762,6 +1737,8 @@ class ClientService extends EventTarget {
const publishOpBatch = new RelayPublishOpBatch(publishBatchSource, event.id, publishTargetUrls)
publishOpBatch.logBegin()
const httpIndexBasesForPublish = await this.resolveViewerHttpIndexBasesForPublish(publishTargetUrls)
// eslint-disable-next-line @typescript-eslint/no-this-alias
const client = this
return new Promise<{ success: boolean; relayStatuses: typeof relayStatuses; successCount: number; totalCount: number }>((resolve) => {
@ -1894,7 +1871,7 @@ class ClientService extends EventTarget { @@ -1894,7 +1871,7 @@ class ClientService extends EventTarget {
}, connectionTimeout + publishAckBudgetMs + 2_000)
try {
if (urlMatchesConfiguredHttpIndexRelay(url, this.viewerHttpIndexRelayBases)) {
if (urlMatchesConfiguredHttpIndexRelay(url, httpIndexBasesForPublish)) {
const base = normalizeHttpRelayUrl(url) || url
logger.debug(`[PublishEvent] Publishing to kind 10243 HTTP index relay`, { url: base })
await Promise.race([
@ -2033,7 +2010,7 @@ class ClientService extends EventTarget { @@ -2033,7 +2010,7 @@ class ClientService extends EventTarget {
}
} catch (error) {
const softHttpDown =
urlMatchesConfiguredHttpIndexRelay(url, this.viewerHttpIndexRelayBases) &&
urlMatchesConfiguredHttpIndexRelay(url, httpIndexBasesForPublish) &&
(error instanceof IndexRelayTransportError || isIndexRelayTransportFailure(error))
if (softHttpDown) {
logger.debug('[PublishEvent] HTTP index relay unreachable', {
@ -4551,9 +4528,7 @@ class ClientService extends EventTarget { @@ -4551,9 +4528,7 @@ class ClientService extends EventTarget {
*/
async getMailboxStackWriteUrlsForRepublish(pubkey: string): Promise<string[]> {
const rl = await this.peekRelayListFromStorage(pubkey)
const ws = (rl.write ?? []).map((u) => normalizeUrl(u) || u).filter((u): u is string => !!u)
const http = (rl.httpWrite ?? []).map((u) => normalizeHttpRelayUrl(u) || u).filter((u): u is string => !!u)
return dedupeNormalizeRelayUrlsOrdered([...http, ...ws])
return collectViewerWriteOutboxUrls(pubkey, rl)
}
/** Newest kind 10002 for `pubkey` from IndexedDB and/or session LRU (session may hold a copy not persisted yet). */
@ -5069,8 +5044,8 @@ class ClientService extends EventTarget { @@ -5069,8 +5044,8 @@ class ClientService extends EventTarget {
if (!/^[0-9a-f]{64}$/.test(pk)) return []
const relayList = await this.fetchRelayList(pk)
const urls = dedupeNormalizeRelayUrlsOrdered([
...relayList.write.map((u) => normalizeUrl(u) || u),
...relayList.read.map((u) => normalizeUrl(u) || u),
...collectWriteOutboxUrlsFromRelayList(relayList),
...collectReadInboxUrlsFromRelayList(relayList),
...FAST_READ_RELAY_URLS.map((u) => normalizeUrl(u) || u),
...PROFILE_RELAY_URLS.map((u) => normalizeUrl(u) || u)
]).filter(Boolean)
@ -5127,7 +5102,9 @@ class ClientService extends EventTarget { @@ -5127,7 +5102,9 @@ class ClientService extends EventTarget {
let urls = [...publicReadRelayFallbackUrls()]
if (myPubkey) {
const relayList = await this.fetchRelayList(myPubkey)
urls = relayList.read.concat([...publicReadRelayFallbackUrls()]).slice(0, 5)
urls = collectReadInboxUrlsFromRelayList(relayList)
.concat([...publicReadRelayFallbackUrls()])
.slice(0, 5)
}
return [{ urls, filter: { authors: pubkeys } }]
}

101
src/services/relay-selection.service.ts

@ -1,10 +1,8 @@ @@ -1,10 +1,8 @@
import { Event, kinds } from 'nostr-tools'
import { ExtendedKind, FAST_WRITE_RELAY_URLS, RANDOM_PUBLISH_RELAY_COUNT } from '@/constants'
import { filterRelaysForEventPublish } from '@/lib/relay-publish-filter'
import {
collectRecipientInboxUrls,
collectSenderOutboxUrls
} from '@/lib/public-message-publish-relays'
import { collectRecipientInboxUrls, collectSenderOutboxUrls } from '@/lib/public-message-publish-relays'
import { collectViewerWriteOutboxUrls } from '@/lib/viewer-write-outboxes'
import storage from '@/services/local-storage.service'
import { NOSTR_URI_FOR_REPLY_PUBKEYS_REGEX } from '@/lib/content-patterns'
import client from '@/services/client.service'
@ -20,6 +18,7 @@ import logger from '@/lib/logger' @@ -20,6 +18,7 @@ import logger from '@/lib/logger'
import indexedDb from '@/services/indexed-db.service'
import { getHttpRelayListFromEvent, getRelayListFromEvent } from '@/lib/event-metadata'
import { stripLocalNetworkRelaysFromRelayList } from '@/lib/relay-list-sanitize'
import { dedupeNormalizeRelayUrlsOrdered } from '@/lib/relay-url-priority'
import nip66Service from '@/services/nip66.service'
export interface RelaySelectionContext {
@ -68,6 +67,18 @@ class RelaySelectionService { @@ -68,6 +67,18 @@ class RelaySelectionService {
* Filter out local network relays from other users' relay lists
* We should only use our own local relays, not other users' local relays
*/
private normRelay(url: string): string {
return normalizeRelayUrlByScheme(url) || url.trim()
}
/** Kind 10002 + 10243 write/both outboxes for the logged-in user. */
private userWriteOutboxRelays(context: RelaySelectionContext): string[] {
return dedupeNormalizeRelayUrlsOrdered([
...(context.userHttpWriteRelays ?? []),
...context.userWriteRelays
])
}
private filterLocalRelaysFromOthers(relays: string[], isOwnRelays: boolean = false): string[] {
if (isOwnRelays) {
// For our own relays, keep all of them including local ones
@ -133,7 +144,7 @@ class RelaySelectionService { @@ -133,7 +144,7 @@ class RelaySelectionService {
.filter(Boolean)
)
filtered.forEach((url) => {
relayTypes[url] = httpSet.has(url) ? 'http_relay_list' : 'relay_list'
relayTypes[url] = httpSet.has(canonicalRelaySessionKey(url)) ? 'http_relay_list' : 'relay_list'
})
return { relays: filtered, relayTypes, randomRelayUrls: [] }
}
@ -396,7 +407,6 @@ class RelaySelectionService { @@ -396,7 +407,6 @@ class RelaySelectionService {
context: RelaySelectionContext
): Promise<string[]> {
const {
userWriteRelays,
parentEvent,
isPublicMessage,
openFrom,
@ -406,11 +416,12 @@ class RelaySelectionService { @@ -406,11 +416,12 @@ class RelaySelectionService {
let selectedRelays: string[] = []
const norm = (url: string) => normalizeAnyRelayUrl(url) || url
const userOutboxes = this.userWriteOutboxRelays(context)
const defaultOutboxes = userOutboxes.length > 0 ? userOutboxes : FAST_WRITE_RELAY_URLS
// If called with specific relay URLs, use those
if (openFrom && openFrom.length > 0) {
selectedRelays = Array.from(new Set(openFrom.map(norm).filter(Boolean)))
selectedRelays = Array.from(new Set(openFrom.map((url) => this.normRelay(url)).filter(Boolean)))
}
// For discussion replies, use relay hints from the kind 11 + user's outboxes + local relays + thecitadel
else if (parentEvent && (parentEvent.kind === ExtendedKind.DISCUSSION || parentEvent.kind === ExtendedKind.COMMENT)) {
@ -423,8 +434,7 @@ class RelaySelectionService { @@ -423,8 +434,7 @@ class RelaySelectionService {
}
// For regular replies, use user's write relays + mention relays
else if (parentEvent && this.isRegularReply(parentEvent)) {
const userRelays = userWriteRelays.length > 0 ? userWriteRelays : FAST_WRITE_RELAY_URLS
selectedRelays = Array.from(new Set(userRelays.map(norm).filter(Boolean)))
selectedRelays = Array.from(new Set(defaultOutboxes.map((url) => this.normRelay(url)).filter(Boolean)))
// Add mention relays
if (userPubkey) {
@ -441,28 +451,27 @@ class RelaySelectionService { @@ -441,28 +451,27 @@ class RelaySelectionService {
try {
const relayList = await this.getCachedRelayList(pubkey)
if (!relayList) return []
return this.filterLocalRelaysFromOthers(relayList.write || [])
return this.filterLocalRelaysFromOthers(collectSenderOutboxUrls(relayList))
} catch (error) {
logger.warn('Failed to get cached relay list', { pubkey, error })
return []
}
})
)
const mentionRelays = mentionRelayLists.flat().map(norm).filter(Boolean)
const mentionRelays = mentionRelayLists.flat().map((url) => this.normRelay(url)).filter(Boolean)
selectedRelays = Array.from(new Set([...selectedRelays, ...mentionRelays]))
}
}
}
// Default: user's write relays (or fallback to fast write relays if no user relays)
else {
const defaultRelays = userWriteRelays.length > 0 ? userWriteRelays : FAST_WRITE_RELAY_URLS
selectedRelays = Array.from(new Set(defaultRelays.map(norm).filter(Boolean)))
selectedRelays = Array.from(new Set(defaultOutboxes.map((url) => this.normRelay(url)).filter(Boolean)))
}
// ALWAYS include cache relays (local network relays) in selected relays
const cacheRelays = userWriteRelays.filter(url => isLocalNetworkUrl(url))
const cacheRelays = context.userWriteRelays.filter(url => isLocalNetworkUrl(url))
if (cacheRelays.length > 0) {
selectedRelays = Array.from(new Set([...selectedRelays, ...cacheRelays.map(norm).filter(Boolean)]))
selectedRelays = Array.from(new Set([...selectedRelays, ...cacheRelays.map((url) => this.normRelay(url)).filter(Boolean)]))
}
// When "add random relays" setting is ON, include random relays in selected by default; when OFF they are still in the list but unchecked
@ -498,20 +507,21 @@ class RelaySelectionService { @@ -498,20 +507,21 @@ class RelaySelectionService {
if (senderRelays.length === 0) {
try {
const userRelayList = await this.getCachedRelayList(userPubkey)
senderRelays = collectSenderOutboxUrls(userRelayList)
if (userRelayList) {
senderRelays = await collectViewerWriteOutboxUrls(userPubkey, userRelayList)
}
} catch (error) {
logger.warn('Failed to fetch user relay list for PM', { error, userPubkey })
}
}
senderRelays.forEach(url => {
const normalized = normalizeAnyRelayUrl(url)
if (normalized) {
if (!relayToMembers.has(normalized)) {
relayToMembers.set(normalized, new Set())
}
relayToMembers.get(normalized)!.add(userPubkey)
const normalized = this.normRelay(url)
if (!normalized) return
if (!relayToMembers.has(normalized)) {
relayToMembers.set(normalized, new Set())
}
relayToMembers.get(normalized)!.add(userPubkey)
})
}
@ -561,13 +571,12 @@ class RelaySelectionService { @@ -561,13 +571,12 @@ class RelaySelectionService {
recipientRelayLists.forEach((relays, index) => {
const pubkey = recipientPubkeys[index]
relays.forEach(url => {
const normalized = normalizeAnyRelayUrl(url)
if (normalized) {
if (!relayToMembers.has(normalized)) {
relayToMembers.set(normalized, new Set())
}
relayToMembers.get(normalized)!.add(pubkey)
const normalized = this.normRelay(url)
if (!normalized) return
if (!relayToMembers.has(normalized)) {
relayToMembers.set(normalized, new Set())
}
relayToMembers.get(normalized)!.add(pubkey)
})
})
}
@ -627,7 +636,7 @@ class RelaySelectionService { @@ -627,7 +636,7 @@ class RelaySelectionService {
// Normalize and deduplicate final list
const normalizedRelays = relays
.map(url => normalizeAnyRelayUrl(url))
.map((url) => this.normRelay(url))
.filter((url): url is string => !!url)
return Array.from(new Set(normalizedRelays))
@ -663,10 +672,11 @@ class RelaySelectionService { @@ -663,10 +672,11 @@ class RelaySelectionService {
* Includes: relay hints from kind 11, wss://thecitadel.nostr1.com, user's outboxes, and local relays
*/
private async getDiscussionReplyRelays(context: RelaySelectionContext): Promise<string[]> {
const { parentEvent, userWriteRelays, userPubkey, blockedRelays } = context
const { parentEvent, userPubkey, blockedRelays } = context
if (!parentEvent) return []
const relayUrls = new Set<string>()
const userOutboxes = this.userWriteOutboxRelays(context)
// Step 1: Get relay hints from the kind 11 event
let discussionEventId: string | null = null
@ -700,24 +710,21 @@ class RelaySelectionService { @@ -700,24 +710,21 @@ class RelaySelectionService {
relayUrls.add(thecitadelUrl)
}
// Step 3: Add user's outboxes (write relays from kind 10002)
if (userWriteRelays.length > 0) {
userWriteRelays.forEach(url => {
const normalized = normalizeAnyRelayUrl(url)
if (normalized) {
relayUrls.add(normalized)
}
// Step 3: Add user's outboxes (NIP-65 + HTTP index write relays)
if (userOutboxes.length > 0) {
userOutboxes.forEach((url) => {
const normalized = this.normRelay(url)
if (normalized) relayUrls.add(normalized)
})
} else if (userPubkey) {
// Fetch user's relay list if not provided
try {
const relayList = await this.getCachedRelayList(userPubkey)
if (relayList?.write) {
relayList.write.forEach(url => {
const normalized = normalizeAnyRelayUrl(url)
if (normalized) {
relayUrls.add(normalized)
}
if (relayList) {
const outboxes = await collectViewerWriteOutboxUrls(userPubkey, relayList)
outboxes.forEach((url) => {
const normalized = this.normRelay(url)
if (normalized) relayUrls.add(normalized)
})
}
} catch (error) {
@ -746,7 +753,7 @@ class RelaySelectionService { @@ -746,7 +753,7 @@ class RelaySelectionService {
// Step 5: Convert to array, normalize, and deduplicate
const normalizedRelays = Array.from(relayUrls)
.map(url => normalizeAnyRelayUrl(url))
.map((url) => this.normRelay(url))
.filter((url): url is string => !!url)
const deduplicatedRelays = Array.from(new Set(normalizedRelays))
@ -849,9 +856,7 @@ class RelaySelectionService { @@ -849,9 +856,7 @@ class RelaySelectionService {
return relays
}
const safeNormalize = (url: string): string => {
return normalizeAnyRelayUrl(url) || url
}
const safeNormalize = (url: string): string => this.normRelay(url)
const normalizedBlocked = blockedRelays.map(safeNormalize)
return relays.filter(relay => {

Loading…
Cancel
Save