Browse Source

bug-fixes

imwald
Silberengel 1 month ago
parent
commit
d65dcf671c
  1. 25
      src/components/Nip05List/index.tsx
  2. 5
      src/i18n/locales/cs.ts
  3. 7
      src/i18n/locales/de.ts
  4. 5
      src/i18n/locales/en.ts
  5. 5
      src/i18n/locales/es.ts
  6. 5
      src/i18n/locales/fr.ts
  7. 5
      src/i18n/locales/nl.ts
  8. 5
      src/i18n/locales/pl.ts
  9. 5
      src/i18n/locales/ru.ts
  10. 5
      src/i18n/locales/tr.ts
  11. 5
      src/i18n/locales/zh.ts
  12. 205
      src/lib/nip05.ts
  13. 62
      src/lib/profile-relay-search-filters.ts
  14. 195
      src/providers/NostrProvider/index.tsx
  15. 3
      src/providers/NostrProvider/nip-07.signer.ts
  16. 4
      src/services/client-query.service.ts
  17. 77
      src/services/client.service.ts

25
src/components/Nip05List/index.tsx

@ -1,5 +1,5 @@
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { verifyNip05 } from '@/lib/nip05' import { splitNip05Identifier, verifyNip05 } from '@/lib/nip05'
import { toNoteList } from '@/lib/link' import { toNoteList } from '@/lib/link'
import { SecondaryPageLink } from '@/PageManager' import { SecondaryPageLink } from '@/PageManager'
import { BadgeAlert, BadgeCheck } from 'lucide-react' import { BadgeAlert, BadgeCheck } from 'lucide-react'
@ -24,13 +24,13 @@ export default function Nip05List({ nip05List, pubkey }: { nip05List: string[];
const newVerifications = new Map<string, Nip05Verification>() const newVerifications = new Map<string, Nip05Verification>()
// Initialize all as fetching // Initialize all as fetching
nip05List.forEach(nip05 => { nip05List.forEach((nip05) => {
const [nip05Name, nip05Domain] = nip05.split('@') const parts = splitNip05Identifier(nip05.trim())
newVerifications.set(nip05, { newVerifications.set(nip05, {
nip05, nip05,
isVerified: false, isVerified: false,
nip05Name: nip05Name || '', nip05Name: parts?.name ?? '',
nip05Domain: nip05Domain || '', nip05Domain: parts?.domain ?? '',
isFetching: true isFetching: true
}) })
}) })
@ -43,11 +43,12 @@ export default function Nip05List({ nip05List, pubkey }: { nip05List: string[];
const result = await verifyNip05(nip05, pubkey) const result = await verifyNip05(nip05, pubkey)
setVerifications(prev => { setVerifications(prev => {
const updated = new Map(prev) const updated = new Map(prev)
const fb = splitNip05Identifier(nip05.trim())
updated.set(nip05, { updated.set(nip05, {
nip05, nip05,
isVerified: result.isVerified, isVerified: result.isVerified,
nip05Name: result.nip05Name || nip05.split('@')[0] || '', nip05Name: result.nip05Name || fb?.name || '',
nip05Domain: result.nip05Domain || nip05.split('@')[1] || '', nip05Domain: result.nip05Domain || fb?.domain || '',
isFetching: false isFetching: false
}) })
return updated return updated
@ -55,11 +56,12 @@ export default function Nip05List({ nip05List, pubkey }: { nip05List: string[];
} catch (error) { } catch (error) {
setVerifications(prev => { setVerifications(prev => {
const updated = new Map(prev) const updated = new Map(prev)
const fb = splitNip05Identifier(nip05.trim())
const existing = updated.get(nip05) || { const existing = updated.get(nip05) || {
nip05, nip05,
isVerified: false, isVerified: false,
nip05Name: nip05.split('@')[0] || '', nip05Name: fb?.name || '',
nip05Domain: nip05.split('@')[1] || '', nip05Domain: fb?.domain || '',
isFetching: false isFetching: false
} }
updated.set(nip05, { ...existing, isFetching: false }) updated.set(nip05, { ...existing, isFetching: false })
@ -81,8 +83,9 @@ export default function Nip05List({ nip05List, pubkey }: { nip05List: string[];
const verification = verifications.get(nip05) const verification = verifications.get(nip05)
const isFetching = verification?.isFetching ?? true const isFetching = verification?.isFetching ?? true
const isVerified = verification?.isVerified ?? false const isVerified = verification?.isVerified ?? false
const nip05Name = verification?.nip05Name || nip05.split('@')[0] || '' const fb = splitNip05Identifier(nip05.trim())
const nip05Domain = verification?.nip05Domain || nip05.split('@')[1] || '' const nip05Name = verification?.nip05Name || fb?.name || ''
const nip05Domain = verification?.nip05Domain || fb?.domain || ''
if (isFetching) { if (isFetching) {
return ( return (

5
src/i18n/locales/cs.ts

@ -1539,8 +1539,11 @@ export default {
"Location (optional)": "Location (optional)", "Location (optional)": "Location (optional)",
"Log in to run this spell (it uses $me or $contacts).": "Log in to run this spell (it uses $me or $contacts).", "Log in to run this spell (it uses $me or $contacts).": "Log in to run this spell (it uses $me or $contacts).",
"Login failed": "Login failed", "Login failed": "Login failed",
"nip07.extensionKeyMismatch": "Your browser extension is using a different key on this tab. Switch to the matching key in the extension, or reload the page to apply your extension's current selection.", "nip07.extensionKeyMismatch": "Your browser extension is using a different key on this tab. Switch to the matching key in the extension, reload the page to apply your extension's current selection, or use the other action on this message to log in with the key currently selected in your extension.",
"nip07.reloadPage": "Reload page", "nip07.reloadPage": "Reload page",
"nip07.useExtensionIdentity": "Use extension identity",
"nip07.switchedToExtensionIdentity": "Switched to your extension's current identity.",
"nip07.adoptExtensionFailed": "Could not switch to extension identity",
"Login to configure RSS feeds": "Login to configure RSS feeds", "Login to configure RSS feeds": "Login to configure RSS feeds",
"Long-form Article": "Long-form Article", "Long-form Article": "Long-form Article",
"Mailbox relays saved": "Mailbox relays saved", "Mailbox relays saved": "Mailbox relays saved",

7
src/i18n/locales/de.ts

@ -1539,8 +1539,11 @@ export default {
"Location (optional)": "Location (optional)", "Location (optional)": "Location (optional)",
"Log in to run this spell (it uses $me or $contacts).": "Zum Ausführen anmelden (verwendet $me oder $contacts).", "Log in to run this spell (it uses $me or $contacts).": "Zum Ausführen anmelden (verwendet $me oder $contacts).",
"Login failed": "Login failed", "Login failed": "Login failed",
"nip07.extensionKeyMismatch": "Your browser extension is using a different key on this tab. Switch to the matching key in the extension, or reload the page to apply your extension's current selection.", "nip07.extensionKeyMismatch": "Ihre Browser-Erweiterung verwendet auf diesem Tab einen anderen Schlüssel. Wechseln Sie in der Erweiterung zum passenden Schlüssel, laden Sie die Seite neu, um die aktuelle Erweiterungsauswahl zu übernehmen, oder nutzen Sie die andere Aktion in dieser Meldung, um sich mit dem in der Erweiterung gewählten Schlüssel anzumelden.",
"nip07.reloadPage": "Reload page", "nip07.reloadPage": "Seite neu laden",
"nip07.useExtensionIdentity": "Erweiterungs-Identität verwenden",
"nip07.switchedToExtensionIdentity": "Auf die aktuelle Identität Ihrer Erweiterung umgestellt.",
"nip07.adoptExtensionFailed": "Wechsel zur Erweiterungs-Identität fehlgeschlagen",
"Login to configure RSS feeds": "Login to configure RSS feeds", "Login to configure RSS feeds": "Login to configure RSS feeds",
"Long-form Article": "Long-form Article", "Long-form Article": "Long-form Article",
"Mailbox relays saved": "Mailbox relays saved", "Mailbox relays saved": "Mailbox relays saved",

5
src/i18n/locales/en.ts

@ -1574,8 +1574,11 @@ export default {
"Location (optional)": "Location (optional)", "Location (optional)": "Location (optional)",
"Log in to run this spell (it uses $me or $contacts).": "Log in to run this spell (it uses $me or $contacts).", "Log in to run this spell (it uses $me or $contacts).": "Log in to run this spell (it uses $me or $contacts).",
"Login failed": "Login failed", "Login failed": "Login failed",
"nip07.extensionKeyMismatch": "Your browser extension is using a different key on this tab. Switch to the matching key in the extension, or reload the page to apply your extension's current selection.", "nip07.extensionKeyMismatch": "Your browser extension is using a different key on this tab. Switch to the matching key in the extension, reload the page to apply your extension's current selection, or use the other action on this message to log in with the key currently selected in your extension.",
"nip07.reloadPage": "Reload page", "nip07.reloadPage": "Reload page",
"nip07.useExtensionIdentity": "Use extension identity",
"nip07.switchedToExtensionIdentity": "Switched to your extension's current identity.",
"nip07.adoptExtensionFailed": "Could not switch to extension identity",
"Login to configure RSS feeds": "Login to configure RSS feeds", "Login to configure RSS feeds": "Login to configure RSS feeds",
"Long-form Article": "Long-form Article", "Long-form Article": "Long-form Article",
"Mailbox relays saved": "Mailbox relays saved", "Mailbox relays saved": "Mailbox relays saved",

5
src/i18n/locales/es.ts

@ -1539,8 +1539,11 @@ export default {
"Location (optional)": "Location (optional)", "Location (optional)": "Location (optional)",
"Log in to run this spell (it uses $me or $contacts).": "Log in to run this spell (it uses $me or $contacts).", "Log in to run this spell (it uses $me or $contacts).": "Log in to run this spell (it uses $me or $contacts).",
"Login failed": "Login failed", "Login failed": "Login failed",
"nip07.extensionKeyMismatch": "Your browser extension is using a different key on this tab. Switch to the matching key in the extension, or reload the page to apply your extension's current selection.", "nip07.extensionKeyMismatch": "Your browser extension is using a different key on this tab. Switch to the matching key in the extension, reload the page to apply your extension's current selection, or use the other action on this message to log in with the key currently selected in your extension.",
"nip07.reloadPage": "Reload page", "nip07.reloadPage": "Reload page",
"nip07.useExtensionIdentity": "Use extension identity",
"nip07.switchedToExtensionIdentity": "Switched to your extension's current identity.",
"nip07.adoptExtensionFailed": "Could not switch to extension identity",
"Login to configure RSS feeds": "Login to configure RSS feeds", "Login to configure RSS feeds": "Login to configure RSS feeds",
"Long-form Article": "Long-form Article", "Long-form Article": "Long-form Article",
"Mailbox relays saved": "Mailbox relays saved", "Mailbox relays saved": "Mailbox relays saved",

5
src/i18n/locales/fr.ts

@ -1539,8 +1539,11 @@ export default {
"Location (optional)": "Location (optional)", "Location (optional)": "Location (optional)",
"Log in to run this spell (it uses $me or $contacts).": "Log in to run this spell (it uses $me or $contacts).", "Log in to run this spell (it uses $me or $contacts).": "Log in to run this spell (it uses $me or $contacts).",
"Login failed": "Login failed", "Login failed": "Login failed",
"nip07.extensionKeyMismatch": "Your browser extension is using a different key on this tab. Switch to the matching key in the extension, or reload the page to apply your extension's current selection.", "nip07.extensionKeyMismatch": "Your browser extension is using a different key on this tab. Switch to the matching key in the extension, reload the page to apply your extension's current selection, or use the other action on this message to log in with the key currently selected in your extension.",
"nip07.reloadPage": "Reload page", "nip07.reloadPage": "Reload page",
"nip07.useExtensionIdentity": "Use extension identity",
"nip07.switchedToExtensionIdentity": "Switched to your extension's current identity.",
"nip07.adoptExtensionFailed": "Could not switch to extension identity",
"Login to configure RSS feeds": "Login to configure RSS feeds", "Login to configure RSS feeds": "Login to configure RSS feeds",
"Long-form Article": "Long-form Article", "Long-form Article": "Long-form Article",
"Mailbox relays saved": "Mailbox relays saved", "Mailbox relays saved": "Mailbox relays saved",

5
src/i18n/locales/nl.ts

@ -1539,8 +1539,11 @@ export default {
"Location (optional)": "Location (optional)", "Location (optional)": "Location (optional)",
"Log in to run this spell (it uses $me or $contacts).": "Log in to run this spell (it uses $me or $contacts).", "Log in to run this spell (it uses $me or $contacts).": "Log in to run this spell (it uses $me or $contacts).",
"Login failed": "Login failed", "Login failed": "Login failed",
"nip07.extensionKeyMismatch": "Your browser extension is using a different key on this tab. Switch to the matching key in the extension, or reload the page to apply your extension's current selection.", "nip07.extensionKeyMismatch": "Your browser extension is using a different key on this tab. Switch to the matching key in the extension, reload the page to apply your extension's current selection, or use the other action on this message to log in with the key currently selected in your extension.",
"nip07.reloadPage": "Reload page", "nip07.reloadPage": "Reload page",
"nip07.useExtensionIdentity": "Use extension identity",
"nip07.switchedToExtensionIdentity": "Switched to your extension's current identity.",
"nip07.adoptExtensionFailed": "Could not switch to extension identity",
"Login to configure RSS feeds": "Login to configure RSS feeds", "Login to configure RSS feeds": "Login to configure RSS feeds",
"Long-form Article": "Long-form Article", "Long-form Article": "Long-form Article",
"Mailbox relays saved": "Mailbox relays saved", "Mailbox relays saved": "Mailbox relays saved",

5
src/i18n/locales/pl.ts

@ -1539,8 +1539,11 @@ export default {
"Location (optional)": "Location (optional)", "Location (optional)": "Location (optional)",
"Log in to run this spell (it uses $me or $contacts).": "Log in to run this spell (it uses $me or $contacts).", "Log in to run this spell (it uses $me or $contacts).": "Log in to run this spell (it uses $me or $contacts).",
"Login failed": "Login failed", "Login failed": "Login failed",
"nip07.extensionKeyMismatch": "Your browser extension is using a different key on this tab. Switch to the matching key in the extension, or reload the page to apply your extension's current selection.", "nip07.extensionKeyMismatch": "Your browser extension is using a different key on this tab. Switch to the matching key in the extension, reload the page to apply your extension's current selection, or use the other action on this message to log in with the key currently selected in your extension.",
"nip07.reloadPage": "Reload page", "nip07.reloadPage": "Reload page",
"nip07.useExtensionIdentity": "Use extension identity",
"nip07.switchedToExtensionIdentity": "Switched to your extension's current identity.",
"nip07.adoptExtensionFailed": "Could not switch to extension identity",
"Login to configure RSS feeds": "Login to configure RSS feeds", "Login to configure RSS feeds": "Login to configure RSS feeds",
"Long-form Article": "Long-form Article", "Long-form Article": "Long-form Article",
"Mailbox relays saved": "Mailbox relays saved", "Mailbox relays saved": "Mailbox relays saved",

5
src/i18n/locales/ru.ts

@ -1539,8 +1539,11 @@ export default {
"Location (optional)": "Location (optional)", "Location (optional)": "Location (optional)",
"Log in to run this spell (it uses $me or $contacts).": "Log in to run this spell (it uses $me or $contacts).", "Log in to run this spell (it uses $me or $contacts).": "Log in to run this spell (it uses $me or $contacts).",
"Login failed": "Login failed", "Login failed": "Login failed",
"nip07.extensionKeyMismatch": "Your browser extension is using a different key on this tab. Switch to the matching key in the extension, or reload the page to apply your extension's current selection.", "nip07.extensionKeyMismatch": "Your browser extension is using a different key on this tab. Switch to the matching key in the extension, reload the page to apply your extension's current selection, or use the other action on this message to log in with the key currently selected in your extension.",
"nip07.reloadPage": "Reload page", "nip07.reloadPage": "Reload page",
"nip07.useExtensionIdentity": "Use extension identity",
"nip07.switchedToExtensionIdentity": "Switched to your extension's current identity.",
"nip07.adoptExtensionFailed": "Could not switch to extension identity",
"Login to configure RSS feeds": "Login to configure RSS feeds", "Login to configure RSS feeds": "Login to configure RSS feeds",
"Long-form Article": "Long-form Article", "Long-form Article": "Long-form Article",
"Mailbox relays saved": "Mailbox relays saved", "Mailbox relays saved": "Mailbox relays saved",

5
src/i18n/locales/tr.ts

@ -1539,8 +1539,11 @@ export default {
"Location (optional)": "Location (optional)", "Location (optional)": "Location (optional)",
"Log in to run this spell (it uses $me or $contacts).": "Log in to run this spell (it uses $me or $contacts).", "Log in to run this spell (it uses $me or $contacts).": "Log in to run this spell (it uses $me or $contacts).",
"Login failed": "Login failed", "Login failed": "Login failed",
"nip07.extensionKeyMismatch": "Your browser extension is using a different key on this tab. Switch to the matching key in the extension, or reload the page to apply your extension's current selection.", "nip07.extensionKeyMismatch": "Your browser extension is using a different key on this tab. Switch to the matching key in the extension, reload the page to apply your extension's current selection, or use the other action on this message to log in with the key currently selected in your extension.",
"nip07.reloadPage": "Reload page", "nip07.reloadPage": "Reload page",
"nip07.useExtensionIdentity": "Use extension identity",
"nip07.switchedToExtensionIdentity": "Switched to your extension's current identity.",
"nip07.adoptExtensionFailed": "Could not switch to extension identity",
"Login to configure RSS feeds": "Login to configure RSS feeds", "Login to configure RSS feeds": "Login to configure RSS feeds",
"Long-form Article": "Long-form Article", "Long-form Article": "Long-form Article",
"Mailbox relays saved": "Mailbox relays saved", "Mailbox relays saved": "Mailbox relays saved",

5
src/i18n/locales/zh.ts

@ -1539,8 +1539,11 @@ export default {
"Location (optional)": "Location (optional)", "Location (optional)": "Location (optional)",
"Log in to run this spell (it uses $me or $contacts).": "Log in to run this spell (it uses $me or $contacts).", "Log in to run this spell (it uses $me or $contacts).": "Log in to run this spell (it uses $me or $contacts).",
"Login failed": "Login failed", "Login failed": "Login failed",
"nip07.extensionKeyMismatch": "Your browser extension is using a different key on this tab. Switch to the matching key in the extension, or reload the page to apply your extension's current selection.", "nip07.extensionKeyMismatch": "Your browser extension is using a different key on this tab. Switch to the matching key in the extension, reload the page to apply your extension's current selection, or use the other action on this message to log in with the key currently selected in your extension.",
"nip07.reloadPage": "Reload page", "nip07.reloadPage": "Reload page",
"nip07.useExtensionIdentity": "Use extension identity",
"nip07.switchedToExtensionIdentity": "Switched to your extension's current identity.",
"nip07.adoptExtensionFailed": "Could not switch to extension identity",
"Login to configure RSS feeds": "Login to configure RSS feeds", "Login to configure RSS feeds": "Login to configure RSS feeds",
"Long-form Article": "Long-form Article", "Long-form Article": "Long-form Article",
"Mailbox relays saved": "Mailbox relays saved", "Mailbox relays saved": "Mailbox relays saved",

205
src/lib/nip05.ts

@ -1,6 +1,7 @@
import { LRUCache } from 'lru-cache' import { LRUCache } from 'lru-cache'
import { nip19 } from 'nostr-tools'
import { buildViteProxySitesFetchUrl } from '@/lib/vite-proxy-url' import { buildViteProxySitesFetchUrl } from '@/lib/vite-proxy-url'
import { isValidPubkey } from './pubkey' import { hexPubkeysEqual, isValidPubkey, normalizeHexPubkey } from './pubkey'
import { fetchWithTimeout } from '@/lib/fetch-with-timeout' import { fetchWithTimeout } from '@/lib/fetch-with-timeout'
import logger from '@/lib/logger' import logger from '@/lib/logger'
@ -11,6 +12,9 @@ type TVerifyNip05Result = {
relays?: string[] relays?: string[]
} }
/** Bumps when verification rules change so LRU does not serve stale false negatives. */
const VERIFY_CACHE_SCHEMA = 4
function asNip05LookupString(value: unknown): string { function asNip05LookupString(value: unknown): string {
if (typeof value === 'string') return value if (typeof value === 'string') return value
if (value == null) return '' if (value == null) return ''
@ -23,43 +27,146 @@ function asNip05LookupString(value: unknown): string {
return String(value) return String(value)
} }
/**
* Split `local@domain` on the **first** `@` only (NIP-05 local part must not contain `@`).
* `split('@')` breaks for domains like `user@sub.example.com` wrong domain.
*/
export function splitNip05Identifier(nip05Str: string): { name: string; domain: string } | null {
const s = nip05Str.trim()
const at = s.indexOf('@')
if (at <= 0 || at >= s.length - 1) return null
const name = s.slice(0, at).trim()
const domain = s.slice(at + 1).trim().replace(/\.$/, '')
if (!name || !domain) return null
return { name, domain: domain.toLowerCase() }
}
/** Normalize a `names` entry: hex (any case) or `npub1…` → lowercase hex, else null. */
function pubkeyHexFromWellKnownNamesValue(v: unknown): string | null {
if (typeof v !== 'string') return null
let t = v.trim()
if (t.startsWith('0x') || t.startsWith('0X')) t = t.slice(2).trim()
if (/^[0-9a-f]{64}$/i.test(t)) return t.toLowerCase()
if (t.startsWith('npub1')) {
try {
const { type, data } = nip19.decode(t)
if (type === 'npub' && typeof data === 'string' && isValidPubkey(data)) return data.toLowerCase()
} catch {
return null
}
}
return null
}
function getNamesEntryRaw(names: Record<string, unknown>, nip05Name: string): string | undefined {
if (!nip05Name || typeof names !== 'object' || names == null) return undefined
const asTrimmedString = (x: unknown): string | undefined =>
typeof x === 'string' && x.trim() ? x.trim() : undefined
const direct = asTrimmedString(names[nip05Name])
if (direct !== undefined) return direct
const want = nip05Name.toLowerCase()
for (const k of Object.keys(names)) {
if (k.toLowerCase() === want) return asTrimmedString(names[k])
}
return undefined
}
function pickRelayListForPubkey(
relays: Record<string, unknown> | undefined,
userPubkeyHex: string,
listedRaw: string,
nip05LocalName: string
): unknown {
if (!relays || typeof relays !== 'object') return undefined
const user = normalizeHexPubkey(userPubkeyHex)
const keysToTry = new Set<string>()
keysToTry.add(user)
keysToTry.add(userPubkeyHex.trim())
keysToTry.add(listedRaw.trim())
const listedHex = pubkeyHexFromWellKnownNamesValue(listedRaw)
if (listedHex) {
keysToTry.add(listedHex)
try {
keysToTry.add(nip19.npubEncode(listedHex))
} catch {
/* ignore */
}
}
for (const k of keysToTry) {
if (!k) continue
const v = relays[k]
if (Array.isArray(v)) return v
}
for (const k of Object.keys(relays)) {
if (pubkeyHexFromWellKnownNamesValue(k) === user) {
const v = relays[k]
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()
if (local) {
const want = local.toLowerCase()
for (const k of Object.keys(relays)) {
if (k.toLowerCase() === want) {
const v = relays[k]
if (Array.isArray(v)) return v
}
}
}
return undefined
}
const verifyNip05ResultCache = new LRUCache<string, TVerifyNip05Result>({ const verifyNip05ResultCache = new LRUCache<string, TVerifyNip05Result>({
max: 1000, max: 1000,
fetchMethod: (key) => { fetchMethod: (key) => {
const { nip05, pubkey } = JSON.parse(key) as { nip05?: unknown; pubkey?: unknown } const parsed = JSON.parse(key) as { s?: number; nip05?: unknown; pubkey?: unknown }
return _verifyNip05(asNip05LookupString(nip05), typeof pubkey === 'string' ? pubkey : '') const nip05 = asNip05LookupString(parsed.nip05).trim()
const pubkey = typeof parsed.pubkey === 'string' ? parsed.pubkey.trim() : ''
return _verifyNip05(nip05, pubkey)
} }
}) })
async function _verifyNip05(nip05: string, pubkey: string): Promise<TVerifyNip05Result> { async function _verifyNip05(nip05: string, pubkey: string): Promise<TVerifyNip05Result> {
const nip05Str = asNip05LookupString(nip05) const nip05Str = asNip05LookupString(nip05).trim()
const parts = nip05Str ? nip05Str.split('@') : [] const split = splitNip05Identifier(nip05Str)
const nip05Name = parts[0] const nip05Name = split?.name ?? ''
const nip05Domain = parts[1] const nip05Domain = split?.domain ?? ''
const result: TVerifyNip05Result = { isVerified: false, nip05Name, nip05Domain } const result: TVerifyNip05Result = { isVerified: false, nip05Name, nip05Domain }
if (!nip05Name || !nip05Domain || !pubkey) return result if (!split || !pubkey || !isValidPubkey(pubkey)) return result
const userHex = normalizeHexPubkey(pubkey)
const json = await fetchWellKnownNostrJson(nip05Domain, nip05Name) const json = await fetchWellKnownNostrJson(nip05Domain, nip05Name)
if (json) { if (!json) return result
const names = json.names as Record<string, string> | undefined
if (names?.[nip05Name] === pubkey) { const names = json.names as Record<string, unknown> | 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 relays = json.relays as Record<string, unknown> | undefined const relays = json.relays as Record<string, unknown> | undefined
const relayList = relays?.[pubkey] const relayList = pickRelayListForPubkey(relays, userHex, listedRaw, nip05Name)
return { ...result, isVerified: true, relays: Array.isArray(relayList) ? relayList : undefined } return { ...result, isVerified: true, relays: Array.isArray(relayList) ? (relayList as string[]) : undefined }
}
}
return result
} }
export async function verifyNip05(nip05: string, pubkey: string): Promise<TVerifyNip05Result> { export async function verifyNip05(nip05: string, pubkey: string): Promise<TVerifyNip05Result> {
const nip05Str = asNip05LookupString(nip05) const nip05Str = asNip05LookupString(nip05).trim()
const pubkeyStr = typeof pubkey === 'string' ? pubkey : '' const pubkeyStr = typeof pubkey === 'string' ? pubkey.trim() : ''
const result = await verifyNip05ResultCache.fetch(JSON.stringify({ nip05: nip05Str, pubkey: pubkeyStr })) const pubkeyNorm = isValidPubkey(pubkeyStr) ? normalizeHexPubkey(pubkeyStr) : pubkeyStr
if (result) { const cached = await verifyNip05ResultCache.fetch(
return result JSON.stringify({ s: VERIFY_CACHE_SCHEMA, nip05: nip05Str, pubkey: pubkeyNorm })
} )
const parts = nip05Str ? nip05Str.split('@') : [] if (cached) return cached
return { isVerified: false, nip05Name: parts[0], nip05Domain: parts[1] } const split = splitNip05Identifier(nip05Str)
return {
isVerified: false,
nip05Name: split?.name ?? '',
nip05Domain: split?.domain ?? ''
}
} }
export function getWellKnownNip05Url(domain: string, name?: string): string { export function getWellKnownNip05Url(domain: string, name?: string): string {
@ -74,17 +181,23 @@ export function getWellKnownNip05Url(domain: string, name?: string): string {
* Fetch `/.well-known/nostr.json` in the browser without tripping third-party CORS: * 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. * when `VITE_PROXY_SERVER` is set (production), use same-origin `/sites/?url=…` like OG preview.
*/ */
async function fetchWellKnownNostrJson(domain: string, name?: string): Promise<Record<string, unknown> | null> { async function fetchWellKnownNostrJsonOnce(
const targetUrl = getWellKnownNip05Url(domain, name) domain: string,
nameInQuery: string | undefined
): Promise<Record<string, unknown> | null> {
const targetUrl = getWellKnownNip05Url(domain, nameInQuery)
const proxyServer = import.meta.env.VITE_PROXY_SERVER?.trim() const proxyServer = import.meta.env.VITE_PROXY_SERVER?.trim()
const fetchUrl = proxyServer ? buildViteProxySitesFetchUrl(targetUrl, proxyServer) : targetUrl const fetchUrl = proxyServer ? buildViteProxySitesFetchUrl(targetUrl, proxyServer) : targetUrl
try { try {
const res = await fetchWithTimeout(fetchUrl, { const res = await fetchWithTimeout(fetchUrl, {
credentials: 'omit', credentials: 'omit',
headers: { Accept: 'application/json, text/plain;q=0.9,*/*;q=0.8' }, headers: {
Accept: 'application/nostr+json, application/json;q=0.9, text/plain;q=0.8, */*;q=0.1'
},
timeoutMs: 15_000 timeoutMs: 15_000
}) })
if (!res.ok) return null /** NIP-05: well-known MUST NOT redirect; following redirects can land on unrelated JSON. */
if (res.redirected || !res.ok) return null
const data: unknown = await res.json() const data: unknown = await res.json()
return data && typeof data === 'object' && !Array.isArray(data) ? (data as Record<string, unknown>) : null return data && typeof data === 'object' && !Array.isArray(data) ? (data as Record<string, unknown>) : null
} catch { } catch {
@ -92,21 +205,39 @@ async function fetchWellKnownNostrJson(domain: string, name?: string): Promise<R
} }
} }
/** Fetch `/.well-known/nostr.json` (with optional `?name=`). Retries without `name` if the entry is missing. */
async function fetchWellKnownNostrJson(domain: string, name?: string): Promise<Record<string, unknown> | null> {
const trimmedName = typeof name === 'string' ? name.trim() : ''
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<string, unknown>, 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<string, unknown>, trimmedName) != null) {
return full
}
}
return withQuery ?? full
}
export async function fetchPubkeysFromDomain(domain: string): Promise<string[]> { export async function fetchPubkeysFromDomain(domain: string): Promise<string[]> {
try { try {
const json = await fetchWellKnownNostrJson(domain) const json = await fetchWellKnownNostrJson(domain)
if (!json) return [] if (!json) return []
const pubkeySet = new Set<string>() const pubkeySet = new Set<string>()
return Object.values((json.names as Record<string, string>) || {}).filter((pubkey) => { const out: string[] = []
if (typeof pubkey !== 'string' || !isValidPubkey(pubkey)) { for (const v of Object.values((json.names as Record<string, unknown>) || {})) {
return false const hex = pubkeyHexFromWellKnownNamesValue(v)
} if (!hex || !isValidPubkey(hex)) continue
if (pubkeySet.has(pubkey)) { if (pubkeySet.has(hex)) continue
return false pubkeySet.add(hex)
out.push(hex)
} }
pubkeySet.add(pubkey) return out
return true
}) as string[]
} catch (error) { } catch (error) {
logger.error('Error fetching pubkeys from domain', { error, domain }) logger.error('Error fetching pubkeys from domain', { error, domain })
return [] return []

62
src/lib/profile-relay-search-filters.ts

@ -0,0 +1,62 @@
import type { Filter } from 'nostr-tools'
import { kinds } from 'nostr-tools'
import { decodeProfileSearchQueryToPubkeyHex } from '@/lib/profile-search-query'
/**
* REQ filters for kind 0 profile discovery: NIP-50 `search` plus tag-shaped queries
* (`#nip05`, `#name`, `#display_name`) that profile mirrors often index.
* When the query is a hex pubkey, `npub`, or `nprofile`, relays are queried by `authors`
* (NIP-50 text search does not match raw hex or bech32).
* Multiple filters are ORmerged by relays.
*/
export function buildProfileKind0SearchFilters(opts: {
search: string
limit: number
until?: number
}): Filter[] {
const search = opts.search.trim()
if (!search) return []
const limit = Math.max(1, Math.min(opts.limit ?? 50, 500))
const time =
typeof opts.until === 'number' && opts.until > 0 ? ({ until: opts.until } as Pick<Filter, 'until'>) : {}
const k = [kinds.Metadata] as number[]
const pubkeyHex = decodeProfileSearchQueryToPubkeyHex(search)
if (pubkeyHex) {
return [{ kinds: k, authors: [pubkeyHex], limit, ...time }]
}
const seen = new Set<string>()
const out: Filter[] = []
const add = (f: Filter) => {
const key = JSON.stringify(f)
if (seen.has(key)) return
seen.add(key)
out.push(f)
}
add({ kinds: k, search, limit, ...time })
if (search.includes('@')) {
const firstToken = search.split(/\s+/)[0] ?? search
const nipLower = firstToken.trim().toLowerCase()
if (nipLower) add({ kinds: k, '#nip05': [nipLower], limit, ...time })
const nipExact = firstToken.trim()
if (nipExact && nipExact !== nipLower) add({ kinds: k, '#nip05': [nipExact], limit, ...time })
}
const token = search.startsWith('@') ? search.slice(1).trim() : search.trim()
if (
token &&
!/\s/.test(token) &&
token.length <= 80 &&
/^[a-zA-Z0-9._-]+$/.test(token) &&
!token.includes('@')
) {
add({ kinds: k, '#name': [token], limit, ...time })
add({ kinds: k, '#display_name': [token], limit, ...time })
}
return out
}

195
src/providers/NostrProvider/index.tsx

@ -1005,81 +1005,6 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
void customEmojiService.init(userEmojiListEvent, account.pubkey, profileEvent) void customEmojiService.init(userEmojiListEvent, account.pubkey, profileEvent)
}, [userEmojiListEvent, account?.pubkey, profileEvent]) }, [userEmojiListEvent, account?.pubkey, profileEvent])
/**
* If session restore temporarily fell back to read-only (`npub`) while the stored
* account is still `nip-07`, periodically retry reconnecting the extension signer.
*/
useEffect(() => {
if (!account || account.signerType !== 'npub') return
const preferred = storage.getCurrentAccount()
if (!preferred || preferred.signerType !== 'nip-07') return
if (preferred.pubkey !== account.pubkey) return
let cancelled = false
let timer: ReturnType<typeof setTimeout> | null = null
let attempts = 0
const maxAttempts = 10
const schedule = (ms: number) => {
if (cancelled) return
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
void tryRecover()
}, ms)
}
const tryRecover = async () => {
if (cancelled || attempts >= maxAttempts) return
attempts += 1
try {
const nip07Signer = new Nip07Signer()
await nip07Signer.init()
const pubkey = await nip07Signer.getPublicKey()
if (pubkey.toLowerCase() !== preferred.pubkey.toLowerCase()) {
throw new Error(NIP07_SIGNER_PUBKEY_MISMATCH_MSG)
}
login(nip07Signer, preferred)
logger.info('[NostrProvider] Recovered NIP-07 signer from read-only fallback', {
pubkeySlice: pubkey.slice(0, 12),
attempts
})
return
} catch (error) {
if (isNip07SignerPubkeyMismatchError(error)) {
logger.info('[NostrProvider] NIP-07 recovery: extension key mismatch on attempt', {
attempts,
wantedPubkey: preferred.pubkey.slice(0, 12)
})
if (!nip07KeyMismatchToastShownRef.current) {
nip07KeyMismatchToastShownRef.current = true
toast.error(t('nip07.extensionKeyMismatch'), {
duration: 20_000,
action: { label: t('nip07.reloadPage'), onClick: () => window.location.reload() }
})
}
// Keep retrying — the extension may update its approved key after a moment.
schedule(3_000)
return
}
logger.info('[NostrProvider] NIP-07 recovery retry failed', {
pubkeySlice: preferred.pubkey.slice(0, 12),
attempts,
error: error instanceof Error ? error.message : String(error)
})
}
schedule(Math.min(10_000, attempts * 1_500))
}
schedule(1_200)
return () => {
cancelled = true
if (timer) clearTimeout(timer)
}
// nip07RecoveryBump is incremented by switchAccount after it updates storage following an
// npub fallback, so the loop re-fires with the correct preferred account.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [account, nip07RecoveryBump])
const hasNostrLoginHash = () => { const hasNostrLoginHash = () => {
return window.location.hash && window.location.hash.startsWith('#nostr-login') return window.location.hash && window.location.hash.startsWith('#nostr-login')
} }
@ -1350,11 +1275,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
(isNip07SignerPubkeyMismatchError(err) || isNip07SignerPubkeyMismatchError(lastNip07Err)) && (isNip07SignerPubkeyMismatchError(err) || isNip07SignerPubkeyMismatchError(lastNip07Err)) &&
!nip07KeyMismatchToastShownRef.current !nip07KeyMismatchToastShownRef.current
) { ) {
nip07KeyMismatchToastShownRef.current = true fireNip07ExtensionKeyMismatchToast()
toast.error(t('nip07.extensionKeyMismatch'), {
duration: 20_000,
action: { label: t('nip07.reloadPage'), onClick: () => window.location.reload() }
})
} }
return fallbackToReadOnlyNpub(storedAccount.pubkey, err) return fallbackToReadOnlyNpub(storedAccount.pubkey, err)
} }
@ -1391,6 +1312,120 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
return null return null
} }
/**
* Stored NIP-07 account pubkey no longer matches the extension (user switched keys).
* Drop the stale stored NIP-07 row and sign in with whatever pubkey the extension returns now.
*/
const adoptCurrentExtensionNip07Identity = useEventCallback(async () => {
try {
const nip07Signer = new Nip07Signer()
await nip07Signer.init()
const extPubkey = await nip07Signer.getPublicKey()
if (!extPubkey?.trim()) {
throw new Error('Empty pubkey from extension')
}
const preferred = storage.getCurrentAccount()
if (
preferred?.signerType === 'nip-07' &&
preferred.pubkey.toLowerCase() !== extPubkey.toLowerCase()
) {
removeAccount(preferred)
}
const existing = storage
.getAccounts()
.find((a) => a.pubkey.toLowerCase() === extPubkey.toLowerCase() && a.signerType === 'nip-07')
const act: TAccount = existing ?? { pubkey: extPubkey, signerType: 'nip-07' }
login(nip07Signer, act)
toast.success(t('nip07.switchedToExtensionIdentity'))
} catch (e) {
toast.error(`${t('nip07.adoptExtensionFailed')}: ${e instanceof Error ? e.message : String(e)}`)
}
})
const fireNip07ExtensionKeyMismatchToast = useCallback(() => {
if (nip07KeyMismatchToastShownRef.current) return
nip07KeyMismatchToastShownRef.current = true
toast.error(t('nip07.extensionKeyMismatch'), {
duration: 35_000,
action: { label: t('nip07.reloadPage'), onClick: () => window.location.reload() },
cancel: {
label: t('nip07.useExtensionIdentity'),
onClick: () => {
void adoptCurrentExtensionNip07Identity()
}
}
})
}, [t, adoptCurrentExtensionNip07Identity])
/**
* If session restore temporarily fell back to read-only (`npub`) while the stored
* account is still `nip-07`, periodically retry reconnecting the extension signer.
*/
useEffect(() => {
if (!account || account.signerType !== 'npub') return
const preferred = storage.getCurrentAccount()
if (!preferred || preferred.signerType !== 'nip-07') return
if (preferred.pubkey !== account.pubkey) return
let cancelled = false
let timer: ReturnType<typeof setTimeout> | null = null
let attempts = 0
const maxAttempts = 10
const schedule = (ms: number) => {
if (cancelled) return
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
void tryRecover()
}, ms)
}
const tryRecover = async () => {
if (cancelled || attempts >= maxAttempts) return
attempts += 1
try {
const nip07Signer = new Nip07Signer()
await nip07Signer.init()
const pubkey = await nip07Signer.getPublicKey()
if (pubkey.toLowerCase() !== preferred.pubkey.toLowerCase()) {
throw new Error(NIP07_SIGNER_PUBKEY_MISMATCH_MSG)
}
login(nip07Signer, preferred)
logger.info('[NostrProvider] Recovered NIP-07 signer from read-only fallback', {
pubkeySlice: pubkey.slice(0, 12),
attempts
})
return
} catch (error) {
if (isNip07SignerPubkeyMismatchError(error)) {
logger.info('[NostrProvider] NIP-07 recovery: extension key mismatch on attempt', {
attempts,
wantedPubkey: preferred.pubkey.slice(0, 12)
})
fireNip07ExtensionKeyMismatchToast()
// Keep retrying — the extension may update its approved key after a moment.
schedule(3_000)
return
}
logger.info('[NostrProvider] NIP-07 recovery retry failed', {
pubkeySlice: preferred.pubkey.slice(0, 12),
attempts,
error: error instanceof Error ? error.message : String(error)
})
}
schedule(Math.min(10_000, attempts * 1_500))
}
schedule(1_200)
return () => {
cancelled = true
if (timer) clearTimeout(timer)
}
// nip07RecoveryBump is incremented by switchAccount after it updates storage following an
// npub fallback, so the loop re-fires with the correct preferred account.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [account, nip07RecoveryBump, fireNip07ExtensionKeyMismatchToast])
const normalizeDraftEventTags = ( const normalizeDraftEventTags = (
draftEvent: TDraftEvent, draftEvent: TDraftEvent,
options?: { addClientTag?: boolean } options?: { addClientTag?: boolean }

3
src/providers/NostrProvider/nip-07.signer.ts

@ -27,9 +27,8 @@ export class Nip07Signer implements ISigner {
if (!this.signer) { if (!this.signer) {
throw new Error('Should call init() first') throw new Error('Should call init() first')
} }
if (!this.pubkey) { /** Always ask the extension — it may change the active key without a full page reload. */
this.pubkey = await this.signer.getPublicKey() this.pubkey = await this.signer.getPublicKey()
}
return this.pubkey return this.pubkey
} }

4
src/services/client-query.service.ts

@ -7,6 +7,7 @@ import {
SOCIAL_KIND_BLOCKED_RELAY_URLS, SOCIAL_KIND_BLOCKED_RELAY_URLS,
MAX_CONCURRENT_RELAY_CONNECTIONS, MAX_CONCURRENT_RELAY_CONNECTIONS,
MAX_CONCURRENT_SUBS_PER_RELAY, MAX_CONCURRENT_SUBS_PER_RELAY,
PROFILE_RELAY_URLS,
RELAY_FILTER_MAX_KINDS_PER_OBJECT, RELAY_FILTER_MAX_KINDS_PER_OBJECT,
RELAY_REQ_MAX_FILTERS_PER_MESSAGE, RELAY_REQ_MAX_FILTERS_PER_MESSAGE,
RELAY_POOL_CONNECTION_TIMEOUT_MS, RELAY_POOL_CONNECTION_TIMEOUT_MS,
@ -623,7 +624,8 @@ export class QueryService {
const searchableSet = new Set([ const searchableSet = new Set([
...SEARCHABLE_RELAY_URLS.map((u) => normalizeUrl(u) || u), ...SEARCHABLE_RELAY_URLS.map((u) => normalizeUrl(u) || u),
...nip66Service.getSearchableRelayUrls().map((u) => normalizeUrl(u) || u) ...nip66Service.getSearchableRelayUrls().map((u) => normalizeUrl(u) || u),
...PROFILE_RELAY_URLS.map((u) => normalizeUrl(u) || u).filter(Boolean)
]) ])
const groupedRequests = Array.from(grouped.entries()).map(([url, f]) => { const groupedRequests = Array.from(grouped.entries()).map(([url, f]) => {

77
src/services/client.service.ts

@ -29,6 +29,7 @@ import {
DEFAULT_FAVORITE_RELAYS, DEFAULT_FAVORITE_RELAYS,
NIP66_DISCOVERY_RELAY_URLS, NIP66_DISCOVERY_RELAY_URLS,
PROFILE_FETCH_RELAY_URLS, PROFILE_FETCH_RELAY_URLS,
PROFILE_RELAY_URLS,
READ_ONLY_RELAY_URLS, READ_ONLY_RELAY_URLS,
NIP42_POOL_AUTOMATIC_AUTH_RELAY_URLS, NIP42_POOL_AUTOMATIC_AUTH_RELAY_URLS,
SEARCHABLE_RELAY_URLS SEARCHABLE_RELAY_URLS
@ -174,6 +175,7 @@ import indexedDb from './indexed-db.service'
import { invalidateArchiveFootprintCache } from './event-archive.service' import { invalidateArchiveFootprintCache } from './event-archive.service'
import { notifyLiveActivitiesPrewarmComplete } from './live-activities-prewarm-bridge' import { notifyLiveActivitiesPrewarmComplete } from './live-activities-prewarm-bridge'
import nip66Service from './nip66.service' import nip66Service from './nip66.service'
import { buildProfileKind0SearchFilters } from '@/lib/profile-relay-search-filters'
import { patchRelayNoticeForFetchFailures } from '@/services/relay-notice-fetch-failure' import { patchRelayNoticeForFetchFailures } from '@/services/relay-notice-fetch-failure'
import { import {
compactFilterForRelayLog, compactFilterForRelayLog,
@ -3357,36 +3359,75 @@ class ClientService extends EventTarget {
const normalizedAll = dedupeNormalizeRelayUrlsOrdered( const normalizedAll = dedupeNormalizeRelayUrlsOrdered(
relayUrls.map((u) => normalizeUrl(u) || u).filter(Boolean) relayUrls.map((u) => normalizeUrl(u) || u).filter(Boolean)
) )
let urls = normalizedAll const profileRelayLayer = dedupeNormalizeRelayUrlsOrdered(
if (searchStr.length > 0) { PROFILE_RELAY_URLS.map((u) => normalizeUrl(u) || u).filter(Boolean)
)
const searchableSet = new Set([ const searchableSet = new Set([
...SEARCHABLE_RELAY_URLS.map((u) => normalizeUrl(u) || u), ...SEARCHABLE_RELAY_URLS.map((u) => normalizeUrl(u) || u),
...nip66Service.getSearchableRelayUrls().map((u) => normalizeUrl(u) || u) ...nip66Service.getSearchableRelayUrls().map((u) => normalizeUrl(u) || u),
...PROFILE_RELAY_URLS.map((u) => normalizeUrl(u) || u).filter(Boolean)
]) ])
let urls = normalizedAll
if (searchStr.length > 0) {
const searchCapable = normalizedAll.filter( const searchCapable = normalizedAll.filter(
(u) => searchableSet.has(u) || nip66Service.isRelaySearchable(u) (u) => searchableSet.has(u) || nip66Service.isRelaySearchable(u)
) )
if (searchCapable.length > 0) { urls = dedupeNormalizeRelayUrlsOrdered([...searchCapable, ...profileRelayLayer])
urls = searchCapable if (urls.length === 0) {
urls = normalizedAll
} }
} }
const events = await this.queryService.query( const limitCap = Math.max(1, Math.min(filter.limit ?? 100, 500))
urls, const queryFilter: Filter | Filter[] =
{ searchStr.length > 0
...filter, ? (() => {
kinds: [kinds.Metadata] const built = buildProfileKind0SearchFilters({
}, search: searchStr,
undefined, limit: limitCap,
{ until: filter.until
replaceableRace: true, })
// Search spans many relays; sub-second EOSE was cutting off almost all index relays. return built.length > 0 ? built : [{ ...filter, kinds: [kinds.Metadata] }]
})()
: { ...filter, kinds: [kinds.Metadata] }
const events = await this.queryService.query(urls, queryFilter, undefined, {
replaceableRace: false,
eoseTimeout: 4500, eoseTimeout: 4500,
globalTimeout: 9000 globalTimeout: 9000,
relayOpSource: 'ClientService.searchProfiles'
})
/** Which relays actually delivered each kind-0 id (for tuning SEARCHABLE_RELAY_URLS). DEBUG only. */
if (searchStr.length > 0) {
const relayHitCounts = new Map<string, number>()
for (const e of events) {
if (e.kind !== kinds.Metadata) continue
for (const u of this.queryService.getSeenEventRelayUrls(e.id)) {
const n = (normalizeUrl(u) || u).trim()
if (!n) continue
relayHitCounts.set(n, (relayHitCounts.get(n) ?? 0) + 1)
}
}
if (relayHitCounts.size > 0) {
const relayHits = [...relayHitCounts.entries()].sort((a, b) => b[1] - a[1])
logger.debug('[ClientService.searchProfiles] kind=0 deliveries by relay URL (count = events relay sent for this query)', {
searchPreview: searchStr.slice(0, 80),
totalKind0Events: events.filter((e) => e.kind === kinds.Metadata).length,
relayHits: Object.fromEntries(relayHits)
})
}
} }
)
const profileEvents = events.sort((a, b) => b.created_at - a.created_at) const byPk = new Map<string, NEvent>()
for (const e of events) {
if (e.kind !== kinds.Metadata) continue
const prev = byPk.get(e.pubkey)
if (!prev || e.created_at > prev.created_at) {
byPk.set(e.pubkey, e)
}
}
const profileEvents = [...byPk.values()].sort((a, b) => b.created_at - a.created_at).slice(0, limitCap)
await Promise.allSettled(profileEvents.map((profile) => this.addUsernameToIndex(profile))) await Promise.allSettled(profileEvents.map((profile) => this.addUsernameToIndex(profile)))
profileEvents.forEach((profile) => this.updateProfileEventCache(profile)) profileEvents.forEach((profile) => this.updateProfileEventCache(profile))
return profileEvents.map((profileEvent) => getProfileFromEvent(profileEvent)) return profileEvents.map((profileEvent) => getProfileFromEvent(profileEvent))

Loading…
Cancel
Save