mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-10 23:47:12 +02:00
fix: Improve NIP-17 message sending and decryption tracking
Multiple fixes for NIP-17 gift-wrapped DMs: 1. Fix sent messages not appearing immediately - After sending, query eventStore for new gift wraps - Pick up our own gift wrap and add to local state - Message now appears right after sending 2. Track failed decryption separately - Add failedGiftWraps$ BehaviorSubject - Mark gift wraps as failed after decrypt error - Exclude failed from pending count (don't retry) - getFailedCount() for UI display if needed 3. Ensure subscription is active - Add ensureSubscription() public method - Track subscriptionActive state to prevent duplicates - Call ensureSubscription in InboxViewer on mount - Call ensureSubscription in ChatViewer for NIP-17 4. Refactor gift wrap handling - Extract handleGiftWrap() for consistent processing - Add removeFromPending() and markAsFailed() helpers - Better error handling in subscription
This commit is contained in:
@@ -371,6 +371,13 @@ export function ChatViewer({
|
||||
// Get the appropriate adapter for this protocol
|
||||
const adapter = useMemo(() => getAdapter(protocol), [protocol]);
|
||||
|
||||
// Ensure NIP-17 subscription is active when ChatViewer mounts
|
||||
useEffect(() => {
|
||||
if (protocol === "nip-17") {
|
||||
nip17Adapter.ensureSubscription();
|
||||
}
|
||||
}, [protocol]);
|
||||
|
||||
// State for retry trigger
|
||||
const [retryCount, setRetryCount] = useState(0);
|
||||
|
||||
|
||||
@@ -261,6 +261,13 @@ export function InboxViewer() {
|
||||
// NIP-17 adapter singleton instance
|
||||
const adapter = nip17Adapter;
|
||||
|
||||
// Ensure subscription is active when component mounts
|
||||
useEffect(() => {
|
||||
if (activePubkey) {
|
||||
adapter.ensureSubscription();
|
||||
}
|
||||
}, [adapter, activePubkey]);
|
||||
|
||||
// Get pending count
|
||||
const pendingCount = use$(() => adapter.getPendingCount$(), [adapter]) ?? 0;
|
||||
|
||||
|
||||
@@ -65,9 +65,15 @@ export class Nip17Adapter extends ChatProtocolAdapter {
|
||||
/** Track pending (undecrypted) gift wrap IDs */
|
||||
private pendingGiftWraps$ = new BehaviorSubject<Set<string>>(new Set());
|
||||
|
||||
/** Track failed (could not decrypt) gift wrap IDs */
|
||||
private failedGiftWraps$ = new BehaviorSubject<Set<string>>(new Set());
|
||||
|
||||
/** Observable of gift wrap events from event store */
|
||||
private giftWraps$ = new BehaviorSubject<NostrEvent[]>([]);
|
||||
|
||||
/** Track if subscription is active */
|
||||
private subscriptionActive = false;
|
||||
|
||||
/**
|
||||
* Parse identifier - accepts npub, nprofile, hex pubkey, NIP-05, or $me
|
||||
*/
|
||||
@@ -340,6 +346,9 @@ export class Nip17Adapter extends ChatProtocolAdapter {
|
||||
throw new Error("No conversation recipient found");
|
||||
}
|
||||
|
||||
// Track existing gift wrap IDs before sending
|
||||
const existingIds = new Set(this.giftWraps$.value.map((g) => g.id));
|
||||
|
||||
// Use applesauce's SendWrappedMessage action
|
||||
// This handles:
|
||||
// - Creating the wrapped message rumor
|
||||
@@ -350,6 +359,43 @@ export class Nip17Adapter extends ChatProtocolAdapter {
|
||||
console.log(
|
||||
`[NIP-17] Sent wrapped message to ${recipientPubkey.slice(0, 8)}...${isSelfConversation ? " (saved)" : ""}`,
|
||||
);
|
||||
|
||||
// After sending, check eventStore for new gift wraps addressed to us
|
||||
// The publishEvent function adds events to eventStore, so our own gift wrap should be there
|
||||
this.pickUpNewGiftWrapsFromStore(activePubkey, existingIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick up new gift wraps from eventStore that we don't have yet
|
||||
* Used after sending to ensure sent message appears immediately
|
||||
*/
|
||||
private async pickUpNewGiftWrapsFromStore(
|
||||
pubkey: string,
|
||||
existingIds: Set<string>,
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Query eventStore for gift wraps addressed to us
|
||||
const giftWraps = await firstValueFrom(
|
||||
eventStore
|
||||
.timeline([{ kinds: [GIFT_WRAP_KIND], "#p": [pubkey] }])
|
||||
.pipe(first()),
|
||||
{ defaultValue: [] },
|
||||
);
|
||||
|
||||
let added = 0;
|
||||
for (const giftWrap of giftWraps) {
|
||||
if (!existingIds.has(giftWrap.id)) {
|
||||
this.handleGiftWrap(giftWrap);
|
||||
added++;
|
||||
}
|
||||
}
|
||||
|
||||
if (added > 0) {
|
||||
console.log(`[NIP-17] Picked up ${added} new gift wrap(s) from store`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("[NIP-17] Failed to pick up gift wraps from store:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -398,17 +444,32 @@ export class Nip17Adapter extends ChatProtocolAdapter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of pending (undecrypted) gift wraps
|
||||
* Get count of pending (undecrypted) gift wraps (excludes failed)
|
||||
*/
|
||||
getPendingCount(): number {
|
||||
return this.pendingGiftWraps$.value.size;
|
||||
const pending = this.pendingGiftWraps$.value;
|
||||
const failed = this.failedGiftWraps$.value;
|
||||
// Only count pending that haven't failed
|
||||
return Array.from(pending).filter((id) => !failed.has(id)).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get observable of pending gift wrap count
|
||||
* Get observable of pending gift wrap count (excludes failed)
|
||||
*/
|
||||
getPendingCount$(): Observable<number> {
|
||||
return this.pendingGiftWraps$.pipe(map((set) => set.size));
|
||||
return this.pendingGiftWraps$.pipe(
|
||||
map((pending) => {
|
||||
const failed = this.failedGiftWraps$.value;
|
||||
return Array.from(pending).filter((id) => !failed.has(id)).length;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of failed gift wraps
|
||||
*/
|
||||
getFailedCount(): number {
|
||||
return this.failedGiftWraps$.value.size;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -422,7 +483,11 @@ export class Nip17Adapter extends ChatProtocolAdapter {
|
||||
throw new Error("No active account");
|
||||
}
|
||||
|
||||
const pendingIds = Array.from(this.pendingGiftWraps$.value);
|
||||
// Only try pending that haven't already failed
|
||||
const failedSet = this.failedGiftWraps$.value;
|
||||
const pendingIds = Array.from(this.pendingGiftWraps$.value).filter(
|
||||
(id) => !failedSet.has(id),
|
||||
);
|
||||
let success = 0;
|
||||
let failed = 0;
|
||||
|
||||
@@ -434,6 +499,8 @@ export class Nip17Adapter extends ChatProtocolAdapter {
|
||||
);
|
||||
|
||||
if (!giftWrap) {
|
||||
// Mark as failed - couldn't find the event
|
||||
this.markAsFailed(giftWrapId);
|
||||
failed++;
|
||||
continue;
|
||||
}
|
||||
@@ -441,9 +508,7 @@ export class Nip17Adapter extends ChatProtocolAdapter {
|
||||
// Already unlocked?
|
||||
if (isGiftWrapUnlocked(giftWrap)) {
|
||||
// Remove from pending
|
||||
const pending = new Set(this.pendingGiftWraps$.value);
|
||||
pending.delete(giftWrapId);
|
||||
this.pendingGiftWraps$.next(pending);
|
||||
this.removeFromPending(giftWrapId);
|
||||
success++;
|
||||
continue;
|
||||
}
|
||||
@@ -451,10 +516,8 @@ export class Nip17Adapter extends ChatProtocolAdapter {
|
||||
// Decrypt using signer - applesauce handles caching automatically
|
||||
await unlockGiftWrap(giftWrap, signer);
|
||||
|
||||
// Remove from pending
|
||||
const pending = new Set(this.pendingGiftWraps$.value);
|
||||
pending.delete(giftWrapId);
|
||||
this.pendingGiftWraps$.next(pending);
|
||||
// Remove from pending (success)
|
||||
this.removeFromPending(giftWrapId);
|
||||
|
||||
// Refresh gift wraps list
|
||||
this.giftWraps$.next([...this.giftWraps$.value]);
|
||||
@@ -465,6 +528,8 @@ export class Nip17Adapter extends ChatProtocolAdapter {
|
||||
`[NIP-17] Failed to decrypt gift wrap ${giftWrapId}:`,
|
||||
error,
|
||||
);
|
||||
// Mark as failed so we don't retry
|
||||
this.markAsFailed(giftWrapId);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
@@ -472,6 +537,24 @@ export class Nip17Adapter extends ChatProtocolAdapter {
|
||||
return { success, failed };
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a gift wrap as failed (won't retry decryption)
|
||||
*/
|
||||
private markAsFailed(giftWrapId: string): void {
|
||||
const failed = new Set(this.failedGiftWraps$.value);
|
||||
failed.add(giftWrapId);
|
||||
this.failedGiftWraps$.next(failed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a gift wrap from pending
|
||||
*/
|
||||
private removeFromPending(giftWrapId: string): void {
|
||||
const pending = new Set(this.pendingGiftWraps$.value);
|
||||
pending.delete(giftWrapId);
|
||||
this.pendingGiftWraps$.next(pending);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all conversations from decrypted rumors
|
||||
*/
|
||||
@@ -574,16 +657,45 @@ export class Nip17Adapter extends ChatProtocolAdapter {
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== Public Methods for Subscription Management ====================
|
||||
|
||||
/**
|
||||
* Ensure gift wrap subscription is active for the current user
|
||||
* Call this when InboxViewer or ChatViewer mounts
|
||||
*/
|
||||
ensureSubscription(): void {
|
||||
const activePubkey = accountManager.active$.value?.pubkey;
|
||||
if (!activePubkey) {
|
||||
console.warn("[NIP-17] Cannot start subscription: no active account");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.subscriptionActive) {
|
||||
console.log("[NIP-17] Starting gift wrap subscription");
|
||||
this.subscribeToGiftWraps(activePubkey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if subscription is currently active
|
||||
*/
|
||||
isSubscriptionActive(): boolean {
|
||||
return this.subscriptionActive;
|
||||
}
|
||||
|
||||
// ==================== Private Methods ====================
|
||||
|
||||
/**
|
||||
* Subscribe to gift wraps for the user from their inbox relays
|
||||
*/
|
||||
private async subscribeToGiftWraps(pubkey: string): Promise<void> {
|
||||
const conversationId = `nip-17:inbox:${pubkey}`;
|
||||
// Don't create duplicate subscriptions
|
||||
if (this.subscriptionActive) {
|
||||
console.log("[NIP-17] Subscription already active, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
// Clean up existing subscription
|
||||
this.cleanup(conversationId);
|
||||
const conversationId = `nip-17:inbox:${pubkey}`;
|
||||
|
||||
// Get user's private inbox relays (kind 10050)
|
||||
const inboxRelays = await this.fetchInboxRelays(pubkey);
|
||||
@@ -605,6 +717,8 @@ export class Nip17Adapter extends ChatProtocolAdapter {
|
||||
"#p": [pubkey],
|
||||
};
|
||||
|
||||
this.subscriptionActive = true;
|
||||
|
||||
const subscription = pool
|
||||
.subscription(inboxRelays, [filter], { eventStore })
|
||||
.subscribe({
|
||||
@@ -617,26 +731,42 @@ export class Nip17Adapter extends ChatProtocolAdapter {
|
||||
console.log(
|
||||
`[NIP-17] Received gift wrap: ${response.id.slice(0, 8)}...`,
|
||||
);
|
||||
|
||||
// Add to gift wraps list
|
||||
const current = this.giftWraps$.value;
|
||||
if (!current.find((g) => g.id === response.id)) {
|
||||
this.giftWraps$.next([...current, response]);
|
||||
}
|
||||
|
||||
// Check if unlocked (cached) or pending
|
||||
if (!isGiftWrapUnlocked(response)) {
|
||||
const pending = new Set(this.pendingGiftWraps$.value);
|
||||
pending.add(response.id);
|
||||
this.pendingGiftWraps$.next(pending);
|
||||
}
|
||||
this.handleGiftWrap(response);
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
console.error("[NIP-17] Subscription error:", err);
|
||||
this.subscriptionActive = false;
|
||||
},
|
||||
complete: () => {
|
||||
console.log("[NIP-17] Subscription completed");
|
||||
this.subscriptionActive = false;
|
||||
},
|
||||
});
|
||||
|
||||
this.subscriptions.set(conversationId, subscription);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a received or sent gift wrap
|
||||
*/
|
||||
private handleGiftWrap(giftWrap: NostrEvent): void {
|
||||
// Add to gift wraps list if not already present
|
||||
const current = this.giftWraps$.value;
|
||||
if (!current.find((g) => g.id === giftWrap.id)) {
|
||||
this.giftWraps$.next([...current, giftWrap]);
|
||||
}
|
||||
|
||||
// Check if unlocked (cached) or pending (skip if already failed)
|
||||
if (!isGiftWrapUnlocked(giftWrap)) {
|
||||
if (!this.failedGiftWraps$.value.has(giftWrap.id)) {
|
||||
const pending = new Set(this.pendingGiftWraps$.value);
|
||||
pending.add(giftWrap.id);
|
||||
this.pendingGiftWraps$.next(pending);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Cache for inbox relays */
|
||||
private inboxRelayCache = new Map<string, string[]>();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user