diff --git a/src/lib/chat/adapters/nip-17-adapter.ts b/src/lib/chat/adapters/nip-17-adapter.ts index 9617303..8a55c29 100644 --- a/src/lib/chat/adapters/nip-17-adapter.ts +++ b/src/lib/chat/adapters/nip-17-adapter.ts @@ -648,88 +648,9 @@ export class Nip17Adapter extends ChatProtocolAdapter { // 5. Execute appropriate action via ActionRunner try { - // Build the rumor (unsigned kind 14 event) for optimistic UI update - const rumorTags = - isReply && parentRumor - ? [ - ["e", parentRumor.id, "", "reply"], - ...participantPubkeys.map((p) => ["p", p] as [string, string]), - ...(actionOpts.emojis?.map((e) => [ - "emoji", - e.shortcode, - e.url, - ]) || []), - ] - : [ - ...participantPubkeys.map((p) => ["p", p] as [string, string]), - ...(actionOpts.emojis?.map((e) => [ - "emoji", - e.shortcode, - e.url, - ]) || []), - ]; - - const rumorCreatedAt = Math.floor(Date.now() / 1000); - - // Calculate rumor ID - const rumorId = await crypto.subtle - .digest( - "SHA-256", - new TextEncoder().encode( - JSON.stringify([ - 0, - activePubkey, - rumorCreatedAt, - PRIVATE_DM_KIND, - rumorTags, - content, - ]), - ), - ) - .then((buf) => - Array.from(new Uint8Array(buf)) - .map((b) => b.toString(16).padStart(2, "0")) - .join(""), - ); - - const rumor: Rumor = { - id: rumorId, - kind: PRIVATE_DM_KIND, - created_at: rumorCreatedAt, - tags: rumorTags, - content, - pubkey: activePubkey, - }; - - // Create synthetic gift wrap for optimistic display - // (will be replaced by real gift wrap when received from relay) - const syntheticGiftWrap: NostrEvent = { - id: `synthetic-${rumorId}`, - kind: 1059, - created_at: rumorCreatedAt, - tags: [["p", activePubkey]], - content: "", - pubkey: activePubkey, - sig: "", - }; - - // Add to decryptedRumors$ for immediate UI update (optimistic) - const currentRumors = giftWrapService.decryptedRumors$.value; - giftWrapService.decryptedRumors$.next([ - ...currentRumors, - { giftWrap: syntheticGiftWrap, rumor }, - ]); - - console.log( - `[NIP-17] 📝 Added rumor ${rumorId.slice(0, 8)} to decryptedRumors$ (optimistic)`, - ); - - // Now send the actual gift wrap if (isReply && parentRumor) { await hub.run(ReplyToWrappedMessage, parentRumor, content, actionOpts); - console.log( - `[NIP-17] ✅ Reply sent successfully (${participantPubkeys.length} participants including self)`, - ); + console.log(`[NIP-17] ✅ Reply sent successfully`); } else { // For self-chat, explicitly send to self. For group chats, filter out self // (applesauce adds sender automatically for group messages) diff --git a/src/services/gift-wrap.ts b/src/services/gift-wrap.ts index 12e40be..cd543cc 100644 --- a/src/services/gift-wrap.ts +++ b/src/services/gift-wrap.ts @@ -564,6 +564,17 @@ class GiftWrapService { return { success, error }; } + /** + * Reload persisted encrypted content IDs from Dexie + * Useful after sending messages to ensure newly persisted content is recognized + */ + async refreshPersistedIds(): Promise { + this.persistedIds = await getStoredEncryptedContentIds(); + console.log( + `[GiftWrap] Refreshed persisted IDs: ${this.persistedIds.size} cached`, + ); + } + /** Auto-decrypt pending gift wraps (called when auto-decrypt is enabled) */ private async autoDecryptPending() { if (!this.signer || !this.settings$.value.autoDecrypt) return; diff --git a/src/services/hub.ts b/src/services/hub.ts index cb88765..351af02 100644 --- a/src/services/hub.ts +++ b/src/services/hub.ts @@ -6,21 +6,35 @@ import { relayListCache } from "./relay-list-cache"; import { getSeenRelays } from "applesauce-core/helpers/relays"; import type { NostrEvent } from "nostr-tools/core"; import accountManager from "./accounts"; +import { encryptedContentStorage } from "./db"; /** - * Publishes a Nostr event to relays using the author's outbox relays - * Falls back to seen relays from the event if no relay list found + * Publishes a Nostr event to relays * * @param event - The signed Nostr event to publish + * @param relayHints - Optional relay hints (used for gift wraps) */ -export async function publishEvent(event: NostrEvent): Promise { - // Try to get author's outbox relays from EventStore (kind 10002) - let relays = await relayListCache.getOutboxRelays(event.pubkey); +export async function publishEvent( + event: NostrEvent, + relayHints?: string[], +): Promise { + let relays: string[]; - // Fallback to relays from the event itself (where it was seen) - if (!relays || relays.length === 0) { - const seenRelays = getSeenRelays(event); - relays = seenRelays ? Array.from(seenRelays) : []; + // If relays explicitly provided (e.g., from gift wrap actions), use them + if (relayHints && relayHints.length > 0) { + relays = relayHints; + console.log( + `[Publish] Using provided relay hints (${relays.length} relays) for event ${event.id.slice(0, 8)}`, + ); + } else { + // Otherwise use author's outbox relays (existing logic) + const outboxRelays = await relayListCache.getOutboxRelays(event.pubkey); + relays = outboxRelays || []; + + if (relays.length === 0) { + const seenRelays = getSeenRelays(event); + relays = seenRelays ? Array.from(seenRelays) : []; + } } // If still no relays, throw error @@ -33,6 +47,23 @@ export async function publishEvent(event: NostrEvent): Promise { // Publish to relay pool await pool.publish(relays, event); + // If this is a gift wrap with decrypted content symbol, persist it to Dexie + // This ensures when we receive it back from relay, it's recognized as unlocked + if (event.kind === 1059) { + const EncryptedContentSymbol = Symbol.for("encrypted-content"); + if (Reflect.has(event, EncryptedContentSymbol)) { + const plaintext = Reflect.get(event, EncryptedContentSymbol); + try { + await encryptedContentStorage.setItem(event.id, plaintext); + console.log( + `[Publish] ✅ Persisted encrypted content for gift wrap ${event.id.slice(0, 8)}`, + ); + } catch (err) { + console.warn(`[Publish] ⚠️ Failed to persist encrypted content:`, err); + } + } + } + // Add to EventStore for immediate local availability eventStore.add(event); }