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.
This commit is contained in:
Claude
2026-01-21 12:29:59 +00:00
parent 94982ca7f4
commit 5aed48d0d4
2 changed files with 101 additions and 24 deletions

View File

@@ -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);
}
/**

View File

@@ -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<string, ProfileContent>();
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<void> {
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;