9 changed files with 348 additions and 52 deletions
@ -0,0 +1,46 @@
@@ -0,0 +1,46 @@
|
||||
import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays' |
||||
import { viewerMayUseNostrLandAggr } from '@/lib/nostr-land-aggr' |
||||
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' |
||||
import { useNostrOptional } from '@/providers/nostr-context' |
||||
import client from '@/services/client.service' |
||||
import type { TRelayList } from '@/types' |
||||
import { useEffect, useMemo, useState } from 'react' |
||||
|
||||
/** |
||||
* Viewer NIP-65 read inboxes (incl. HTTP read) for embed / thread fan-out, plus whether they may use |
||||
* {@link AGGR_NOSTR_LAND_WSS} (nostr.land in favorites or NIP-65 lists). |
||||
*/ |
||||
export function useViewerInboxRelayUrlsAndAggrEligibility(): { |
||||
inboxRelayUrls: string[] |
||||
allowNostrLandAggr: boolean |
||||
} { |
||||
const nostr = useNostrOptional() |
||||
const pk = nostr?.pubkey?.trim() |
||||
const { favoriteRelays } = useFavoriteRelays() |
||||
const [inboxRelayUrls, setInboxRelayUrls] = useState<string[]>([]) |
||||
const [peekedNip65, setPeekedNip65] = useState<TRelayList | null>(null) |
||||
|
||||
useEffect(() => { |
||||
if (!pk) { |
||||
setInboxRelayUrls([]) |
||||
setPeekedNip65(null) |
||||
return |
||||
} |
||||
let cancelled = false |
||||
void client.peekRelayListFromStorage(pk).then((rl) => { |
||||
if (cancelled) return |
||||
setPeekedNip65(rl) |
||||
setInboxRelayUrls(userReadRelaysWithHttp(rl).slice(0, 14)) |
||||
}) |
||||
return () => { |
||||
cancelled = true |
||||
} |
||||
}, [pk]) |
||||
|
||||
const allowNostrLandAggr = useMemo( |
||||
() => viewerMayUseNostrLandAggr(favoriteRelays ?? [], peekedNip65 ?? undefined), |
||||
[favoriteRelays, peekedNip65] |
||||
) |
||||
|
||||
return { inboxRelayUrls, allowNostrLandAggr } |
||||
} |
||||
@ -0,0 +1,74 @@
@@ -0,0 +1,74 @@
|
||||
import type { TRelayList } from '@/types' |
||||
import { normalizeAnyRelayUrl } from '@/lib/url' |
||||
|
||||
/** Paid / subscription relay — presence in favorites or NIP-65 lists implies access to the matching aggregator. */ |
||||
export const NOSTR_LAND_WSS = 'wss://nostr.land' |
||||
|
||||
/** Aggregator for nostr.land subscribers only; others get auth / policy errors if contacted. */ |
||||
export const AGGR_NOSTR_LAND_WSS = 'wss://aggr.nostr.land' |
||||
|
||||
function canonWs(url: string): string { |
||||
return (normalizeAnyRelayUrl(url) || url.trim()).toLowerCase() |
||||
} |
||||
|
||||
const NOSTR_LAND_CANON = canonWs(NOSTR_LAND_WSS) |
||||
const AGGR_CANON = canonWs(AGGR_NOSTR_LAND_WSS) |
||||
|
||||
/** True if this URL is the nostr.land websocket (normalized). */ |
||||
export function isNostrLandWsUrl(url: string | undefined | null): boolean { |
||||
if (!url?.trim()) return false |
||||
return canonWs(url) === NOSTR_LAND_CANON |
||||
} |
||||
|
||||
/** True if any normalized URL equals nostr.land. */ |
||||
export function relayUrlListMentionsNostrLand(urls: readonly string[] | undefined): boolean { |
||||
if (!urls?.length) return false |
||||
for (const u of urls) { |
||||
if (isNostrLandWsUrl(u)) return true |
||||
} |
||||
return false |
||||
} |
||||
|
||||
/** True if nostr.land appears in any NIP-65 / HTTP relay list slice. */ |
||||
export function nip65RelayListMentionsNostrLand(rl: TRelayList | null | undefined): boolean { |
||||
if (!rl) return false |
||||
return ( |
||||
relayUrlListMentionsNostrLand(rl.read) || |
||||
relayUrlListMentionsNostrLand(rl.write) || |
||||
relayUrlListMentionsNostrLand(rl.httpRead) || |
||||
relayUrlListMentionsNostrLand(rl.httpWrite) |
||||
) |
||||
} |
||||
|
||||
/** |
||||
* Subscriber may use {@link AGGR_NOSTR_LAND_WSS}: they listed nostr.land in kind-10012 favorites or NIP-65 lists. |
||||
*/ |
||||
export function viewerMayUseNostrLandAggr( |
||||
favoriteRelays: readonly string[] | undefined, |
||||
nip65: TRelayList | null | undefined |
||||
): boolean { |
||||
if (relayUrlListMentionsNostrLand(favoriteRelays)) return true |
||||
return nip65RelayListMentionsNostrLand(nip65 ?? undefined) |
||||
} |
||||
|
||||
/** |
||||
* Drop {@link AGGR_NOSTR_LAND_WSS} when the viewer is not a nostr.land subscriber; otherwise ensure it appears once. |
||||
*/ |
||||
export function applyNostrLandAggrRelayPolicy(urls: readonly string[], allowAggr: boolean): string[] { |
||||
const out: string[] = [] |
||||
const seen = new Set<string>() |
||||
const push = (u: string) => { |
||||
const c = canonWs(u) |
||||
if (!c || seen.has(c)) return |
||||
if (!allowAggr && c === AGGR_CANON) return |
||||
seen.add(c) |
||||
out.push(normalizeAnyRelayUrl(u) || u.trim()) |
||||
} |
||||
for (const u of urls) { |
||||
push(u) |
||||
} |
||||
if (allowAggr && !seen.has(AGGR_CANON)) { |
||||
out.unshift(AGGR_NOSTR_LAND_WSS) |
||||
} |
||||
return out |
||||
} |
||||
Loading…
Reference in new issue