From 5aed48d0d49fbdb32f5a193bfd83f6b67f0efb8c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Jan 2026 12:29:59 +0000 Subject: [PATCH] fix: show display names when pasting npub/nprofile in editors Previously, pasting npub/nprofile strings would only check the EventStore's in-memory cache for profiles. If a profile was cached in IndexedDB but not yet loaded into the EventStore, it would show a hex preview instead. This adds a ProfileCache service that: - Loads profiles from IndexedDB on startup for instant access - Subscribes to EventStore for new profiles as they arrive - Provides synchronous lookups for the paste handler Also uses consistent fallback format (XXXX:YYYY) when no profile is found. --- .../editor/extensions/nostr-paste-handler.ts | 32 ++----- src/services/profile-cache.ts | 93 +++++++++++++++++++ 2 files changed, 101 insertions(+), 24 deletions(-) create mode 100644 src/services/profile-cache.ts diff --git a/src/components/editor/extensions/nostr-paste-handler.ts b/src/components/editor/extensions/nostr-paste-handler.ts index 08d6986..3f87447 100644 --- a/src/components/editor/extensions/nostr-paste-handler.ts +++ b/src/components/editor/extensions/nostr-paste-handler.ts @@ -1,37 +1,21 @@ import { Extension } from "@tiptap/core"; import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state"; import { nip19 } from "nostr-tools"; -import eventStore from "@/services/event-store"; -import { getProfileContent } from "applesauce-core/helpers"; +import profileCache from "@/services/profile-cache"; import { getDisplayName } from "@/lib/nostr-utils"; /** * Helper to get display name for a pubkey (synchronous lookup from cache) */ function getDisplayNameForPubkey(pubkey: string): string { - try { - // Try to get profile from event store (check if it's a BehaviorSubject with .value) - const profile$ = eventStore.replaceable(0, pubkey) as any; - if (profile$ && profile$.value) { - const profileEvent = profile$.value; - if (profileEvent) { - const content = getProfileContent(profileEvent); - if (content) { - // Use the Grimoire helper which handles fallbacks - return getDisplayName(pubkey, content); - } - } - } - } catch (err) { - // Ignore errors, fall through to default - console.debug( - "[NostrPasteHandler] Could not get profile for", - pubkey.slice(0, 8), - err, - ); + // Check profile cache first (includes Dexie + EventStore profiles) + const cachedProfile = profileCache.get(pubkey); + if (cachedProfile) { + return getDisplayName(pubkey, cachedProfile); } - // Fallback to short pubkey - return pubkey.slice(0, 8); + + // Fallback to placeholder format + return getDisplayName(pubkey, undefined); } /** diff --git a/src/services/profile-cache.ts b/src/services/profile-cache.ts new file mode 100644 index 0000000..b89948a --- /dev/null +++ b/src/services/profile-cache.ts @@ -0,0 +1,93 @@ +import type { NostrEvent } from "nostr-tools"; +import { + getProfileContent, + type ProfileContent, +} from "applesauce-core/helpers"; +import eventStore from "./event-store"; +import db from "./db"; + +/** + * Simple singleton profile cache for synchronous display name lookups. + * Used by paste handlers and other places that need instant profile access. + */ +class ProfileCache { + private profiles = new Map(); + private initialized = false; + + /** + * Initialize the cache by: + * 1. Loading profiles from Dexie (IndexedDB) + * 2. Subscribing to EventStore for new kind 0 events + */ + async init(): Promise { + if (this.initialized) return; + this.initialized = true; + + // Load from Dexie first (persisted profiles) + try { + const cachedProfiles = await db.profiles.toArray(); + for (const profile of cachedProfiles) { + const { pubkey, created_at: _created_at, ...content } = profile; + this.profiles.set(pubkey, content as ProfileContent); + } + console.debug( + `[ProfileCache] Loaded ${cachedProfiles.length} profiles from IndexedDB`, + ); + } catch (err) { + console.warn("[ProfileCache] Failed to load from IndexedDB:", err); + } + + // Subscribe to EventStore for new kind 0 events + eventStore.timeline([{ kinds: [0] }]).subscribe({ + next: (events) => { + for (const event of events) { + this.addFromEvent(event); + } + }, + error: (err) => { + console.warn("[ProfileCache] EventStore subscription error:", err); + }, + }); + } + + /** + * Add a profile from a kind 0 event + */ + addFromEvent(event: NostrEvent): void { + if (event.kind !== 0) return; + + const content = getProfileContent(event); + if (content) { + this.profiles.set(event.pubkey, content); + } + } + + /** + * Get profile content for a pubkey (synchronous) + */ + get(pubkey: string): ProfileContent | undefined { + return this.profiles.get(pubkey); + } + + /** + * Check if a profile is cached + */ + has(pubkey: string): boolean { + return this.profiles.has(pubkey); + } + + /** + * Get the number of cached profiles + */ + get size(): number { + return this.profiles.size; + } +} + +// Singleton instance +const profileCache = new ProfileCache(); + +// Auto-initialize on module load +profileCache.init(); + +export default profileCache;