From 00d00032f6302fe6121d2939cd6819ebd953b3cc Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Jan 2026 16:13:41 +0000 Subject: [PATCH] fix: Add decrypt button to NIP-17 chat and load cached gift wraps - Add decrypt button to ChatViewer header for NIP-17 conversations - Show pending count with unlock icon, allows decryption directly from chat - Load cached gift wraps from Dexie on cold start (not just EventStore) - EventStore is in-memory only, so on app reload we need to query Dexie - Add events back to EventStore after loading from Dexie for consistency - This fixes cold start scenarios where chat $me shows nothing --- src/components/ChatViewer.tsx | 59 +++++++++++++++++++++++++ src/lib/chat/adapters/nip-17-adapter.ts | 36 +++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/src/components/ChatViewer.tsx b/src/components/ChatViewer.tsx index a389126..f41eb10 100644 --- a/src/components/ChatViewer.tsx +++ b/src/components/ChatViewer.tsx @@ -9,6 +9,7 @@ import { AlertTriangle, RefreshCw, Paperclip, + Unlock, } from "lucide-react"; import { getZapRequest } from "applesauce-common/helpers/zap"; import { toast } from "sonner"; @@ -378,6 +379,42 @@ export function ChatViewer({ } }, [protocol]); + // NIP-17 decrypt state + const [isDecrypting, setIsDecrypting] = useState(false); + const pendingCount = + use$( + () => + protocol === "nip-17" ? nip17Adapter.getPendingCount$() : undefined, + [protocol], + ) ?? 0; + + // Handle decrypt for NIP-17 + const handleDecrypt = useCallback(async () => { + if (protocol !== "nip-17") return; + setIsDecrypting(true); + try { + const result = await nip17Adapter.decryptPending(); + console.log( + `[Chat] Decrypted ${result.success} messages, ${result.failed} failed`, + ); + if (result.success > 0) { + toast.success( + `Decrypted ${result.success} message${result.success !== 1 ? "s" : ""}`, + ); + } + if (result.failed > 0) { + toast.warning( + `${result.failed} message${result.failed !== 1 ? "s" : ""} failed to decrypt`, + ); + } + } catch (error) { + console.error("[Chat] Decrypt error:", error); + toast.error("Failed to decrypt messages"); + } finally { + setIsDecrypting(false); + } + }, [protocol]); + // State for retry trigger const [retryCount, setRetryCount] = useState(0); @@ -810,6 +847,28 @@ export function ChatViewer({
+ {/* NIP-17 decrypt button */} + {protocol === "nip-17" && pendingCount > 0 && ( + + )} {(conversation.type === "group" || conversation.type === "live-chat" || conversation.type === "dm") && ( diff --git a/src/lib/chat/adapters/nip-17-adapter.ts b/src/lib/chat/adapters/nip-17-adapter.ts index 43cf5db..fb66994 100644 --- a/src/lib/chat/adapters/nip-17-adapter.ts +++ b/src/lib/chat/adapters/nip-17-adapter.ts @@ -34,6 +34,7 @@ import accountManager from "@/services/accounts"; import { hub } from "@/services/hub"; import { relayListCache } from "@/services/relay-list-cache"; import { AGGREGATOR_RELAYS } from "@/services/loaders"; +import { getEventsForFilters } from "@/services/event-cache"; import { isNip05, resolveNip05 } from "@/lib/nip05"; import { getDisplayName } from "@/lib/nostr-utils"; import { isValidHexPubkey } from "@/lib/nostr-validation"; @@ -660,6 +661,10 @@ export class Nip17Adapter extends ChatProtocolAdapter { this.subscriptionActive = true; + // First, load any cached gift wraps from EventStore (persisted to Dexie) + // This is critical for cold start scenarios + await this.loadCachedGiftWraps(pubkey); + // Subscribe to eventStore.insert$ to catch gift wraps added locally (e.g., after sending) // This is critical for immediate display of sent messages const insertSub = eventStore.insert$.subscribe((event) => { @@ -724,6 +729,37 @@ export class Nip17Adapter extends ChatProtocolAdapter { this.subscriptions.set(conversationId, relaySub); } + /** + * Load cached gift wraps from Dexie (persistent storage) + * This is called on cold start to restore previously received gift wraps + * We query Dexie directly because EventStore is in-memory and empty on cold start + */ + private async loadCachedGiftWraps(pubkey: string): Promise { + try { + // Query Dexie directly for cached gift wraps addressed to this user + // EventStore is in-memory only, so on cold start it's empty + const cachedGiftWraps = await getEventsForFilters([ + { kinds: [GIFT_WRAP_KIND], "#p": [pubkey] }, + ]); + + if (cachedGiftWraps.length > 0) { + console.log( + `[NIP-17] Loading ${cachedGiftWraps.length} cached gift wrap(s) from Dexie`, + ); + for (const giftWrap of cachedGiftWraps) { + // Add to EventStore so other parts of the app can access it + eventStore.add(giftWrap); + // Handle in adapter state + this.handleGiftWrap(giftWrap); + } + } else { + console.log("[NIP-17] No cached gift wraps found in Dexie"); + } + } catch (error) { + console.warn("[NIP-17] Failed to load cached gift wraps:", error); + } + } + /** * Handle a received or sent gift wrap */