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
This commit is contained in:
Claude
2026-01-14 16:13:41 +00:00
parent 797b3b6782
commit 00d00032f6
2 changed files with 95 additions and 0 deletions

View File

@@ -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({
<div className="flex items-center gap-2 text-xs text-muted-foreground p-1">
<MembersDropdown participants={derivedParticipants} />
<RelaysDropdown conversation={conversation} />
{/* NIP-17 decrypt button */}
{protocol === "nip-17" && pendingCount > 0 && (
<Button
variant="outline"
size="sm"
className="h-6 gap-1 text-xs px-2"
onClick={handleDecrypt}
disabled={isDecrypting}
>
{isDecrypting ? (
<>
<Loader2 className="size-3 animate-spin" />
<span className="hidden sm:inline">Decrypting...</span>
</>
) : (
<>
<Unlock className="size-3" />
<span>{pendingCount}</span>
</>
)}
</Button>
)}
{(conversation.type === "group" ||
conversation.type === "live-chat" ||
conversation.type === "dm") && (

View File

@@ -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<void> {
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
*/