+
{feedFullSearchEvents !== null ? (
-
- ) : null}
+ ) : null
+
+ const feedClientFilterChrome = feedClientFilterPanelPortalMode ? (
+
+ {feedClientFilterToggleButton}
+ {feedClientFilterPanel}
+
+ ) : (
+ <>
+
{feedClientFilterToggleButton}
+ {feedClientFilterPanel}
>
)
@@ -4593,10 +4610,7 @@ const NoteList = forwardRef(
const feedClientFilterBar =
useFeedFilterTabRowPortal && feedClientFilterTabRowHost
- ? createPortal(
-
{feedClientFilterChrome}
,
- feedClientFilterTabRowHost
- )
+ ? createPortal(feedClientFilterChrome, feedClientFilterTabRowHost)
: useFeedFilterTabRowPortal && !feedClientFilterTabRowHost
? null
: feedClientFilterBarEmbedded
diff --git a/src/components/NoteStats/ZapButton.tsx b/src/components/NoteStats/ZapButton.tsx
index c1db668a..be54ee83 100644
--- a/src/components/NoteStats/ZapButton.tsx
+++ b/src/components/NoteStats/ZapButton.tsx
@@ -228,7 +228,6 @@ export function ZapButtonWithStats({ event, hideCount = false, noteStats }: ZapB
recipientPubkey: event.pubkey,
referencedEvent: event,
recipientPayment: recipientPaymentForZap,
- onPostPaymentRequest: handlePostPaymentRequest,
onZapDialogClose: () => setOpenPaymentDialog(false)
})
diff --git a/src/components/ZapDialog/index.tsx b/src/components/ZapDialog/index.tsx
index 9d076a67..0f488c92 100644
--- a/src/components/ZapDialog/index.tsx
+++ b/src/components/ZapDialog/index.tsx
@@ -99,7 +99,6 @@ export default function ZapDialog({
recipientPubkey: pubkey,
referencedEvent: event,
recipientPayment,
- onPostPaymentRequest: openPostPaymentPrompt,
onZapDialogClose: () => setOpen(false)
})
diff --git a/src/hooks/useNip57QuickZap.ts b/src/hooks/useNip57QuickZap.ts
index 52b14d79..ba7f040e 100644
--- a/src/hooks/useNip57QuickZap.ts
+++ b/src/hooks/useNip57QuickZap.ts
@@ -1,6 +1,4 @@
import { buildOrderedZapLightningAddresses } from '@/lib/merge-payment-methods'
-import { mergePostPaymentContext, type PostPaymentContext } from '@/lib/post-payment-context'
-import { buildPaytoUri } from '@/lib/payto'
import { formatNpub, pubkeyToNpub } from '@/lib/pubkey'
import { useNostr } from '@/providers/NostrProvider'
import { useZap } from '@/providers/ZapProvider'
@@ -17,7 +15,6 @@ export function useNip57QuickZap(opts: {
recipientPubkey: string
referencedEvent?: NostrEvent
recipientPayment: RecipientPaymentData
- onPostPaymentRequest?: (context: PostPaymentContext) => void
onZapDialogClose?: () => void
}) {
const { t } = useTranslation()
@@ -58,29 +55,13 @@ export function useNip57QuickZap(opts: {
if (!pubkey) return
try {
setZapping(true)
- const paymentDetails = {
- amountMsat: defaultZapSats * 1000,
- paytoUri: buildPaytoUri('lightning', lightningAddressOptions[0] ?? ''),
- messageDraft: defaultZapComment.trim() || undefined
- }
const zapResult = await lightning.zap(
pubkey,
opts.referencedEvent ?? opts.recipientPubkey,
defaultZapSats,
defaultZapComment,
opts.onZapDialogClose,
- (result) => {
- if (!result) return
- opts.onPostPaymentRequest?.(
- mergePostPaymentContext(
- {
- recipientPubkey: opts.recipientPubkey,
- referencedEvent: opts.referencedEvent
- },
- paymentDetails
- )
- )
- },
+ undefined,
{
address: lightningAddressOptions[0],
candidates: lightningAddressOptions
diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts
index d8eeb2f3..ee090b2e 100644
--- a/src/i18n/locales/de.ts
+++ b/src/i18n/locales/de.ts
@@ -989,6 +989,7 @@ export default {
Continue: 'Weiter',
'Successfully updated mute list': 'Stummschalteliste erfolgreich aktualisiert',
'No pubkeys found from {url}': 'Keine Pubkeys von {{url}} gefunden',
+ 'No pubkeys found on NIP-05 domain': 'Keine Pubkeys in dieser NIP-05-Domain gefunden',
'Translating...': 'Übersetze...',
Translate: 'Übersetzen',
'Show original': 'Original anzeigen',
diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts
index 9262cac0..dbb21c94 100644
--- a/src/i18n/locales/en.ts
+++ b/src/i18n/locales/en.ts
@@ -979,6 +979,7 @@ export default {
Continue: 'Continue',
'Successfully updated mute list': 'Successfully updated mute list',
'No pubkeys found from {url}': 'No pubkeys found from {{url}}',
+ 'No pubkeys found on NIP-05 domain': 'No pubkeys found on this NIP-05 domain',
'Translating...': 'Translating...',
Translate: 'Translate',
'Show original': 'Show original',
diff --git a/src/layouts/PrimaryPageLayout/index.tsx b/src/layouts/PrimaryPageLayout/index.tsx
index 42b26368..aceee56d 100644
--- a/src/layouts/PrimaryPageLayout/index.tsx
+++ b/src/layouts/PrimaryPageLayout/index.tsx
@@ -11,6 +11,10 @@ import {
isRadixDialogOpen,
shouldIgnoreKeyboardShortcutEvent
} from '@/lib/keyboard-shortcuts'
+import {
+ peekMobilePrimaryFeedScroll,
+ saveMobilePrimaryFeedScroll
+} from '@/lib/mobile-primary-feed-scroll'
import { cn } from '@/lib/utils'
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'
@@ -42,7 +46,6 @@ const PrimaryPageLayout = forwardRef(
) => {
const scrollAreaRef = useRef
(null)
const smallScreenScrollAreaRef = useRef(null)
- const smallScreenLastScrollTopRef = useRef(0)
const { isSmallScreen } = useScreenSize()
const { current, display, frozen } = usePrimaryPage()
const savedScrollTopRef = useRef(0)
@@ -64,27 +67,25 @@ const PrimaryPageLayout = forwardRef(
)
useEffect(() => {
- if (!isSmallScreen) return
-
- const isVisible = () => {
- return smallScreenScrollAreaRef.current?.checkVisibility
- ? smallScreenScrollAreaRef.current?.checkVisibility()
- : false
- }
+ if (!isSmallScreen || current !== pageName) return
- if (isVisible()) {
- window.scrollTo({ top: smallScreenLastScrollTopRef.current, behavior: 'instant' })
- }
const handleScroll = () => {
- if (isVisible()) {
- smallScreenLastScrollTopRef.current = window.scrollY
- }
+ saveMobilePrimaryFeedScroll(pageName, window.scrollY)
}
- window.addEventListener('scroll', handleScroll)
+ window.addEventListener('scroll', handleScroll, { passive: true })
return () => {
+ handleScroll()
window.removeEventListener('scroll', handleScroll)
}
- }, [current, isSmallScreen, display])
+ }, [current, isSmallScreen, pageName])
+
+ useEffect(() => {
+ if (!isSmallScreen || current !== pageName || !display) return
+ const top = peekMobilePrimaryFeedScroll(pageName)
+ requestAnimationFrame(() => {
+ window.scrollTo({ top, behavior: 'instant' })
+ })
+ }, [current, display, isSmallScreen, pageName])
useEffect(() => {
if (isSmallScreen) return
diff --git a/src/lib/mobile-primary-feed-scroll.ts b/src/lib/mobile-primary-feed-scroll.ts
new file mode 100644
index 00000000..8583d295
--- /dev/null
+++ b/src/lib/mobile-primary-feed-scroll.ts
@@ -0,0 +1,17 @@
+import type { TPrimaryPageName } from '@/PageManager'
+
+/** Persist primary feed window scroll across mobile secondary unmount (PageManager hides the feed while a panel is open). */
+const scrollByPage = new Map()
+
+export function saveMobilePrimaryFeedScroll(page: TPrimaryPageName, top: number): void {
+ if (!Number.isFinite(top) || top < 0) return
+ scrollByPage.set(page, top)
+}
+
+export function peekMobilePrimaryFeedScroll(page: TPrimaryPageName): number {
+ return scrollByPage.get(page) ?? 0
+}
+
+export function captureMobilePrimaryFeedScrollFromWindow(page: TPrimaryPageName): void {
+ saveMobilePrimaryFeedScroll(page, window.scrollY)
+}
diff --git a/src/lib/nip05-well-known.test.ts b/src/lib/nip05-well-known.test.ts
new file mode 100644
index 00000000..46098f28
--- /dev/null
+++ b/src/lib/nip05-well-known.test.ts
@@ -0,0 +1,71 @@
+import { describe, expect, it } from 'vitest'
+import { nip19 } from 'nostr-tools'
+import { parseNip05NamePubkeyEntry, parseNip05NamePubkeysFromWellKnownJson } from '@/lib/nip05'
+
+const THEFOREST_WELL_KNOWN = {
+ names: {
+ '137': '6da819f91d69cbe591c08b31f555c6d0ab9905197eb515856e339049c018c1af',
+ '430': '6da819f91d69cbe591c08b31f555c6d0ab9905197eb515856e339049c018c1af',
+ cloudfodder: '7cc328a08ddb2afdf9f9be77beff4c83489ff979721827d628a542f32a247c0e',
+ laeserin: 'dd664d5e4016433a8cd69f005ae1480804351789b59de5af06276de65633d319',
+ testuser: 'd04bc6808885b8db9c344675c89b442e5f1c30430548bfb263731e1b662d4846',
+ testerin: '573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc',
+ metoo: '59c67d18b2e470fc5a6dd27c8f21ed538d9047808304ebd3305bc104dfc83627',
+ crackerjack: '58d953d05e751803dbb7b8b58f130b5553c13286e04951eac901c50e08ee7313',
+ nostrbots: 'dcc95cc9b0b85802e634f407ed91990471bd69d4af07e719eea8ddfc93f20153',
+ poe: 'e034d654802d7cfaa2d41a952801054114e09ad6a352b28288e23075ca919814',
+ YODL: 'd28413712171c33e117d4bd0930ac05b2c51b30eb3021ef8d4f1233f02c90a2b',
+ superuserdo: '5b0867ea4a23b3b04fe17d0ed52d4529661514b4d84d4a1d86f98eb7c175aab1',
+ daniel: 'ee6ea13ab9fe5c4a68eaf9b1a34fe014a66b40117c50ee2a614f4cda959b6e74',
+ orange: 'de599d3d84a30f8dcb1dd86658655b1ee0318880dc9e53cdc6c367c0b9498700',
+ ThatWhichisNotSeen: 'cf8f07ebffbdce4976ea8ab830cfd6036ffb6203e67ba8eb7a9a448a742a6eaa'
+ },
+ relays: {
+ '137': [],
+ cloudfodder: ['wss://nostr21.com'],
+ laeserin: [],
+ metoo: ['wss://nostr21.com']
+ }
+} as const
+
+describe('parseNip05NamePubkeysFromWellKnownJson', () => {
+ it('parses theforest.nostr1.com well-known names', () => {
+ const rows = parseNip05NamePubkeysFromWellKnownJson(THEFOREST_WELL_KNOWN)
+ expect(rows).toHaveLength(15)
+ expect(rows.find((r) => r.name === 'laeserin')?.pubkey).toBe(
+ 'dd664d5e4016433a8cd69f005ae1480804351789b59de5af06276de65633d319'
+ )
+ expect(rows.find((r) => r.name === '137')?.pubkey).toBe(
+ '6da819f91d69cbe591c08b31f555c6d0ab9905197eb515856e339049c018c1af'
+ )
+ expect(rows.find((r) => r.name === 'YODL')?.pubkey).toBe(
+ 'd28413712171c33e117d4bd0930ac05b2c51b30eb3021ef8d4f1233f02c90a2b'
+ )
+ })
+
+ it('rejects proxy error stubs without names', () => {
+ expect(parseNip05NamePubkeysFromWellKnownJson({ ok: false, error: 'og_proxy_unreachable' })).toEqual(
+ []
+ )
+ })
+
+ it('parses npub-keyed names with username labels', () => {
+ const laeserinHex = 'dd664d5e4016433a8cd69f005ae1480804351789b59de5af06276de65633d319'
+ const npub = nip19.npubEncode(laeserinHex)
+ const rows = parseNip05NamePubkeysFromWellKnownJson({
+ names: {
+ [npub]: 'laeserin'
+ }
+ })
+ expect(rows).toEqual([{ name: 'laeserin', pubkey: laeserinHex }])
+ })
+
+ it('matches npub keys when resolving entries', () => {
+ const laeserinHex = 'dd664d5e4016433a8cd69f005ae1480804351789b59de5af06276de65633d319'
+ const npub = nip19.npubEncode(laeserinHex)
+ expect(parseNip05NamePubkeyEntry(npub, 'laeserin')).toEqual({
+ name: 'laeserin',
+ pubkey: laeserinHex
+ })
+ })
+})
diff --git a/src/lib/nip05.ts b/src/lib/nip05.ts
index 4a7913e8..8398aba1 100644
--- a/src/lib/nip05.ts
+++ b/src/lib/nip05.ts
@@ -18,9 +18,12 @@ type TVerifyNip05Result = {
}
/** Bumps when verification rules change so LRU does not serve stale false negatives. */
-const VERIFY_CACHE_SCHEMA = 4
+const VERIFY_CACHE_SCHEMA = 5
-type WellKnownCacheEntry = { json: Record | null }
+/** Bumps when well-known fetch/parse rules change so stale empty cache entries are not reused. */
+const WELL_KNOWN_CACHE_SCHEMA = 3
+
+type WellKnownCacheEntry = { json: Record | null; schema: number }
/** Per-domain `nostr.json` (or negative `null`) so feeds do not re-fetch every NIP-05 on the same host. */
const wellKnownJsonByDomain = new LRUCache({ max: 512 })
@@ -73,6 +76,86 @@ function pubkeyHexFromWellKnownNamesValue(v: unknown): string | null {
return null
}
+function npubFromHexPubkey(hex: string): string | null {
+ try {
+ return nip19.npubEncode(normalizeHexPubkey(hex))
+ } catch {
+ return null
+ }
+}
+
+export type TNip05NamePubkeyEntry = { name: string; pubkey: string }
+
+/**
+ * Parse one `names` row. Supports:
+ * - standard: `username` → hex or npub
+ * - inverted: hex or npub key → `username` label
+ */
+export function parseNip05NamePubkeyEntry(key: string, value: unknown): TNip05NamePubkeyEntry | null {
+ const hexFromValue = pubkeyHexFromWellKnownNamesValue(value)
+ if (hexFromValue) {
+ return { name: key, pubkey: hexFromValue }
+ }
+ const hexFromKey = pubkeyHexFromWellKnownNamesValue(key)
+ if (!hexFromKey) return null
+ const label = asNip05LookupString(value).trim()
+ if (!label || pubkeyHexFromWellKnownNamesValue(label)) return null
+ return { name: label, pubkey: hexFromKey }
+}
+
+function resolveNamesEntry(
+ names: Record,
+ nip05Name: string,
+ userPubkeyHex?: string
+): TNip05NamePubkeyEntry | null {
+ const want = nip05Name.trim()
+ if (!want) return null
+
+ const direct = getNamesEntryRaw(names, want)
+ if (direct != null) {
+ const hex = pubkeyHexFromWellKnownNamesValue(direct)
+ if (hex) return { name: want, pubkey: hex }
+ }
+
+ for (const [key, v] of Object.entries(names)) {
+ const entry = parseNip05NamePubkeyEntry(key, v)
+ if (entry && entry.name.toLowerCase() === want.toLowerCase()) return entry
+ }
+
+ if (!userPubkeyHex || !isValidPubkey(userPubkeyHex)) return null
+ const user = normalizeHexPubkey(userPubkeyHex)
+ const userNpub = npubFromHexPubkey(user)
+ if (userNpub) {
+ for (const [key, v] of Object.entries(names)) {
+ if (key.toLowerCase() !== userNpub.toLowerCase()) continue
+ const entry = parseNip05NamePubkeyEntry(key, v)
+ if (entry && hexPubkeysEqual(entry.pubkey, user)) return entry
+ const hex = pubkeyHexFromWellKnownNamesValue(v)
+ if (hex && hexPubkeysEqual(hex, user)) return { name: want, pubkey: hex }
+ }
+ }
+
+ for (const [key, v] of Object.entries(names)) {
+ const entry = parseNip05NamePubkeyEntry(key, v)
+ if (entry && hexPubkeysEqual(entry.pubkey, user)) return entry
+ }
+
+ return null
+}
+
+function nip05LocalNameForPubkeyFromNames(
+ names: Record | undefined,
+ userPubkeyHex: string
+): string | undefined {
+ if (!names || typeof names !== 'object') return undefined
+ const user = normalizeHexPubkey(userPubkeyHex)
+ for (const [key, v] of Object.entries(names)) {
+ const entry = parseNip05NamePubkeyEntry(key, v)
+ if (entry && hexPubkeysEqual(entry.pubkey, user)) return entry.name
+ }
+ return undefined
+}
+
function getNamesEntryRaw(names: Record, nip05Name: string): string | undefined {
if (!nip05Name || typeof names !== 'object' || names == null) return undefined
const asTrimmedString = (x: unknown): string | undefined =>
@@ -90,7 +173,8 @@ function pickRelayListForPubkey(
relays: Record | undefined,
userPubkeyHex: string,
listedRaw: string,
- nip05LocalName: string
+ nip05LocalName: string,
+ names?: Record
): unknown {
if (!relays || typeof relays !== 'object') return undefined
const user = normalizeHexPubkey(userPubkeyHex)
@@ -98,14 +182,13 @@ function pickRelayListForPubkey(
keysToTry.add(user)
keysToTry.add(userPubkeyHex.trim())
keysToTry.add(listedRaw.trim())
+ const userNpub = npubFromHexPubkey(user)
+ if (userNpub) keysToTry.add(userNpub)
const listedHex = pubkeyHexFromWellKnownNamesValue(listedRaw)
if (listedHex) {
keysToTry.add(listedHex)
- try {
- keysToTry.add(nip19.npubEncode(listedHex))
- } catch {
- /* ignore */
- }
+ const listedNpub = npubFromHexPubkey(listedHex)
+ if (listedNpub) keysToTry.add(listedNpub)
}
for (const k of keysToTry) {
if (!k) continue
@@ -118,8 +201,8 @@ function pickRelayListForPubkey(
if (Array.isArray(v)) return v
}
}
- /** Some hosts key `relays` by NIP-05 local part (non-standard); NIP recommends pubkey keys. */
- const local = nip05LocalName.trim()
+ const local =
+ nip05LocalName.trim() || nip05LocalNameForPubkeyFromNames(names, userPubkeyHex) || ''
if (local) {
const want = local.toLowerCase()
for (const k of Object.keys(relays)) {
@@ -157,14 +240,17 @@ async function _verifyNip05(nip05: string, pubkey: string): Promise | undefined
if (!names || typeof names !== 'object') return result
- const listedRaw = getNamesEntryRaw(names, nip05Name)
- if (listedRaw == null) return result
-
- const listedHex = pubkeyHexFromWellKnownNamesValue(listedRaw)
- if (!listedHex || !hexPubkeysEqual(listedHex, userHex)) return result
+ const resolved = resolveNamesEntry(names, nip05Name, userHex)
+ if (!resolved || !hexPubkeysEqual(resolved.pubkey, userHex)) return result
const relays = json.relays as Record | undefined
- const relayList = pickRelayListForPubkey(relays, userHex, listedRaw, nip05Name)
+ const relayList = pickRelayListForPubkey(
+ relays,
+ userHex,
+ resolved.pubkey,
+ resolved.name,
+ names
+ )
return { ...result, isVerified: true, relays: Array.isArray(relayList) ? (relayList as string[]) : undefined }
}
@@ -192,39 +278,85 @@ export function getWellKnownNip05Url(domain: string, name?: string): string {
return url.toString()
}
-/**
- * Fetch `/.well-known/nostr.json` in the browser without tripping third-party CORS:
- * when `VITE_PROXY_SERVER` is set (production), use same-origin `/sites/?url=…` like OG preview.
- */
-async function fetchWellKnownNostrJsonOnce(
- domain: string,
- nameInQuery: string | undefined
+function isWellKnownNostrJsonDocument(data: unknown): data is Record {
+ if (!data || typeof data !== 'object' || Array.isArray(data)) return false
+ const names = (data as Record).names
+ return typeof names === 'object' && names != null && !Array.isArray(names)
+}
+
+async function readWellKnownNostrJsonResponse(res: Response): Promise | null> {
+ try {
+ const data: unknown = await res.json()
+ return isWellKnownNostrJsonDocument(data) ? data : null
+ } catch {
+ return null
+ }
+}
+
+async function fetchWellKnownNostrJsonDirect(targetUrl: string): Promise | null> {
+ try {
+ const res = await fetchWithTimeout(targetUrl, {
+ credentials: 'omit',
+ headers: {
+ Accept: 'application/nostr+json, application/json;q=0.9, text/plain;q=0.8, */*;q=0.1'
+ },
+ timeoutMs: 15_000,
+ mode: 'cors'
+ })
+ if (!res.ok) return null
+ return readWellKnownNostrJsonResponse(res)
+ } catch {
+ return null
+ }
+}
+
+async function fetchWellKnownNostrJsonViaProxy(
+ targetUrl: string,
+ proxyServer: string
): Promise | null> {
- const targetUrl = getWellKnownNip05Url(domain, nameInQuery)
- const proxyServer = import.meta.env.VITE_PROXY_SERVER?.trim()
- const useProxy = Boolean(proxyServer && !isSitesProxyUnavailableThisSession())
- const fetchUrl = useProxy ? buildViteProxySitesFetchUrl(targetUrl, proxyServer!) : targetUrl
+ const fetchUrl = buildViteProxySitesFetchUrl(targetUrl, proxyServer)
try {
const res = await fetchWithTimeout(fetchUrl, {
credentials: 'omit',
headers: {
Accept: 'application/nostr+json, application/json;q=0.9, text/plain;q=0.8, */*;q=0.1'
},
- timeoutMs: 15_000
+ timeoutMs: 15_000,
+ mode: 'cors'
})
- /** NIP-05: well-known MUST NOT redirect; following redirects can land on unrelated JSON. */
- if (res.redirected || !res.ok) {
- if (useProxy && !res.redirected) markSitesProxyUnavailableFromHttpStatus(res.status)
+ if (!res.ok) {
+ if (!res.redirected) markSitesProxyUnavailableFromHttpStatus(res.status)
return null
}
- if (useProxy) clearSitesProxyUnavailableThisSession()
- const data: unknown = await res.json()
- return data && typeof data === 'object' && !Array.isArray(data) ? (data as Record) : null
+ const json = await readWellKnownNostrJsonResponse(res)
+ if (json) clearSitesProxyUnavailableThisSession()
+ return json
} catch {
return null
}
}
+/**
+ * Fetch `/.well-known/nostr.json` in the browser without tripping third-party CORS:
+ * when `VITE_PROXY_SERVER` is set (production), use same-origin `/sites/?url=…` like OG preview.
+ */
+async function fetchWellKnownNostrJsonOnce(
+ domain: string,
+ nameInQuery: string | undefined
+): Promise | null> {
+ const targetUrl = getWellKnownNip05Url(domain, nameInQuery)
+ const proxyServer = import.meta.env.VITE_PROXY_SERVER?.trim()
+ const useProxy = Boolean(proxyServer && !isSitesProxyUnavailableThisSession())
+
+ if (useProxy) {
+ const viaProxy = await fetchWellKnownNostrJsonViaProxy(targetUrl, proxyServer!)
+ if (viaProxy) return viaProxy
+ return fetchWellKnownNostrJsonDirect(targetUrl)
+ }
+
+ return fetchWellKnownNostrJsonDirect(targetUrl)
+}
+
/** Uncached network: optional `?name=` then full document. */
async function fetchWellKnownNostrJsonNetwork(
domain: string,
@@ -234,13 +366,13 @@ async function fetchWellKnownNostrJsonNetwork(
const withQuery =
trimmedName.length > 0 ? await fetchWellKnownNostrJsonOnce(domain, trimmedName) : null
if (withQuery && typeof withQuery.names === 'object' && withQuery.names != null) {
- if (getNamesEntryRaw(withQuery.names as Record, trimmedName) != null) {
+ if (resolveNamesEntry(withQuery.names as Record, trimmedName) != null) {
return withQuery
}
}
const full = await fetchWellKnownNostrJsonOnce(domain, undefined)
if (full && trimmedName && typeof full.names === 'object' && full.names != null) {
- if (getNamesEntryRaw(full.names as Record, trimmedName) != null) {
+ if (resolveNamesEntry(full.names as Record, trimmedName) != null) {
return full
}
}
@@ -254,13 +386,19 @@ async function getOrFetchWellKnownJsonForDomain(
const key = normalizeNip05Domain(domain)
if (!key) return null
if (wellKnownJsonByDomain.has(key)) {
- return wellKnownJsonByDomain.get(key)!.json
+ const cached = wellKnownJsonByDomain.get(key)!
+ if (cached.schema === WELL_KNOWN_CACHE_SCHEMA && cached.json && isWellKnownNostrJsonDocument(cached.json)) {
+ return cached.json
+ }
+ wellKnownJsonByDomain.delete(key)
}
let inflight = wellKnownDomainInFlight.get(key)
if (!inflight) {
inflight = fetchWellKnownNostrJsonNetwork(key, nameHint).then((json) => {
- wellKnownJsonByDomain.set(key, { json })
wellKnownDomainInFlight.delete(key)
+ if (isWellKnownNostrJsonDocument(json)) {
+ wellKnownJsonByDomain.set(key, { json, schema: WELL_KNOWN_CACHE_SCHEMA })
+ }
return json
})
wellKnownDomainInFlight.set(key, inflight)
@@ -276,19 +414,36 @@ async function fetchWellKnownNostrJson(domain: string, name?: string): Promise {
+ const entries = await fetchNip05NamePubkeysFromDomain(domain)
+ return entries.map((e) => e.pubkey)
+}
+
+export function parseNip05NamePubkeysFromWellKnownJson(
+ json: Record
+): Array<{ name: string; pubkey: string }> {
+ const names = json.names as Record | undefined
+ if (!names || typeof names !== 'object') return []
+ const out: Array<{ name: string; pubkey: string }> = []
+ const seen = new Set()
+ for (const [key, v] of Object.entries(names)) {
+ const entry = parseNip05NamePubkeyEntry(key, v)
+ if (!entry || !isValidPubkey(entry.pubkey)) continue
+ const dedupe = `${entry.name}:${entry.pubkey}`
+ if (seen.has(dedupe)) continue
+ seen.add(dedupe)
+ out.push(entry)
+ }
+ out.sort((a, b) => a.name.localeCompare(b.name))
+ return out
+}
+
+export async function fetchNip05NamePubkeysFromDomain(
+ domain: string
+): Promise> {
try {
const json = await fetchWellKnownNostrJson(domain)
if (!json) return []
- const pubkeySet = new Set()
- const out: string[] = []
- for (const v of Object.values((json.names as Record) || {})) {
- const hex = pubkeyHexFromWellKnownNamesValue(v)
- if (!hex || !isValidPubkey(hex)) continue
- if (pubkeySet.has(hex)) continue
- pubkeySet.add(hex)
- out.push(hex)
- }
- return out
+ return parseNip05NamePubkeysFromWellKnownJson(json)
} catch (error) {
logger.error('Error fetching pubkeys from domain', { error, domain })
return []
diff --git a/src/pages/secondary/NoteListPage/index.tsx b/src/pages/secondary/NoteListPage/index.tsx
index b4259855..6800377b 100644
--- a/src/pages/secondary/NoteListPage/index.tsx
+++ b/src/pages/secondary/NoteListPage/index.tsx
@@ -1,4 +1,5 @@
import { Favicon } from '@/components/Favicon'
+import Nip05DomainEmptyState from '@/components/Nip05DomainPanel/Nip05DomainEmptyState'
import type { TNoteListRef } from '@/components/NoteList'
import NormalFeed from '@/components/NormalFeed'
import { RefreshButton } from '@/components/RefreshButton'
@@ -22,7 +23,7 @@ import {
buildAlexandriaEventsUrlForHashtagParam
} from '@/lib/alexandria-events-search-url'
import { compareEventsForDTagQuery, eventMatchesDTagLooseQuery } from '@/lib/dtag-search'
-import { fetchPubkeysFromDomain, getWellKnownNip05Url } from '@/lib/nip05'
+import { fetchPubkeysFromDomain } from '@/lib/nip05'
import { usePrimaryNoteView } from '@/contexts/primary-note-view-context'
import { useSecondaryPage } from '@/PageManager'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
@@ -358,13 +359,7 @@ const NoteListPage = forwardRef(({ index, hid
let content: React.ReactNode = null
if (data?.type === 'domain' && subRequests.length === 0) {
- content = (
-
-
- {t('No pubkeys found from {url}', { url: getWellKnownNip05Url(data.domain) })}
-
-
- )
+ content =
} else if (data) {
content =
data.type === 'dtag' && data.dtag ? (
diff --git a/src/pages/secondary/ProfileListPage/index.tsx b/src/pages/secondary/ProfileListPage/index.tsx
index 7e8b5b90..c72349b7 100644
--- a/src/pages/secondary/ProfileListPage/index.tsx
+++ b/src/pages/secondary/ProfileListPage/index.tsx
@@ -1,10 +1,9 @@
import { Favicon } from '@/components/Favicon'
-import ProfileList from '@/components/ProfileList'
+import ProfileListByNip05Domain from '@/components/Nip05DomainPanel/ProfileListByNip05Domain'
import { ProfileListBySearch } from '@/components/ProfileListBySearch'
import { RefreshButton } from '@/components/RefreshButton'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { buildAlexandriaEventsSearchUrlForTSearchParams } from '@/lib/alexandria-events-search-url'
-import { fetchPubkeysFromDomain } from '@/lib/nip05'
import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -54,7 +53,7 @@ const ProfileListPage = forwardRef(({ index }: { index?: number }, ref) => {
)
} else if (data?.type === 'domain') {
- content =
+ content =
}
return (
@@ -73,17 +72,3 @@ const ProfileListPage = forwardRef(({ index }: { index?: number }, ref) => {
})
ProfileListPage.displayName = 'ProfileListPage'
export default ProfileListPage
-
-function ProfileListByDomain({ domain }: { domain: string }) {
- const [pubkeys, setPubkeys] = useState([])
-
- useEffect(() => {
- const init = async () => {
- const _pubkeys = await fetchPubkeysFromDomain(domain)
- setPubkeys(_pubkeys)
- }
- init()
- }, [domain])
-
- return
-}