mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-11 16:07:15 +02:00
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:
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
93
src/services/profile-cache.ts
Normal file
93
src/services/profile-cache.ts
Normal 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;
|
||||
Reference in New Issue
Block a user