Browse Source

fix http relay service

imwald
Silberengel 3 weeks ago
parent
commit
8f1d44404a
  1. 2
      src/components/ContentPreview/FollowPackPreview.tsx
  2. 2
      src/components/FollowButton/index.tsx
  3. 34
      src/components/NoteOptions/useMenuActions.tsx
  4. 2
      src/components/PostEditor/PostContent.tsx
  5. 2
      src/components/Profile/Followings.tsx
  6. 2
      src/components/Profile/SmartFollowings.tsx
  7. 12
      src/components/Profile/index.tsx
  8. 12
      src/components/ProfileOptions/index.tsx
  9. 11
      src/components/Relay/index.tsx
  10. 4
      src/components/ReplyNoteList/index.tsx
  11. 8
      src/components/SaveRelayDropdownMenu/index.tsx
  12. 6
      src/components/SearchBar/index.tsx
  13. 2
      src/components/TopicSubscribeButton/index.tsx
  14. 4
      src/hooks/useQuoteEvents.tsx
  15. 6
      src/lib/account-list-relay-urls.ts
  16. 26
      src/lib/index-relay-http.ts
  17. 25
      src/lib/replaceable-list-latest.ts
  18. 18
      src/lib/url.ts
  19. 8
      src/pages/primary/ExplorePage/index.tsx
  20. 4
      src/pages/primary/RelayPage/index.tsx
  21. 2
      src/pages/secondary/InterestListPage/index.tsx
  22. 2
      src/pages/secondary/NoteListPage/index.tsx
  23. 4
      src/pages/secondary/RelayPage/index.tsx
  24. 4
      src/pages/secondary/RelayReviewsPage/index.tsx
  25. 20
      src/providers/FavoriteRelaysProvider.tsx
  26. 12
      src/providers/FeedProvider.tsx
  27. 24
      src/providers/FollowListProvider.tsx
  28. 20
      src/providers/GroupListProvider.tsx
  29. 28
      src/providers/InterestListProvider.tsx
  30. 2
      src/providers/LiveActivitiesProvider.tsx
  31. 8
      src/providers/NostrProvider/index.tsx
  32. 22
      src/providers/follow-list-context.tsx
  33. 18
      src/providers/group-list-context.tsx
  34. 27
      src/providers/interest-list-context.tsx
  35. 132
      src/services/client.service.ts
  36. 8
      src/services/relay-info.service.ts
  37. 4
      src/services/relay-operation-log.service.ts
  38. 2
      vite.config.ts

2
src/components/ContentPreview/FollowPackPreview.tsx

@ -3,7 +3,7 @@ import { getImetaInfosFromEvent } from '@/lib/event' @@ -3,7 +3,7 @@ import { getImetaInfosFromEvent } from '@/lib/event'
import { getPubkeysFromPTags } from '@/lib/tag'
import logger from '@/lib/logger'
import { cn } from '@/lib/utils'
import { useFollowListOptional } from '@/providers/FollowListProvider'
import { useFollowListOptional } from '@/providers/follow-list-context'
import { useMuteList } from '@/contexts/mute-list-context'
import { muteSetHas } from '@/lib/mute-set'
import { useNostr } from '@/providers/NostrProvider'

2
src/components/FollowButton/index.tsx

@ -11,7 +11,7 @@ import { @@ -11,7 +11,7 @@ import {
} from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import { useFollowListOptional } from '@/providers/FollowListProvider'
import { useFollowListOptional } from '@/providers/follow-list-context'
import { useMuteList } from '@/contexts/mute-list-context'
import { muteSetHas } from '@/lib/mute-set'
import { useNostr } from '@/providers/NostrProvider'

34
src/components/NoteOptions/useMenuActions.tsx

@ -11,7 +11,7 @@ import { @@ -11,7 +11,7 @@ import {
parsePublicationATagCoordinate,
type PublicationSectionRef
} from '@/lib/publication-section-fetch'
import { normalizeUrl, simplifyUrl } from '@/lib/url'
import { normalizeAnyRelayUrl, normalizeHttpRelayUrl, simplifyUrl } from '@/lib/url'
import { speakNoteReadAloud } from '@/lib/read-aloud'
import {
buildPinListTagsAfterToggle,
@ -104,25 +104,30 @@ export function useMenuActions({ @@ -104,25 +104,30 @@ export function useMenuActions({
// Use useContext directly to avoid error if provider is not available
const primaryPageContext = useContext(PrimaryPageContext)
const currentPrimaryPage = primaryPageContext?.current ?? null
const { pubkey, profile, attemptDelete, publish, account } = useNostr()
const { pubkey, profile, attemptDelete, publish, account, relayList } = useNostr()
const canSignEvents = account != null && account.signerType !== 'npub'
const { relayUrls: currentBrowsingRelayUrls } = useCurrentRelays()
const { relaySets, favoriteRelays } = useFavoriteRelays()
const httpWriteRelayUrls = useMemo(() => {
return (relayList?.httpWrite ?? [])
.map(url => normalizeHttpRelayUrl(url) || url)
.filter(Boolean) as string[]
}, [relayList?.httpWrite])
const relayUrls = useMemo(() => {
return Array.from(new Set([
...currentBrowsingRelayUrls.map(url => normalizeUrl(url) || url),
...favoriteRelays.map(url => normalizeUrl(url) || url)
...currentBrowsingRelayUrls.map(url => normalizeAnyRelayUrl(url) || url),
...favoriteRelays.map(url => normalizeAnyRelayUrl(url) || url)
]))
}, [currentBrowsingRelayUrls, favoriteRelays])
/** All available relays: current feed, favorites, relay sets, defaults (BIG, FAST_READ, FAST_WRITE). */
const allAvailableRelayUrls = useMemo(() => {
const urls = [
...currentBrowsingRelayUrls.map(url => normalizeUrl(url) || url),
...favoriteRelays.map(url => normalizeUrl(url) || url),
...relaySets.flatMap(set => set.relayUrls.map(url => normalizeUrl(url) || url)),
...FAST_READ_RELAY_URLS.map(url => normalizeUrl(url) || url),
...FAST_WRITE_RELAY_URLS.map(url => normalizeUrl(url) || url)
...currentBrowsingRelayUrls.map(url => normalizeAnyRelayUrl(url) || url),
...favoriteRelays.map(url => normalizeAnyRelayUrl(url) || url),
...relaySets.flatMap(set => set.relayUrls.map(url => normalizeAnyRelayUrl(url) || url)),
...FAST_READ_RELAY_URLS.map(url => normalizeAnyRelayUrl(url) || url),
...FAST_WRITE_RELAY_URLS.map(url => normalizeAnyRelayUrl(url) || url)
].filter(Boolean) as string[]
return Array.from(new Set(urls))
}, [currentBrowsingRelayUrls, favoriteRelays, relaySets])
@ -163,7 +168,7 @@ export function useMenuActions({ @@ -163,7 +168,7 @@ export function useMenuActions({
...FAST_WRITE_RELAY_URLS
]
const comprehensiveRelays = Array.from(
new Set(allRelays.map(url => normalizeUrl(url)).filter((url): url is string => !!url))
new Set(allRelays.map(url => normalizeAnyRelayUrl(url)).filter((url): url is string => !!url))
)
const pinListEvent = await fetchNewestPinListForPubkey(pubkey, comprehensiveRelays)
if (pinListEvent) {
@ -196,7 +201,7 @@ export function useMenuActions({ @@ -196,7 +201,7 @@ export function useMenuActions({
]
const normalizedRelays = allRelays
.map(url => normalizeUrl(url))
.map(url => normalizeAnyRelayUrl(url))
.filter((url): url is string => !!url)
const comprehensiveRelays = Array.from(new Set(normalizedRelays))
@ -386,9 +391,10 @@ export function useMenuActions({ @@ -386,9 +391,10 @@ export function useMenuActions({
)
}
if (relayUrls.length) {
const wsAndHttpRelayUrls = Array.from(new Set([...relayUrls, ...httpWriteRelayUrls]))
if (wsAndHttpRelayUrls.length) {
items.push(
...relayUrls.map((relay, index) => ({
...wsAndHttpRelayUrls.map((relay, index) => ({
label: (
<div className="flex items-center gap-2 w-full">
<RelayIcon url={relay} />
@ -418,7 +424,7 @@ export function useMenuActions({ @@ -418,7 +424,7 @@ export function useMenuActions({
}
return items
}, [pubkey, relayUrls, relaySets, allAvailableRelayUrls, monitoringListRelayCount, event, closeDrawer, t])
}, [pubkey, relayUrls, httpWriteRelayUrls, relaySets, allAvailableRelayUrls, monitoringListRelayCount, event, closeDrawer, t])
// Check if this is an article-type event
const isArticleType = useMemo(() => {

2
src/components/PostEditor/PostContent.tsx

@ -89,7 +89,7 @@ import { @@ -89,7 +89,7 @@ import {
import { prefixNostrAddresses } from '@/lib/nostr-address'
import dayjs from 'dayjs'
import { TDraftEvent } from '@/types'
import { useGroupList } from '@/providers/GroupListProvider'
import { useGroupList } from '@/providers/group-list-context'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Switch } from '@/components/ui/switch'
import { DISCUSSION_TOPICS } from '@/pages/primary/DiscussionsPage/discussionTopics'

2
src/components/Profile/Followings.tsx

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
import { useFetchFollowings } from '@/hooks'
import { toFollowingList } from '@/lib/link'
import { SecondaryPageLink } from '@/PageManager'
import { useFollowList } from '@/providers/FollowListProvider'
import { useFollowList } from '@/providers/follow-list-context'
import { useNostr } from '@/providers/NostrProvider'
import { Skeleton } from '@/components/ui/skeleton'
import { useTranslation } from 'react-i18next'

2
src/components/Profile/SmartFollowings.tsx

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
import { useFetchFollowings } from '@/hooks'
import { toFollowingList } from '@/lib/link'
import { useSmartFollowingListNavigation } from '@/PageManager'
import { useFollowListOptional } from '@/providers/FollowListProvider'
import { useFollowListOptional } from '@/providers/follow-list-context'
import { useNostr } from '@/providers/NostrProvider'
import { Skeleton } from '@/components/ui/skeleton'
import { useTranslation } from 'react-i18next'

12
src/components/Profile/index.tsx

@ -66,7 +66,7 @@ import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' @@ -66,7 +66,7 @@ import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useCurrentRelays } from '@/providers/CurrentRelaysProvider'
import { FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS } from '@/constants'
import { nip66Service } from '@/services/nip66.service'
import { normalizeUrl } from '@/lib/url'
import { normalizeAnyRelayUrl } from '@/lib/url'
import type { TProfile } from '@/types'
/**
@ -292,11 +292,11 @@ export default function Profile({ @@ -292,11 +292,11 @@ export default function Profile({
/** All available relays: current feed, favorites, relay sets, defaults (FAST_READ, FAST_WRITE). */
const allAvailableRelayUrls = useMemo(() => {
const urls = [
...currentBrowsingRelayUrls.map(url => normalizeUrl(url) || url),
...favoriteRelays.map(url => normalizeUrl(url) || url),
...relaySets.flatMap(set => set.relayUrls.map(url => normalizeUrl(url) || url)),
...FAST_READ_RELAY_URLS.map(url => normalizeUrl(url) || url),
...FAST_WRITE_RELAY_URLS.map(url => normalizeUrl(url) || url)
...currentBrowsingRelayUrls.map(url => normalizeAnyRelayUrl(url) || url),
...favoriteRelays.map(url => normalizeAnyRelayUrl(url) || url),
...relaySets.flatMap(set => set.relayUrls.map(url => normalizeAnyRelayUrl(url) || url)),
...FAST_READ_RELAY_URLS.map(url => normalizeAnyRelayUrl(url) || url),
...FAST_WRITE_RELAY_URLS.map(url => normalizeAnyRelayUrl(url) || url)
].filter(Boolean) as string[]
return Array.from(new Set(urls))
}, [currentBrowsingRelayUrls, favoriteRelays, relaySets])

12
src/components/ProfileOptions/index.tsx

@ -8,7 +8,7 @@ import { @@ -8,7 +8,7 @@ import {
} from '@/components/ui/dropdown-menu'
import { buildHiveTalkJoinUrl, roomIdForPubkeys } from '@/lib/hivetalk'
import { formatPubkey, pubkeyToNpub } from '@/lib/pubkey'
import { normalizeUrl } from '@/lib/url'
import { normalizeAnyRelayUrl } from '@/lib/url'
import { useMuteList } from '@/contexts/mute-list-context'
import { muteSetHas } from '@/lib/mute-set'
import { useNostr } from '@/providers/NostrProvider'
@ -82,11 +82,11 @@ export default function ProfileOptions({ @@ -82,11 +82,11 @@ export default function ProfileOptions({
/** All available relays: current feed, favorites, relay sets, defaults (FAST_READ, FAST_WRITE). */
const allAvailableRelayUrls = useMemo(() => {
const urls = [
...currentBrowsingRelayUrls.map(url => normalizeUrl(url) || url),
...favoriteRelays.map(url => normalizeUrl(url) || url),
...relaySets.flatMap(set => set.relayUrls.map(url => normalizeUrl(url) || url)),
...FAST_READ_RELAY_URLS.map(url => normalizeUrl(url) || url),
...FAST_WRITE_RELAY_URLS.map(url => normalizeUrl(url) || url)
...currentBrowsingRelayUrls.map(url => normalizeAnyRelayUrl(url) || url),
...favoriteRelays.map(url => normalizeAnyRelayUrl(url) || url),
...relaySets.flatMap(set => set.relayUrls.map(url => normalizeAnyRelayUrl(url) || url)),
...FAST_READ_RELAY_URLS.map(url => normalizeAnyRelayUrl(url) || url),
...FAST_WRITE_RELAY_URLS.map(url => normalizeAnyRelayUrl(url) || url)
].filter(Boolean) as string[]
return Array.from(new Set(urls))
}, [currentBrowsingRelayUrls, favoriteRelays, relaySets])

11
src/components/Relay/index.tsx

@ -5,7 +5,7 @@ import SearchInput from '@/components/SearchInput' @@ -5,7 +5,7 @@ import SearchInput from '@/components/SearchInput'
import { useFetchRelayInfo } from '@/hooks'
import type { TPrimaryPageName } from '@/PageManager'
import { SINGLE_RELAY_KINDLESS_REQ_LIMIT } from '@/constants'
import { normalizeUrl } from '@/lib/url'
import { isHttpRelayUrl, normalizeAnyRelayUrl } from '@/lib/url'
import { useCurrentRelays } from '@/providers/CurrentRelaysProvider'
import client, { JUMBLE_SESSION_RELAY_STRIKES_CHANGED } from '@/services/client.service'
import type { TFeedSubRequest } from '@/types'
@ -19,7 +19,8 @@ const Relay = forwardRef< @@ -19,7 +19,8 @@ const Relay = forwardRef<
>(function Relay({ url, className, hostPrimaryPageName }, ref) {
const { t } = useTranslation()
const { addRelayUrls, removeRelayUrls } = useCurrentRelays()
const normalizedUrl = useMemo(() => (url ? normalizeUrl(url) : undefined), [url])
const normalizedUrl = useMemo(() => (url ? normalizeAnyRelayUrl(url) : undefined), [url])
const isHttpRelay = useMemo(() => !!normalizedUrl && isHttpRelayUrl(normalizedUrl), [normalizedUrl])
const { relayInfo } = useFetchRelayInfo(normalizedUrl)
const [searchInput, setSearchInput] = useState('')
const [debouncedInput, setDebouncedInput] = useState(searchInput)
@ -82,7 +83,7 @@ const Relay = forwardRef< @@ -82,7 +83,7 @@ const Relay = forwardRef<
const handleRelayRefresh = (event: CustomEvent) => {
const { relayUrl } = event.detail
if (normalizeUrl(relayUrl) === normalizedUrl) {
if (normalizeAnyRelayUrl(relayUrl) === normalizedUrl) {
if (noteListRef && typeof noteListRef !== 'function') {
noteListRef.current?.refresh()
}
@ -97,7 +98,7 @@ const Relay = forwardRef< @@ -97,7 +98,7 @@ const Relay = forwardRef<
}, [normalizedUrl, noteListRef])
const relayFeedSubRequests = useMemo<TFeedSubRequest[]>(() => {
if (!normalizedUrl) return []
if (!normalizedUrl || isHttpRelay) return []
const q = debouncedInput.trim()
return [
{
@ -107,7 +108,7 @@ const Relay = forwardRef< @@ -107,7 +108,7 @@ const Relay = forwardRef<
: { limit: SINGLE_RELAY_KINDLESS_REQ_LIMIT }
}
]
}, [normalizedUrl, debouncedInput])
}, [normalizedUrl, isHttpRelay, debouncedInput])
if (!normalizedUrl) {
return <NotFound />

4
src/components/ReplyNoteList/index.tsx

@ -19,7 +19,7 @@ import { @@ -19,7 +19,7 @@ import {
} from '@/lib/event'
import logger from '@/lib/logger'
import { getZapInfoFromEvent, shouldIncludeZapReceiptAtReplyThreshold } from '@/lib/event-metadata'
import { normalizeUrl } from '@/lib/url'
import { normalizeAnyRelayUrl, normalizeUrl } from '@/lib/url'
import { shouldHideThreadResponseEvent } from '@/lib/thread-response-filter'
import { getCachedThreadContextEvents } from '@/lib/navigation-related-events'
import { toNote } from '@/lib/link'
@ -768,7 +768,7 @@ function ReplyNoteList({ @@ -768,7 +768,7 @@ function ReplyNoteList({
// READ from: FAST_READ_RELAY_URLS + user's inboxes + local relays + OP author's outboxes
const opAuthorPubkey = rootInfo.type === 'E' || rootInfo.type === 'A' ? rootInfo.pubkey : undefined
const seenOn = client.getSeenEventRelayUrls(event.id).map((u) => normalizeUrl(u) || u).filter(Boolean)
const fromBrowsingFeed = browsingRelayUrls.map((u) => normalizeUrl(u) || u).filter(Boolean)
const fromBrowsingFeed = browsingRelayUrls.map((u) => normalizeAnyRelayUrl(u) || u).filter(Boolean)
const threadRelayHints = [
...new Set([...relayHintsFromEventTags(event), ...seenOn, ...fromBrowsingFeed])
]

8
src/components/SaveRelayDropdownMenu/index.tsx

@ -26,7 +26,7 @@ import { Input } from '@/components/ui/input' @@ -26,7 +26,7 @@ import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Separator } from '@/components/ui/separator'
import { Skeleton } from '@/components/ui/skeleton'
import { normalizeUrl } from '@/lib/url'
import { normalizeAnyRelayUrl } from '@/lib/url'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useNostr } from '@/providers/NostrProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
@ -47,7 +47,7 @@ export default function SaveRelayDropdownMenu({ @@ -47,7 +47,7 @@ export default function SaveRelayDropdownMenu({
const { t } = useTranslation()
const { isSmallScreen } = useScreenSize()
const { favoriteRelays, relaySets } = useFavoriteRelays()
const normalizedUrls = useMemo(() => urls.map((url) => normalizeUrl(url)).filter(Boolean), [urls])
const normalizedUrls = useMemo(() => urls.map((url) => normalizeAnyRelayUrl(url)).filter(Boolean), [urls])
const alreadySaved = useMemo(() => {
return (
normalizedUrls.every((url) => favoriteRelays.includes(url)) ||
@ -188,8 +188,8 @@ function RelaySetItem({ set, urls }: { set: TRelaySet; urls: string[] }) { @@ -188,8 +188,8 @@ function RelaySetItem({ set, urls }: { set: TRelaySet; urls: string[] }) {
updateRelaySet({
...set,
relayUrls: Array.from(new Set([
...set.relayUrls.map(url => normalizeUrl(url) || url),
...urls.map(url => normalizeUrl(url) || url)
...set.relayUrls.map(url => normalizeAnyRelayUrl(url) || url),
...urls.map(url => normalizeAnyRelayUrl(url) || url)
]))
})
}

6
src/components/SearchBar/index.tsx

@ -4,7 +4,7 @@ import { toNote, toNoteList } from '@/lib/link' @@ -4,7 +4,7 @@ import { toNote, toNoteList } from '@/lib/link'
import client from '@/services/client.service'
import { eventService } from '@/services/client.service'
import { randomString } from '@/lib/random'
import { normalizeUrl } from '@/lib/url'
import { isHttpRelayUrl, isWebsocketUrl, normalizeAnyRelayUrl } from '@/lib/url'
import { normalizeToDTag } from '@/lib/search-parser'
import { cn } from '@/lib/utils'
import { useSmartNoteNavigation, useSmartHashtagNavigation } from '@/PageManager'
@ -53,7 +53,9 @@ const SearchBar = forwardRef< @@ -53,7 +53,9 @@ const SearchBar = forwardRef<
return undefined
}
try {
return normalizeUrl(input)
const n = normalizeAnyRelayUrl(input)
if (!n || (!isHttpRelayUrl(n) && !isWebsocketUrl(n))) return undefined
return n
} catch {
return undefined
}

2
src/components/TopicSubscribeButton/index.tsx

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import { useInterestList } from '@/providers/InterestListProvider'
import { useInterestList } from '@/providers/interest-list-context'
import { useNostr } from '@/providers/NostrProvider'
import { Bell, BellOff } from 'lucide-react'
import { useTranslation } from 'react-i18next'

4
src/hooks/useQuoteEvents.tsx

@ -6,7 +6,7 @@ import { @@ -6,7 +6,7 @@ import {
} from '@/constants'
import { getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event'
import { buildNormalizedBlockedRelaySet } from '@/lib/thread-response-filter'
import { normalizeUrl } from '@/lib/url'
import { normalizeAnyRelayUrl, normalizeUrl } from '@/lib/url'
import { useCurrentRelays } from '@/providers/CurrentRelaysProvider'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useNostr } from '@/providers/NostrProvider'
@ -69,7 +69,7 @@ export function useQuoteEvents(event: Event | null, enabled: boolean) { @@ -69,7 +69,7 @@ export function useQuoteEvents(event: Event | null, enabled: boolean) {
}, INITIAL_QUOTE_LOAD_TIMEOUT_MS)
const userRelays = userRelayList?.read || []
const fromFeed = browsingRelayUrls.map((u) => normalizeUrl(u) || u).filter(Boolean)
const fromFeed = browsingRelayUrls.map((u) => normalizeAnyRelayUrl(u) || u).filter(Boolean)
const seenOn = client.getSeenEventRelayUrls(ev.id)
const eTagBlockedSet = new Set(
E_TAG_FILTER_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u)

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

@ -16,15 +16,15 @@ export async function buildAccountListRelayUrlsForMerge(options: { @@ -16,15 +16,15 @@ export async function buildAccountListRelayUrlsForMerge(options: {
const myRelayList = await client.fetchRelayList(accountPubkey)
const favoritesTier = getFavoritesFeedRelayUrls(favoriteRelays ?? [], blockedRelays)
const read = buildPrioritizedReadRelayUrls({
userReadRelays: [...(myRelayList.httpRead ?? []), ...(myRelayList.read ?? [])],
userWriteRelays: [...(myRelayList.httpWrite ?? []), ...(myRelayList.write ?? [])],
userReadRelays: myRelayList.read ?? [],
userWriteRelays: myRelayList.write ?? [],
favoriteRelays: favoritesTier,
blockedRelays,
maxRelays: 100,
applySocialKindBlockedFilter: false
})
const write = buildPrioritizedWriteRelayUrls({
userWriteRelays: [...(myRelayList.httpWrite ?? []), ...(myRelayList.write ?? [])],
userWriteRelays: myRelayList.write ?? [],
favoriteRelays: favoritesTier,
blockedRelays,
maxRelays: 100,

26
src/lib/index-relay-http.ts

@ -8,7 +8,7 @@ @@ -8,7 +8,7 @@
*/
import { fetchWithTimeout } from '@/lib/fetch-with-timeout'
import logger from '@/lib/logger'
import { normalizeHttpRelayUrl } from '@/lib/url'
import { devProxyLoopbackHttpRelayBase, normalizeHttpRelayUrl } from '@/lib/url'
import type { Filter, Event as NEvent } from 'nostr-tools'
import { verifyEvent } from 'nostr-tools'
@ -16,24 +16,6 @@ function trimSlash(base: string): string { @@ -16,24 +16,6 @@ function trimSlash(base: string): string {
return base.replace(/\/+$/, '')
}
/**
* Avoid browser CORS in dev: `http://localhost:1122/api/...` becomes same-origin `…/dev-index-relay/api/…`
* and Vite forwards to the real relay (see `vite.config.ts`).
*/
function devProxyLoopbackIndexRelayBase(normalizedBase: string): string {
if (import.meta.env.PROD || typeof window === 'undefined') return normalizedBase
let u: URL
try {
u = new URL(normalizedBase)
} catch {
return normalizedBase
}
if (u.protocol !== 'http:') return normalizedBase
const h = u.hostname
if (h !== 'localhost' && h !== '127.0.0.1') return normalizedBase
return `${window.location.origin}/dev-index-relay`
}
export function indexRelayFilterUrl(baseUrl: string): string {
return `${trimSlash(normalizeHttpRelayUrl(baseUrl) || baseUrl)}/api/events/filter`
}
@ -162,7 +144,7 @@ export async function queryIndexRelay( @@ -162,7 +144,7 @@ export async function queryIndexRelay(
filter: Filter | Filter[],
options?: { signal?: AbortSignal; onHardFailure?: () => void }
): Promise<NEvent[]> {
const base = devProxyLoopbackIndexRelayBase(normalizeHttpRelayUrl(baseUrl) || baseUrl)
const base = devProxyLoopbackHttpRelayBase(normalizeHttpRelayUrl(baseUrl) || baseUrl)
const endpoint = indexRelayFilterUrl(base)
const filters = Array.isArray(filter) ? filter : [filter]
const out: NEvent[] = []
@ -225,12 +207,12 @@ function filterForIndexRelay(f: Filter): Filter { @@ -225,12 +207,12 @@ function filterForIndexRelay(f: Filter): Filter {
return rest as Filter
}
export async function publishEventToIndexRelay(
export async function publishEventToHttpRelay(
baseUrl: string,
event: NEvent,
options?: { signal?: AbortSignal }
): Promise<void> {
const base = devProxyLoopbackIndexRelayBase(normalizeHttpRelayUrl(baseUrl) || baseUrl)
const base = devProxyLoopbackHttpRelayBase(normalizeHttpRelayUrl(baseUrl) || baseUrl)
const endpoint = indexRelayPublishUrl(base)
try {
const res = await fetchWithTimeout(endpoint, {

25
src/lib/replaceable-list-latest.ts

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
import { METADATA_BATCH_QUERY_EOSE_TIMEOUT_MS, METADATA_BATCH_QUERY_GLOBAL_TIMEOUT_MS } from '@/constants'
import { normalizeHexPubkey } from '@/lib/pubkey'
import { normalizeUrl } from '@/lib/url'
import client, { queryService } from '@/services/client.service'
import { normalizeAnyRelayUrl } from '@/lib/url'
import client from '@/services/client.service'
import type { TPersonalListBech32Ref } from '@/lib/personal-list-mutations'
import type { Event } from 'nostr-tools'
@ -15,17 +15,16 @@ export async function fetchLatestReplaceableListEvent( @@ -15,17 +15,16 @@ export async function fetchLatestReplaceableListEvent(
relayUrls: string[]
): Promise<Event | undefined> {
const pk = normalizeHexPubkey(pubkeyHex)
const urls = [...new Set(relayUrls.map((u) => normalizeUrl(u) || u).filter(Boolean))]
if (!urls.length) return undefined
const rows = await queryService.fetchEvents(
urls,
{ authors: [pk], kinds: [kind], limit: 80 },
{
replaceableRace: true,
eoseTimeout: METADATA_BATCH_QUERY_EOSE_TIMEOUT_MS,
globalTimeout: METADATA_BATCH_QUERY_GLOBAL_TIMEOUT_MS
}
)
const allUrls = [...new Set(relayUrls.map((u) => normalizeAnyRelayUrl(u) || u).filter(Boolean))]
if (!allUrls.length) return undefined
// client.fetchEvents() handles both HTTP index relays and WebSocket relays internally.
const rows = await client.fetchEvents(allUrls, { authors: [pk], kinds: [kind], limit: 80 }, {
replaceableRace: true,
eoseTimeout: METADATA_BATCH_QUERY_EOSE_TIMEOUT_MS,
globalTimeout: METADATA_BATCH_QUERY_GLOBAL_TIMEOUT_MS
})
if (!rows.length) return undefined
return rows.reduce((best, e) => (e.created_at > best.created_at ? e : best))
}

18
src/lib/url.ts

@ -30,6 +30,24 @@ export function normalizeHttpRelayUrl(url: string): string { @@ -30,6 +30,24 @@ export function normalizeHttpRelayUrl(url: string): string {
return normalizeHttpUrl(url)
}
/**
* In dev, loopback HTTP relay bases (`http://localhost:*` / `http://127.0.0.1:*`) use the Vite
* same-origin `/dev-index-relay` proxy (see `vite.config.ts`) so JSON APIs and NIP-11 avoid CORS.
*/
export function devProxyLoopbackHttpRelayBase(normalizedBase: string): string {
if (import.meta.env.PROD || typeof window === 'undefined') return normalizedBase
let u: URL
try {
u = new URL(normalizedBase)
} catch {
return normalizedBase
}
if (u.protocol !== 'http:') return normalizedBase
const h = u.hostname
if (h !== 'localhost' && h !== '127.0.0.1') return normalizedBase
return `${window.location.origin}/dev-index-relay`
}
/**
* Normalize relay URL for deduplication: WebSocket URLs via {@link normalizeUrl}, HTTPS index relays via {@link normalizeHttpRelayUrl}.
*/

8
src/pages/primary/ExplorePage/index.tsx

@ -7,7 +7,7 @@ import { Button } from '@/components/ui/button' @@ -7,7 +7,7 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { toRelay } from '@/lib/link'
import { cn } from '@/lib/utils'
import { isWebsocketUrl, normalizeUrl, simplifyUrl } from '@/lib/url'
import { isHttpRelayUrl, isWebsocketUrl, normalizeAnyRelayUrl, simplifyUrl } from '@/lib/url'
import { RefreshButton } from '@/components/RefreshButton'
import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
import { syncUserDeletionTombstones } from '@/lib/sync-user-deletions'
@ -35,7 +35,7 @@ function dedupeNormalizedRelayUrls(urls: string[]): string[] { @@ -35,7 +35,7 @@ function dedupeNormalizedRelayUrls(urls: string[]): string[] {
const seen = new Set<string>()
const out: string[] = []
for (const u of urls) {
const k = normalizeUrl(u) || u
const k = normalizeAnyRelayUrl(u) || u.trim()
if (!k || seen.has(k)) continue
seen.add(k)
out.push(k)
@ -232,8 +232,8 @@ function ExploreRelaySearchSection() { @@ -232,8 +232,8 @@ function ExploreRelaySearchSection() {
const tryOpenRelay = () => {
const trimmed = relayQuery.trim()
if (!trimmed) return
const normalized = normalizeUrl(trimmed)
if (!normalized || !isWebsocketUrl(normalized)) {
const normalized = normalizeAnyRelayUrl(trimmed)
if (!normalized || (!isHttpRelayUrl(normalized) && !isWebsocketUrl(normalized))) {
toast.error(t('invalid relay URL'))
return
}

4
src/pages/primary/RelayPage/index.tsx

@ -3,13 +3,13 @@ import { RefreshButton } from '@/components/RefreshButton' @@ -3,13 +3,13 @@ import { RefreshButton } from '@/components/RefreshButton'
import Relay from '@/components/Relay'
import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
import { TPageRef } from '@/types'
import { normalizeUrl, simplifyUrl } from '@/lib/url'
import { normalizeAnyRelayUrl, simplifyUrl } from '@/lib/url'
import client from '@/services/client.service'
import { Server } from 'lucide-react'
import { forwardRef, useCallback, useImperativeHandle, useMemo, useRef } from 'react'
const RelayPage = forwardRef<TPageRef, { url?: string }>(({ url }, ref) => {
const normalizedUrl = useMemo(() => (url ? normalizeUrl(url) : undefined), [url])
const normalizedUrl = useMemo(() => (url ? normalizeAnyRelayUrl(url) : undefined), [url])
const layoutRef = useRef<TPageRef>(null)
const feedRef = useRef<TNoteListRef>(null)

2
src/pages/secondary/InterestListPage/index.tsx

@ -27,7 +27,7 @@ import { toNoteList } from '@/lib/link' @@ -27,7 +27,7 @@ import { toNoteList } from '@/lib/link'
import { fetchLatestReplaceableListEvent } from '@/lib/replaceable-list-latest'
import { cn } from '@/lib/utils'
import { useSmartHashtagNavigation } from '@/PageManager'
import { useInterestList } from '@/providers/InterestListProvider'
import { useInterestList } from '@/providers/interest-list-context'
import { useNostr } from '@/providers/NostrProvider'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import client from '@/services/client.service'

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

@ -17,7 +17,7 @@ import { usePrimaryNoteView } from '@/contexts/primary-note-view-context' @@ -17,7 +17,7 @@ import { usePrimaryNoteView } from '@/contexts/primary-note-view-context'
import { useSecondaryPage } from '@/PageManager'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useNostr } from '@/providers/NostrProvider'
import { useInterestListOptional } from '@/providers/InterestListProvider'
import { useInterestListOptional } from '@/providers/interest-list-context'
import client from '@/services/client.service'
import { TFeedSubRequest } from '@/types'
import { UserRound, Plus } from 'lucide-react'

4
src/pages/secondary/RelayPage/index.tsx

@ -3,7 +3,7 @@ import Relay from '@/components/Relay' @@ -3,7 +3,7 @@ import Relay from '@/components/Relay'
import { RefreshButton } from '@/components/RefreshButton'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { usePrimaryNoteView } from '@/contexts/primary-note-view-context'
import { normalizeUrl, simplifyUrl } from '@/lib/url'
import { normalizeAnyRelayUrl, simplifyUrl } from '@/lib/url'
import client from '@/services/client.service'
import { forwardRef, useCallback, useEffect, useMemo, useRef } from 'react'
import NotFoundPage from '../NotFoundPage'
@ -11,7 +11,7 @@ import NotFoundPage from '../NotFoundPage' @@ -11,7 +11,7 @@ import NotFoundPage from '../NotFoundPage'
const RelayPage = forwardRef(({ url, index, hideTitlebar = false }: { url?: string; index?: number; hideTitlebar?: boolean }, ref) => {
const { registerPrimaryPanelRefresh } = usePrimaryNoteView()
const feedRef = useRef<TNoteListRef>(null)
const normalizedUrl = useMemo(() => (url ? normalizeUrl(url) : undefined), [url])
const normalizedUrl = useMemo(() => (url ? normalizeAnyRelayUrl(url) : undefined), [url])
const title = useMemo(() => (url ? simplifyUrl(url) : undefined), [url])
const bumpFeed = useCallback(() => {

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

@ -5,7 +5,7 @@ import { FAST_READ_RELAY_URLS, ExtendedKind } from '@/constants' @@ -5,7 +5,7 @@ import { FAST_READ_RELAY_URLS, ExtendedKind } from '@/constants'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { usePrimaryNoteView } from '@/contexts/primary-note-view-context'
import { relayReviewDTagsForRelayUrl, relayReviewsFeedSnapshotKey } from '@/lib/relay-review-feed'
import { normalizeUrl, simplifyUrl } from '@/lib/url'
import { normalizeAnyRelayUrl, simplifyUrl } from '@/lib/url'
import type { TFeedSubRequest } from '@/types'
import { forwardRef, useCallback, useEffect, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
@ -26,7 +26,7 @@ const RelayReviewsPage = forwardRef(({ url, index, hideTitlebar = false }: { url @@ -26,7 +26,7 @@ const RelayReviewsPage = forwardRef(({ url, index, hideTitlebar = false }: { url
return () => registerPrimaryPanelRefresh(null)
}, [hideTitlebar, registerPrimaryPanelRefresh, bumpFeed])
const normalizedUrl = useMemo(() => (url ? normalizeUrl(url) : undefined), [url])
const normalizedUrl = useMemo(() => (url ? normalizeAnyRelayUrl(url) : undefined), [url])
/** `d` tag values vary by client (raw vs normalized URL); REQ must OR-match every variant. */
const relayReviewDTags = useMemo(
() => (url ? relayReviewDTagsForRelayUrl(url) : []),

20
src/providers/FavoriteRelaysProvider.tsx

@ -3,7 +3,7 @@ import { createFavoriteRelaysDraftEvent, createBlockedRelaysDraftEvent, createRe @@ -3,7 +3,7 @@ import { createFavoriteRelaysDraftEvent, createBlockedRelaysDraftEvent, createRe
import { getReplaceableEventIdentifier } from '@/lib/event'
import { getRelaySetFromEvent } from '@/lib/event-metadata'
import { randomString } from '@/lib/random'
import { isWebsocketUrl, normalizeUrl } from '@/lib/url'
import { isHttpRelayUrl, isWebsocketUrl, normalizeAnyRelayUrl, normalizeUrl } from '@/lib/url'
import { queryService } from '@/services/client.service'
import indexedDb from '@/services/indexed-db.service'
import storage from '@/services/local-storage.service'
@ -54,7 +54,7 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode @@ -54,7 +54,7 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
if (!tagValue) return
if (tagName === 'relay') {
const normalizedUrl = normalizeUrl(tagValue)
const normalizedUrl = normalizeAnyRelayUrl(tagValue)
if (normalizedUrl && !relays.includes(normalizedUrl)) {
relays.push(normalizedUrl)
}
@ -84,7 +84,7 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode @@ -84,7 +84,7 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
setRelaySetEvents(storedRelaySetEvents.filter(Boolean) as Event[])
const normalizedRelays = [
...(relayList?.write ?? []).map(url => normalizeUrl(url) || url),
...(relayList?.write ?? []).map(url => normalizeAnyRelayUrl(url) || url),
...FAST_READ_RELAY_URLS.map(url => normalizeUrl(url) || url)
]
const newRelaySetEvents = await queryService.fetchEvents(
@ -133,7 +133,7 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode @@ -133,7 +133,7 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
const relays: string[] = []
blockedRelaysEvent.tags.forEach(([tagName, tagValue]) => {
if (tagName === 'relay' && tagValue) {
const normalizedUrl = normalizeUrl(tagValue)
const normalizedUrl = normalizeAnyRelayUrl(tagValue)
if (normalizedUrl && !relays.includes(normalizedUrl)) {
relays.push(normalizedUrl)
}
@ -151,7 +151,7 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode @@ -151,7 +151,7 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
const addFavoriteRelays = useCallback(
async (relayUrls: string[]) => {
const normalizedUrls = relayUrls
.map((relayUrl) => normalizeUrl(relayUrl))
.map((relayUrl) => normalizeAnyRelayUrl(relayUrl))
.filter((url) => !!url && !favoriteRelays.includes(url))
if (!normalizedUrls.length) return
@ -168,7 +168,7 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode @@ -168,7 +168,7 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
const deleteFavoriteRelays = useCallback(
async (relayUrls: string[]) => {
const normalizedUrls = relayUrls
.map((relayUrl) => normalizeUrl(relayUrl))
.map((relayUrl) => normalizeAnyRelayUrl(relayUrl))
.filter((url) => !!url && favoriteRelays.includes(url))
if (!normalizedUrls.length) return
@ -185,8 +185,8 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode @@ -185,8 +185,8 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
const createRelaySet = useCallback(
async (relaySetName: string, relayUrls: string[] = []) => {
const normalizedUrls = relayUrls
.map((url) => normalizeUrl(url))
.filter((url) => isWebsocketUrl(url))
.map((url) => normalizeAnyRelayUrl(url))
.filter((url) => isWebsocketUrl(url) || isHttpRelayUrl(url))
const id = randomString()
const relaySetDraftEvent = createRelaySetDraftEvent({
id,
@ -271,7 +271,7 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode @@ -271,7 +271,7 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
const addBlockedRelays = useCallback(
async (relayUrls: string[]) => {
const normalizedUrls = relayUrls
.map((relayUrl) => normalizeUrl(relayUrl))
.map((relayUrl) => normalizeAnyRelayUrl(relayUrl))
.filter((url) => !!url && !blockedRelays.includes(url))
if (!normalizedUrls.length) return
const newBlockedRelays = [...blockedRelays, ...normalizedUrls]
@ -285,7 +285,7 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode @@ -285,7 +285,7 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
const deleteBlockedRelays = useCallback(
async (relayUrls: string[]) => {
const normalizedUrls = relayUrls.map((relayUrl) => normalizeUrl(relayUrl)).filter(Boolean)
const normalizedUrls = relayUrls.map((relayUrl) => normalizeAnyRelayUrl(relayUrl)).filter(Boolean)
const newBlockedRelays = blockedRelays.filter((relay) => !normalizedUrls.includes(relay))
setBlockedRelays(newBlockedRelays)
const draftEvent = createBlockedRelaysDraftEvent(newBlockedRelays)

12
src/providers/FeedProvider.tsx

@ -2,7 +2,7 @@ import { DEFAULT_FAVORITE_RELAYS } from '@/constants' @@ -2,7 +2,7 @@ import { DEFAULT_FAVORITE_RELAYS } from '@/constants'
import { getFavoritesFeedRelayUrls } from '@/lib/favorites-feed-relays'
import { getRelaySetFromEvent } from '@/lib/event-metadata'
import logger from '@/lib/logger'
import { isWebsocketUrl, normalizeUrl } from '@/lib/url'
import { isHttpRelayUrl, isWebsocketUrl, normalizeAnyRelayUrl } from '@/lib/url'
import indexedDb from '@/services/indexed-db.service'
import storage from '@/services/local-storage.service'
import { TFeedInfo, TFeedType } from '@/types'
@ -38,10 +38,12 @@ export function FeedProvider({ children }: { children: React.ReactNode }) { @@ -38,10 +38,12 @@ export function FeedProvider({ children }: { children: React.ReactNode }) {
logger.debug('switchFeed called:', { feedType, options })
setIsReady(false)
if (feedType === 'relay') {
const normalizedUrl = normalizeUrl(options.relay ?? '')
logger.debug('Relay switchFeed:', { normalizedUrl, isWebsocketUrl: isWebsocketUrl(normalizedUrl), blockedRelays })
if (!normalizedUrl || !isWebsocketUrl(normalizedUrl)) {
const normalizedUrl = normalizeAnyRelayUrl(options.relay ?? '')
const isRelayFeedUrl =
!!normalizedUrl && (isHttpRelayUrl(normalizedUrl) || isWebsocketUrl(normalizedUrl))
logger.debug('Relay switchFeed:', { normalizedUrl, isRelayFeedUrl, blockedRelays })
if (!isRelayFeedUrl) {
logger.debug('Invalid relay URL, setting isReady to true')
setIsReady(true)
return

24
src/providers/FollowListProvider.tsx

@ -8,31 +8,11 @@ import { @@ -8,31 +8,11 @@ import {
import { getPubkeysFromPTags } from '@/lib/tag'
import client from '@/services/client.service'
import { kinds } from 'nostr-tools'
import { createContext, useContext, useMemo, useCallback } from 'react'
import { useMemo, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useNostr } from './NostrProvider'
import { useFavoriteRelays } from './FavoriteRelaysProvider'
type TFollowListContext = {
followings: string[]
follow: (pubkey: string) => Promise<void>
unfollow: (pubkey: string) => Promise<void>
}
const FollowListContext = createContext<TFollowListContext | undefined>(undefined)
export const useFollowList = () => {
const context = useContext(FollowListContext)
if (!context) {
throw new Error('useFollowList must be used within a FollowListProvider')
}
return context
}
/** Same as {@link useFollowList} but returns undefined outside the provider (avoids HMR / refresh-boundary crashes). */
export function useFollowListOptional(): TFollowListContext | undefined {
return useContext(FollowListContext)
}
import { FollowListContext } from './follow-list-context'
export function FollowListProvider({ children }: { children: React.ReactNode }) {
const { t } = useTranslation()

20
src/providers/GroupListProvider.tsx

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import { createContext, useContext, useEffect, useState, useCallback, useMemo } from 'react'
import { useEffect, useState, useCallback, useMemo } from 'react'
import { useNostr } from '@/providers/NostrProvider'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { ExtendedKind } from '@/constants'
@ -7,23 +7,7 @@ import { fetchLatestReplaceableListEvent } from '@/lib/replaceable-list-latest' @@ -7,23 +7,7 @@ import { fetchLatestReplaceableListEvent } from '@/lib/replaceable-list-latest'
import { buildPrioritizedReadRelayUrls } from '@/lib/relay-url-priority'
import client from '@/services/client.service'
import logger from '@/lib/logger'
interface GroupListContextType {
userGroups: string[]
isUserInGroup: (groupId: string) => boolean
refreshGroupList: () => Promise<void>
isLoading: boolean
}
const GroupListContext = createContext<GroupListContextType | undefined>(undefined)
export const useGroupList = () => {
const context = useContext(GroupListContext)
if (context === undefined) {
throw new Error('useGroupList must be used within a GroupListProvider')
}
return context
}
import { GroupListContext } from './group-list-context'
export function GroupListProvider({ children }: { children: React.ReactNode }) {
const { pubkey: accountPubkey } = useNostr()

28
src/providers/InterestListProvider.tsx

@ -4,36 +4,12 @@ import { normalizeTopic } from '@/lib/discussion-topics' @@ -4,36 +4,12 @@ import { normalizeTopic } from '@/lib/discussion-topics'
import { fetchLatestReplaceableListEvent } from '@/lib/replaceable-list-latest'
import logger from '@/lib/logger'
import client from '@/services/client.service'
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { useNostr } from '@/providers/nostr-context'
import { useFavoriteRelays } from './FavoriteRelaysProvider'
type TInterestListContext = {
subscribedTopics: Set<string>
changing: boolean
isSubscribed: (topic: string) => boolean
subscribe: (topic: string) => Promise<void>
unsubscribe: (topic: string) => Promise<void>
getSubscribedTopics: () => string[]
}
const InterestListContext = createContext<TInterestListContext | undefined>(undefined)
export const useInterestList = () => {
const context = useContext(InterestListContext)
if (!context) {
throw new Error('useInterestList must be used within an InterestListProvider')
}
return context
}
/**
* Optional variant for routes/components that can be mounted
* during transient navigation/HMR paths before providers settle.
*/
export const useInterestListOptional = () => useContext(InterestListContext)
import { InterestListContext } from './interest-list-context'
export function InterestListProvider({ children }: { children: React.ReactNode }) {
const { t } = useTranslation()

2
src/providers/LiveActivitiesProvider.tsx

@ -21,7 +21,7 @@ import { @@ -21,7 +21,7 @@ import {
useState
} from 'react'
import { useFavoriteRelays } from './FavoriteRelaysProvider'
import { useFollowListOptional } from './FollowListProvider'
import { useFollowListOptional } from './follow-list-context'
import { useNostr } from './NostrProvider'
import { useUserPreferencesOptional } from './UserPreferencesProvider'

8
src/providers/NostrProvider/index.tsx

@ -24,7 +24,7 @@ import { getLatestEvent, minePow } from '@/lib/event' @@ -24,7 +24,7 @@ import { getLatestEvent, minePow } from '@/lib/event'
import { getHttpRelayListFromEvent, getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata'
import logger from '@/lib/logger'
import { LoginRequiredError } from '@/lib/nostr-errors'
import { normalizeHttpRelayUrl, normalizeUrl } from '@/lib/url'
import { normalizeAnyRelayUrl, normalizeUrl } from '@/lib/url'
import { formatPubkey, pubkeyToNpub } from '@/lib/pubkey'
import { showPublishingFeedback, showSimplePublishSuccess } from '@/lib/publishing-feedback'
import client from '@/services/client.service'
@ -70,7 +70,7 @@ function favoriteRelayUrlsForPublish(favoriteRelaysEvent: Event | null, pubkey: @@ -70,7 +70,7 @@ function favoriteRelayUrlsForPublish(favoriteRelaysEvent: Event | null, pubkey:
const urls: string[] = []
favoriteRelaysEvent.tags.forEach(([name, v]) => {
if (name === 'relay' && v) {
const n = normalizeUrl(v) || v
const n = normalizeAnyRelayUrl(v) || v
if (n && !urls.includes(n)) urls.push(n)
}
})
@ -82,7 +82,7 @@ function blockedRelayUrlsFromEvent(blockedRelaysEvent: Event | null): string[] { @@ -82,7 +82,7 @@ function blockedRelayUrlsFromEvent(blockedRelaysEvent: Event | null): string[] {
if (!blockedRelaysEvent) return out
blockedRelaysEvent.tags.forEach(([tagName, tagValue]) => {
if (tagName === 'relay' && tagValue) {
const n = normalizeUrl(tagValue)
const n = normalizeAnyRelayUrl(tagValue)
if (n && !out.includes(n)) out.push(n)
}
})
@ -477,8 +477,6 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -477,8 +477,6 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
const normalizedRelays = [
...mergedRelayList.write.map((url: string) => normalizeUrl(url) || url),
...mergedRelayList.read.map((url: string) => normalizeUrl(url) || url),
...mergedRelayList.httpRead.map((url: string) => normalizeHttpRelayUrl(url) || url),
...mergedRelayList.httpWrite.map((url: string) => normalizeHttpRelayUrl(url) || url),
...FAST_WRITE_RELAY_URLS.map((url: string) => normalizeUrl(url) || url),
...PROFILE_FETCH_RELAY_URLS.map((url: string) => normalizeUrl(url) || url)
]

22
src/providers/follow-list-context.tsx

@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
import { createContext, useContext } from 'react'
export type TFollowListContext = {
followings: string[]
follow: (pubkey: string) => Promise<void>
unfollow: (pubkey: string) => Promise<void>
}
export const FollowListContext = createContext<TFollowListContext | undefined>(undefined)
export const useFollowList = (): TFollowListContext => {
const context = useContext(FollowListContext)
if (!context) {
throw new Error('useFollowList must be used within a FollowListProvider')
}
return context
}
/** Same as {@link useFollowList} but returns undefined outside the provider (avoids HMR / refresh-boundary crashes). */
export function useFollowListOptional(): TFollowListContext | undefined {
return useContext(FollowListContext)
}

18
src/providers/group-list-context.tsx

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
import { createContext, useContext } from 'react'
export interface GroupListContextType {
userGroups: string[]
isUserInGroup: (groupId: string) => boolean
refreshGroupList: () => Promise<void>
isLoading: boolean
}
export const GroupListContext = createContext<GroupListContextType | undefined>(undefined)
export const useGroupList = (): GroupListContextType => {
const context = useContext(GroupListContext)
if (context === undefined) {
throw new Error('useGroupList must be used within a GroupListProvider')
}
return context
}

27
src/providers/interest-list-context.tsx

@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
import { createContext, useContext } from 'react'
export type TInterestListContext = {
subscribedTopics: Set<string>
changing: boolean
isSubscribed: (topic: string) => boolean
subscribe: (topic: string) => Promise<void>
unsubscribe: (topic: string) => Promise<void>
getSubscribedTopics: () => string[]
}
export const InterestListContext = createContext<TInterestListContext | undefined>(undefined)
export const useInterestList = (): TInterestListContext => {
const context = useContext(InterestListContext)
if (!context) {
throw new Error('useInterestList must be used within an InterestListProvider')
}
return context
}
/**
* Optional variant for routes/components that can be mounted
* during transient navigation/HMR paths before providers settle.
*/
export const useInterestListOptional = (): TInterestListContext | undefined =>
useContext(InterestListContext)

132
src/services/client.service.ts

@ -106,7 +106,7 @@ import { @@ -106,7 +106,7 @@ import {
import {
IndexRelayTransportError,
isIndexRelayTransportFailure,
publishEventToIndexRelay
publishEventToHttpRelay
} from '@/lib/index-relay-http'
import {
relayFiltersUseCapitalLetterTagKeys,
@ -285,6 +285,14 @@ class ClientService extends EventTarget { @@ -285,6 +285,14 @@ class ClientService extends EventTarget {
params?: { connectionTimeout?: number; abort?: AbortSignal }
) => {
const n = normalizeUrl(url) || url
// ── DIAGNOSTIC: catch any local-network WS attempt so we can trace its origin ──
if (isLocalNetworkUrl(n)) {
logger.warn('[DIAG] pool.ensureRelay called with LOCAL-NETWORK WS URL', {
url,
normalizedUrl: n,
stack: new Error('stack').stack?.split('\n').slice(1, 8).join(' | ')
})
}
const base = params?.connectionTimeout ?? RELAY_POOL_CONNECTION_TIMEOUT_MS
const connectionTimeout = READ_ONLY_RELAY_CONNECT_BOOST_URLS.has(n)
? Math.max(base, RELAY_READ_ONLY_POOL_CONNECT_TIMEOUT_MS)
@ -429,7 +437,7 @@ class ClientService extends EventTarget { @@ -429,7 +437,7 @@ class ClientService extends EventTarget {
*/
async fetchNip66DiscoveryForRelay(relayUrl: string): Promise<void> {
const discoveryRelays = Array.from(new Set([...FAST_READ_RELAY_URLS, ...NIP66_DISCOVERY_RELAY_URLS]))
const dTag = normalizeUrl(relayUrl) || relayUrl
const dTag = normalizeAnyRelayUrl(relayUrl) || relayUrl
const shortForm = simplifyUrl(dTag)
const dValues = dTag !== shortForm ? [dTag, shortForm] : [dTag]
try {
@ -576,11 +584,10 @@ class ClientService extends EventTarget { @@ -576,11 +584,10 @@ class ClientService extends EventTarget {
let userWriteSet = new Set<string>()
try {
const rl = await this.fetchRelayList(event.pubkey)
userWriteSet = new Set(
(rl?.write ?? [])
.map((u) => normalizeUrl(u) || u)
.filter((u): u is string => !!u)
)
userWriteSet = new Set([
...(rl?.write ?? []).map((u) => normalizeUrl(u) || u).filter((u): u is string => !!u),
...(rl?.httpWrite ?? []).map((u) => normalizeHttpRelayUrl(u) || u).filter((u): u is string => !!u)
])
} catch {
// ignore
}
@ -617,7 +624,7 @@ class ClientService extends EventTarget { @@ -617,7 +624,7 @@ class ClientService extends EventTarget {
const t4: string[] = []
const t5: string[] = []
for (const u of relayUrls) {
const n = normalizeUrl(u) || u
const n = normalizeAnyRelayUrl(u) || u
if (!n) continue
if (userWriteSet.has(n)) t0.push(n)
else if (authorReadSet.has(n)) t1.push(n)
@ -628,7 +635,7 @@ class ClientService extends EventTarget { @@ -628,7 +635,7 @@ class ClientService extends EventTarget {
}
return dedupeNormalizeRelayUrlsOrdered([...t0, ...t1, ...t2, ...t3, ...t4, ...t5])
.filter((url) => {
const n = normalizeUrl(url) || url
const n = normalizeAnyRelayUrl(url) || url
if (readOnlySet.has(n)) return false
if (isSocialKindBlockedKind(event.kind) && socialKindBlockedSet.has(n)) return false
return true
@ -673,9 +680,13 @@ class ClientService extends EventTarget { @@ -673,9 +680,13 @@ class ClientService extends EventTarget {
if (event.kind === kinds.Report) {
// Start with user's write relays (outboxes) - these are the primary targets for reports
const relayList = await this.fetchRelayList(event.pubkey)
const userWriteRelays = dedupeNormalizeRelayUrlsOrdered(
(relayList?.write ?? []).map((url) => normalizeUrl(url) || url).filter((u): u is string => !!u)
)
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])
// Get seen relays where the reported event was found
const targetEventId = event.tags.find(tagNameEquals('e'))?.[1]
@ -685,9 +696,9 @@ class ClientService extends EventTarget { @@ -685,9 +696,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 => normalizeUrl(url) || url))
const userWriteRelaySet = new Set(userWriteRelays.map(url => normalizeAnyRelayUrl(url) || url))
seenRelays.push(...allSeenRelays.filter(url => {
const normalized = normalizeUrl(url) || url
const normalized = normalizeAnyRelayUrl(url) || url
return userWriteRelaySet.has(normalized)
}))
}
@ -722,8 +733,14 @@ class ClientService extends EventTarget { @@ -722,8 +733,14 @@ class ClientService extends EventTarget {
event.kind === ExtendedKind.PUBLIC_MESSAGE ||
event.kind === ExtendedKind.CALENDAR_EVENT_RSVP
) {
const authorRelayList = await this.fetchRelayList(event.pubkey).catch(() => ({ write: [] as string[], read: [] as string[] }))
let authorWrite = (authorRelayList?.write ?? []).map((url) => normalizeUrl(url)).filter(Boolean) as string[]
const authorRelayList = await this.fetchRelayList(event.pubkey).catch(() => ({ write: [] as string[], read: [] as string[], httpWrite: [] as string[], httpRead: [] as string[] }))
const authorHttpWrites = (authorRelayList?.httpWrite ?? [])
.map((url) => normalizeHttpRelayUrl(url))
.filter((url): url is string => !!url)
const authorWsWrites = (authorRelayList?.write ?? [])
.map((url) => normalizeUrl(url))
.filter((url): url is string => !!url)
let authorWrite = dedupeNormalizeRelayUrlsOrdered([...authorHttpWrites, ...authorWsWrites])
if (authorWrite.length === 0) {
authorWrite = [...FAST_WRITE_RELAY_URLS]
}
@ -735,10 +752,11 @@ class ClientService extends EventTarget { @@ -735,10 +752,11 @@ class ClientService extends EventTarget {
let recipientRead: string[] = []
if (recipientPubkeys.length > 0) {
const recipientRelayLists = await this.fetchRelayLists(recipientPubkeys)
recipientRead = recipientRelayLists.flatMap((rl) => rl?.read ?? [])
recipientRead = recipientRead
.map((url) => normalizeUrl(url))
.filter((url): url is string => !!url && !isLocalNetworkUrl(url))
recipientRead = recipientRelayLists.flatMap((rl) => [
...(rl?.httpRead ?? []).map((url) => normalizeHttpRelayUrl(url)).filter((u): u is string => !!u && !isLocalNetworkUrl(u)),
...(rl?.read ?? []).map((url) => normalizeUrl(url)).filter((u): u is string => !!u && !isLocalNetworkUrl(u))
])
recipientRead = dedupeNormalizeRelayUrlsOrdered(recipientRead)
}
let pubRelays = mergeRelayPriorityLayers(
[relayUrlsLocalsFirst(authorWrite), dedupeNormalizeRelayUrlsOrdered(recipientRead)],
@ -791,14 +809,16 @@ class ClientService extends EventTarget { @@ -791,14 +809,16 @@ class ClientService extends EventTarget {
httpOriginalRelays: []
}
}
const normalizedWrite = dedupeNormalizeRelayUrlsOrdered(
(spellRelayList?.write ?? [])
.map((url) => normalizeUrl(url))
.filter((url): url is string => !!url)
)
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 readOnlySet = new Set(READ_ONLY_RELAY_URLS.map((u) => normalizeUrl(u) || u))
const spellWriteFiltered = normalizedWrite.filter((url) => {
const n = normalizeUrl(url) || url
const n = normalizeAnyRelayUrl(url) || url
return !readOnlySet.has(n)
})
return this.filterPublishingRelays(
@ -826,6 +846,10 @@ class ClientService extends EventTarget { @@ -826,6 +846,10 @@ class ClientService extends EventTarget {
if (ctxPubkeys.length > 0) {
const relayLists = await this.fetchRelayLists(ctxPubkeys)
relayLists.forEach((relayList) => {
for (const u of relayList.httpRead ?? []) {
const n = normalizeHttpRelayUrl(u) || u
if (n) authorInboxFromContext.push(n)
}
for (const u of relayList.read ?? []) {
const n = normalizeUrl(u) || u
if (n) authorInboxFromContext.push(n)
@ -904,9 +928,13 @@ class ClientService extends EventTarget { @@ -904,9 +928,13 @@ class ClientService extends EventTarget {
writeRelays: relayList?.write?.slice(0, MAX_PUBLISH_RELAYS) ?? []
})
}
const userWritesOrdered = dedupeNormalizeRelayUrlsOrdered(
(relayList?.write ?? []).map((u) => normalizeUrl(u) || u).filter((u): u is string => !!u)
)
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 = dedupeNormalizeRelayUrlsOrdered([...httpWrites, ...wsWrites])
relays = this.filterPublishingRelays(
buildPrioritizedWriteRelayUrls({
userWriteRelays: userWritesOrdered,
@ -1005,6 +1033,14 @@ class ClientService extends EventTarget { @@ -1005,6 +1033,14 @@ class ClientService extends EventTarget {
private recordSessionRelayFailure(url: string) {
const n = normalizeAnyRelayUrl(url) || url
if (!n) return
// ── DIAGNOSTIC: trace who is recording failures for local-network relays ──
if (isLocalNetworkUrl(n)) {
logger.warn('[DIAG] recordSessionRelayFailure for LOCAL-NETWORK relay', {
url,
normalizedUrl: n,
stack: new Error('stack').stack?.split('\n').slice(1, 8).join(' | ')
})
}
const prev = this.publishStrikeCount.get(n) ?? 0
if (prev >= ClientService.SESSION_RELAY_FAILURE_STRIKE_THRESHOLD) {
return
@ -1373,7 +1409,7 @@ class ClientService extends EventTarget { @@ -1373,7 +1409,7 @@ class ClientService extends EventTarget {
const base = normalizeHttpRelayUrl(url) || url
logger.debug(`[PublishEvent] Publishing to HTTP index relay`, { url: base })
await Promise.race([
publishEventToIndexRelay(base, event),
publishEventToHttpRelay(base, event),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error(`HTTP publish timeout after ${publishTimeout}ms`)), publishTimeout)
)
@ -1906,6 +1942,15 @@ class ClientService extends EventTarget { @@ -1906,6 +1942,15 @@ class ClientService extends EventTarget {
relayReqLog?: { groupId?: string; onBatchEnd?: (rows: RelayOpTerminalRow[]) => void }
) {
const originalDedupedRelays = Array.from(new Set(urls))
// ── DIAGNOSTIC: trace local-network URLs entering subscribe() ──
const localInSubscribe = originalDedupedRelays.filter((u) => isLocalNetworkUrl(normalizeAnyRelayUrl(u) || u))
if (localInSubscribe.length > 0) {
logger.warn('[DIAG] subscribe() received LOCAL-NETWORK relay URLs', {
localUrls: localInSubscribe,
allUrls: originalDedupedRelays,
stack: new Error('stack').stack?.split('\n').slice(1, 8).join(' | ')
})
}
let relays = originalDedupedRelays.filter((url) => !isHttpRelayUrl(url))
const filters = sanitizeSubscribeFiltersBeforeReq(filter)
if (filters.length === 0) {
@ -2263,6 +2308,16 @@ class ClientService extends EventTarget { @@ -2263,6 +2308,16 @@ class ClientService extends EventTarget {
} = {}
) {
let relays = Array.from(new Set(urls))
// ── DIAGNOSTIC: trace local-network URLs entering _subscribeTimeline ──
const localInTimeline = relays.filter((u) => isLocalNetworkUrl(normalizeAnyRelayUrl(u) || u))
if (localInTimeline.length > 0) {
logger.warn('[DIAG] _subscribeTimeline received LOCAL-NETWORK relay URLs', {
localUrls: localInTimeline,
allUrls: relays,
httpOnes: relays.filter((u) => isHttpRelayUrl(u)),
stack: new Error('stack').stack?.split('\n').slice(1, 10).join(' | ')
})
}
if (relayFiltersUseCapitalLetterTagKeys(filter as Filter)) {
relays = relayUrlsStripExtendedTagReqBlocked(relays)
if (relays.length === 0) {
@ -2605,7 +2660,7 @@ class ClientService extends EventTarget { @@ -2605,7 +2660,7 @@ class ClientService extends EventTarget {
getSeenEventRelayUrls(eventId: string): string[] {
const key = canonicalSeenOnEventId(eventId)
const poolUrls = this.getSeenEventRelays(key).map((r) => normalizeUrl(r.url) || r.url)
const queryUrls = this.queryService.getSeenEventRelayUrls(key).map((u) => normalizeUrl(u) || u)
const queryUrls = this.queryService.getSeenEventRelayUrls(key).map((u) => normalizeAnyRelayUrl(u) || u)
return Array.from(new Set([...poolUrls, ...queryUrls].filter(Boolean)))
}
@ -2717,10 +2772,21 @@ class ClientService extends EventTarget { @@ -2717,10 +2772,21 @@ class ClientService extends EventTarget {
filter: Filter | Filter[],
options?: { globalTimeout?: number }
): Promise<{ events: NEvent[]; connectionError?: string }> {
const normalized = normalizeUrl(url) || url
const normalized = normalizeAnyRelayUrl(url) || url
if (!normalized) {
return { events: [], connectionError: 'Invalid relay URL' }
}
if (isHttpRelayUrl(normalized)) {
// HTTP index relay: use HTTP API instead of WebSocket pool
try {
const events = await this.queryService.query([normalized], filter, undefined, {
globalTimeout: options?.globalTimeout ?? 25_000
})
return { events, connectionError: undefined }
} catch (e) {
return { events: [], connectionError: e instanceof Error ? e.message : String(e) }
}
}
const usableAfterStrikes = this.relayUrlsAfterStrikesOrRecover([normalized])
if (usableAfterStrikes.length === 0) {
return { events: [], connectionError: 'Relay skipped this session (repeated failures)' }
@ -3476,8 +3542,6 @@ class ClientService extends EventTarget { @@ -3476,8 +3542,6 @@ class ClientService extends EventTarget {
const urls = dedupeNormalizeRelayUrlsOrdered([
...relayList.write.map((u) => normalizeUrl(u) || u),
...relayList.read.map((u) => normalizeUrl(u) || u),
...relayList.httpRead.map((u) => normalizeHttpRelayUrl(u) || u),
...relayList.httpWrite.map((u) => normalizeHttpRelayUrl(u) || u),
...FAST_READ_RELAY_URLS.map((u) => normalizeUrl(u) || u),
...PROFILE_FETCH_RELAY_URLS.map((u) => normalizeUrl(u) || u)
]).filter(Boolean)

8
src/services/relay-info.service.ts

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import { simplifyUrl } from '@/lib/url'
import { devProxyLoopbackHttpRelayBase, normalizeHttpRelayUrl, simplifyUrl } from '@/lib/url'
import indexDb from '@/services/indexed-db.service'
import { TAwesomeRelayCollection, TRelayInfo } from '@/types'
import DataLoader from 'dataloader'
@ -148,10 +148,14 @@ class RelayInfoService { @@ -148,10 +148,14 @@ class RelayInfoService {
private async fetchRelayNip11(url: string) {
try {
logger.debug('Fetching NIP-11 metadata', { url })
const res = await fetchWithTimeout(url.replace('ws://', 'http://').replace('wss://', 'https://'), {
const httpCandidate = url.trim().replace(/^ws:\/\//i, 'http://').replace(/^wss:\/\//i, 'https://')
const httpBase = normalizeHttpRelayUrl(httpCandidate) || httpCandidate
const fetchUrl = devProxyLoopbackHttpRelayBase(httpBase)
const res = await fetchWithTimeout(fetchUrl, {
headers: { Accept: 'application/nostr+json' },
timeoutMs: 12_000
})
if (!res.ok) return undefined
return res.json() as Omit<TRelayInfo, 'url' | 'shortUrl'>
} catch {
return undefined

4
src/services/relay-operation-log.service.ts

@ -1,11 +1,11 @@ @@ -1,11 +1,11 @@
import logger from '@/lib/logger'
import { normalizeUrl } from '@/lib/url'
import { normalizeAnyRelayUrl } from '@/lib/url'
import type { Filter } from 'nostr-tools'
let batchSeq = 0
function relayHostForPublishLog(url: string): string {
const n = normalizeUrl(url) || url
const n = normalizeAnyRelayUrl(url) || url
try {
const u = new URL(n.replace(/^wss:/i, 'https:').replace(/^ws:/i, 'http:'))
const path = u.pathname && u.pathname !== '/' ? u.pathname.replace(/\/$/, '') : ''

2
vite.config.ts

@ -85,7 +85,7 @@ export default defineConfig(({ mode }) => { @@ -85,7 +85,7 @@ export default defineConfig(({ mode }) => {
// `.env.local` is not on `process.env` when this file is evaluated unless we load it.
const env = loadEnv(mode, process.cwd(), '')
const devIndexRelayTarget =
env.VITE_DEV_INDEX_RELAY_TARGET?.trim() || 'http://127.0.0.1:1122'
env.VITE_DEV_INDEX_RELAY_TARGET?.trim() || 'http://127.0.0.1:4000'
return {
base: '/',

Loading…
Cancel
Save