Browse Source
- Created profileCache utility to fetch and cache kind 0 (Metadata) events - Replace pubkeys with display names throughout the visualization: - Node tooltips show author display names instead of pubkeys - p tags in tooltips show display names - Network nodes store display names in author field - Fetch user profiles when loading events for better UX - Fixed infinite loading loop by: - Adding isFetching guard to prevent concurrent fetchEvents calls - Temporarily disabling the re-enabled kinds watcher that was causing loops - Extract pubkeys from event content (nostr:npub1... format) for profile fetching 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>master
4 changed files with 394 additions and 32 deletions
@ -0,0 +1,218 @@
@@ -0,0 +1,218 @@
|
||||
import type { NDKEvent } from "@nostr-dev-kit/ndk"; |
||||
import { ndkInstance } from "$lib/ndk"; |
||||
import { get } from "svelte/store"; |
||||
|
||||
interface ProfileData { |
||||
display_name?: string; |
||||
name?: string; |
||||
picture?: string; |
||||
about?: string; |
||||
} |
||||
|
||||
// Cache for user profiles
|
||||
const profileCache = new Map<string, ProfileData>(); |
||||
|
||||
/** |
||||
* Fetches profile data for a pubkey |
||||
* @param pubkey - The public key to fetch profile for |
||||
* @returns Profile data or null if not found |
||||
*/ |
||||
async function fetchProfile(pubkey: string): Promise<ProfileData | null> { |
||||
try { |
||||
const ndk = get(ndkInstance); |
||||
const profileEvents = await ndk.fetchEvents({ |
||||
kinds: [0], |
||||
authors: [pubkey], |
||||
limit: 1 |
||||
}); |
||||
|
||||
if (profileEvents.size === 0) { |
||||
return null; |
||||
} |
||||
|
||||
// Get the most recent profile event
|
||||
const profileEvent = Array.from(profileEvents)[0]; |
||||
|
||||
try { |
||||
const content = JSON.parse(profileEvent.content); |
||||
return content as ProfileData; |
||||
} catch (e) { |
||||
console.error("Failed to parse profile content:", e); |
||||
return null; |
||||
} |
||||
} catch (e) { |
||||
console.error("Failed to fetch profile:", e); |
||||
return null; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Gets the display name for a pubkey, using cache |
||||
* @param pubkey - The public key to get display name for |
||||
* @returns Display name, name, or shortened pubkey |
||||
*/ |
||||
export async function getDisplayName(pubkey: string): Promise<string> { |
||||
// Check cache first
|
||||
if (profileCache.has(pubkey)) { |
||||
const profile = profileCache.get(pubkey)!; |
||||
return profile.display_name || profile.name || shortenPubkey(pubkey); |
||||
} |
||||
|
||||
// Fetch profile
|
||||
const profile = await fetchProfile(pubkey); |
||||
if (profile) { |
||||
profileCache.set(pubkey, profile); |
||||
return profile.display_name || profile.name || shortenPubkey(pubkey); |
||||
} |
||||
|
||||
// Fallback to shortened pubkey
|
||||
return shortenPubkey(pubkey); |
||||
} |
||||
|
||||
/** |
||||
* Batch fetches profiles for multiple pubkeys |
||||
* @param pubkeys - Array of public keys to fetch profiles for |
||||
*/ |
||||
export async function batchFetchProfiles(pubkeys: string[]): Promise<void> { |
||||
// Filter out already cached pubkeys
|
||||
const uncachedPubkeys = pubkeys.filter(pk => !profileCache.has(pk)); |
||||
|
||||
if (uncachedPubkeys.length === 0) { |
||||
return; |
||||
} |
||||
|
||||
try { |
||||
const ndk = get(ndkInstance); |
||||
const profileEvents = await ndk.fetchEvents({ |
||||
kinds: [0], |
||||
authors: uncachedPubkeys |
||||
}); |
||||
|
||||
// Process each profile event
|
||||
profileEvents.forEach((event: NDKEvent) => { |
||||
try { |
||||
const content = JSON.parse(event.content); |
||||
profileCache.set(event.pubkey, content as ProfileData); |
||||
} catch (e) { |
||||
console.error("Failed to parse profile content:", e); |
||||
} |
||||
}); |
||||
} catch (e) { |
||||
console.error("Failed to batch fetch profiles:", e); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Gets display name synchronously from cache |
||||
* @param pubkey - The public key to get display name for |
||||
* @returns Display name, name, or shortened pubkey |
||||
*/ |
||||
export function getDisplayNameSync(pubkey: string): string { |
||||
if (profileCache.has(pubkey)) { |
||||
const profile = profileCache.get(pubkey)!; |
||||
return profile.display_name || profile.name || shortenPubkey(pubkey); |
||||
} |
||||
return shortenPubkey(pubkey); |
||||
} |
||||
|
||||
/** |
||||
* Shortens a pubkey for display |
||||
* @param pubkey - The public key to shorten |
||||
* @returns Shortened pubkey (first 8 chars...last 4 chars) |
||||
*/ |
||||
function shortenPubkey(pubkey: string): string { |
||||
if (pubkey.length <= 12) return pubkey; |
||||
return `${pubkey.slice(0, 8)}...${pubkey.slice(-4)}`; |
||||
} |
||||
|
||||
/** |
||||
* Clears the profile cache |
||||
*/ |
||||
export function clearProfileCache(): void { |
||||
profileCache.clear(); |
||||
} |
||||
|
||||
/** |
||||
* Extracts all pubkeys from events (authors and p tags) |
||||
* @param events - Array of events to extract pubkeys from |
||||
* @returns Set of unique pubkeys |
||||
*/ |
||||
export function extractPubkeysFromEvents(events: NDKEvent[]): Set<string> { |
||||
const pubkeys = new Set<string>(); |
||||
|
||||
events.forEach(event => { |
||||
// Add author pubkey
|
||||
if (event.pubkey) { |
||||
pubkeys.add(event.pubkey); |
||||
} |
||||
|
||||
// Add pubkeys from p tags
|
||||
const pTags = event.getMatchingTags("p"); |
||||
pTags.forEach(tag => { |
||||
if (tag[1]) { |
||||
pubkeys.add(tag[1]); |
||||
} |
||||
}); |
||||
|
||||
// Extract pubkeys from content (nostr:npub1... format)
|
||||
const npubPattern = /nostr:npub1[a-z0-9]{58}/g; |
||||
const matches = event.content?.match(npubPattern) || []; |
||||
matches.forEach(match => { |
||||
try { |
||||
const npub = match.replace('nostr:', ''); |
||||
const decoded = nip19.decode(npub); |
||||
if (decoded.type === 'npub') { |
||||
pubkeys.add(decoded.data as string); |
||||
} |
||||
} catch (e) { |
||||
// Invalid npub, ignore
|
||||
} |
||||
}); |
||||
}); |
||||
|
||||
return pubkeys; |
||||
} |
||||
|
||||
/** |
||||
* Replaces pubkeys in content with display names |
||||
* @param content - The content to process |
||||
* @returns Content with pubkeys replaced by display names |
||||
*/ |
||||
export function replaceContentPubkeys(content: string): string { |
||||
if (!content) return content; |
||||
|
||||
// Replace nostr:npub1... references
|
||||
const npubPattern = /nostr:npub1[a-z0-9]{58}/g; |
||||
let result = content; |
||||
|
||||
const matches = content.match(npubPattern) || []; |
||||
matches.forEach(match => { |
||||
try { |
||||
const npub = match.replace('nostr:', ''); |
||||
const decoded = nip19.decode(npub); |
||||
if (decoded.type === 'npub') { |
||||
const pubkey = decoded.data as string; |
||||
const displayName = getDisplayNameSync(pubkey); |
||||
result = result.replace(match, `@${displayName}`); |
||||
} |
||||
} catch (e) { |
||||
// Invalid npub, leave as is
|
||||
} |
||||
}); |
||||
|
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Replaces pubkey references in text with display names |
||||
* @param text - Text that may contain pubkey references |
||||
* @returns Text with pubkeys replaced by display names |
||||
*/ |
||||
export function replacePubkeysWithDisplayNames(text: string): string { |
||||
// Match hex pubkeys (64 characters)
|
||||
const pubkeyRegex = /\b[0-9a-fA-F]{64}\b/g; |
||||
|
||||
return text.replace(pubkeyRegex, (match) => { |
||||
return getDisplayNameSync(match); |
||||
}); |
||||
} |
||||
Loading…
Reference in new issue