diff --git a/src/components/ChatViewer.tsx b/src/components/ChatViewer.tsx index 187ef50..81a936d 100644 --- a/src/components/ChatViewer.tsx +++ b/src/components/ChatViewer.tsx @@ -23,7 +23,7 @@ import type { // import { NipC7Adapter } from "@/lib/chat/adapters/nip-c7-adapter"; // Coming soon import { Nip29Adapter } from "@/lib/chat/adapters/nip-29-adapter"; import { Nip53Adapter } from "@/lib/chat/adapters/nip-53-adapter"; -import { Nip17Adapter } from "@/lib/chat/adapters/nip-17-adapter"; +import { nip17Adapter } from "@/lib/chat/adapters/nip-17-adapter"; import type { ChatProtocolAdapter } from "@/lib/chat/adapters/base-adapter"; import type { Message } from "@/types/chat"; import type { ChatAction } from "@/types/chat-actions"; @@ -960,7 +960,7 @@ function getAdapter(protocol: ChatProtocol): ChatProtocolAdapter { case "nip-29": return new Nip29Adapter(); case "nip-17": - return new Nip17Adapter(); + return nip17Adapter; // case "nip-28": // Phase 3 - Public channels (coming soon) // return new Nip28Adapter(); case "nip-53": diff --git a/src/components/InboxViewer.tsx b/src/components/InboxViewer.tsx index 96f4b6a..c7b82d4 100644 --- a/src/components/InboxViewer.tsx +++ b/src/components/InboxViewer.tsx @@ -31,7 +31,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar"; import { Button } from "@/components/ui/button"; import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet"; import * as VisuallyHidden from "@radix-ui/react-visually-hidden"; -import { Nip17Adapter } from "@/lib/chat/adapters/nip-17-adapter"; +import { nip17Adapter } from "@/lib/chat/adapters/nip-17-adapter"; import { useProfile } from "@/hooks/useProfile"; import { getDisplayName } from "@/lib/nostr-utils"; @@ -258,8 +258,8 @@ export function InboxViewer() { const [isResizing, setIsResizing] = useState(false); const [isDecrypting, setIsDecrypting] = useState(false); - // NIP-17 adapter instance - const adapter = useMemo(() => new Nip17Adapter(), []); + // NIP-17 adapter singleton instance + const adapter = nip17Adapter; // Get pending count const pendingCount = use$(() => adapter.getPendingCount$(), [adapter]) ?? 0; diff --git a/src/components/chat/RelaysDropdown.tsx b/src/components/chat/RelaysDropdown.tsx index 7803508..6261266 100644 --- a/src/components/chat/RelaysDropdown.tsx +++ b/src/components/chat/RelaysDropdown.tsx @@ -11,7 +11,7 @@ import { useRelayState } from "@/hooks/useRelayState"; import { getConnectionIcon, getAuthIcon } from "@/lib/relay-status-utils"; import { normalizeRelayURL } from "@/lib/relay-url"; import type { Conversation } from "@/types/chat"; -import { Nip17Adapter } from "@/lib/chat/adapters/nip-17-adapter"; +import { nip17Adapter } from "@/lib/chat/adapters/nip-17-adapter"; interface RelaysDropdownProps { conversation: Conversation; @@ -38,7 +38,6 @@ export function RelaysDropdown({ conversation }: RelaysDropdownProps) { useEffect(() => { if (conversation.protocol !== "nip-17") return; - const adapter = new Nip17Adapter(); const participants = conversation.participants; // Initialize with loading state @@ -55,7 +54,7 @@ export function RelaysDropdown({ conversation }: RelaysDropdownProps) { const results = await Promise.all( participants.map(async (p) => { try { - const relays = await adapter.getInboxRelays(p.pubkey); + const relays = await nip17Adapter.getInboxRelays(p.pubkey); return { pubkey: p.pubkey, relays, loading: false }; } catch { return { pubkey: p.pubkey, relays: [], loading: false }; diff --git a/src/lib/chat-parser.ts b/src/lib/chat-parser.ts index 1e32570..d374eb0 100644 --- a/src/lib/chat-parser.ts +++ b/src/lib/chat-parser.ts @@ -1,6 +1,6 @@ import type { ChatCommandResult, GroupListIdentifier } from "@/types/chat"; // import { NipC7Adapter } from "./chat/adapters/nip-c7-adapter"; -import { Nip17Adapter } from "./chat/adapters/nip-17-adapter"; +import { nip17Adapter } from "./chat/adapters/nip-17-adapter"; import { Nip29Adapter } from "./chat/adapters/nip-29-adapter"; import { Nip53Adapter } from "./chat/adapters/nip-53-adapter"; import { nip19 } from "nostr-tools"; @@ -61,8 +61,9 @@ export function parseChatCommand(args: string[]): ChatCommandResult { } // Try each adapter in priority order + // NIP-17 uses singleton to share gift wrap state across app const adapters = [ - new Nip17Adapter(), // NIP-17 - Private DMs (gift wrapped) + nip17Adapter, // NIP-17 - Private DMs (gift wrapped) - singleton // new Nip28Adapter(), // NIP-28 - Public channels (coming soon) new Nip29Adapter(), // NIP-29 - Relay groups new Nip53Adapter(), // NIP-53 - Live activity chat diff --git a/src/lib/chat/adapters/nip-17-adapter.ts b/src/lib/chat/adapters/nip-17-adapter.ts index 187350d..c936d51 100644 --- a/src/lib/chat/adapters/nip-17-adapter.ts +++ b/src/lib/chat/adapters/nip-17-adapter.ts @@ -781,4 +781,21 @@ export class Nip17Adapter extends ChatProtocolAdapter { defaultValue: undefined, }); } + + /** + * Add a gift wrap event directly to local state + * Used for optimistic updates after sending + */ + addGiftWrapLocally(giftWrap: NostrEvent): void { + const current = this.giftWraps$.value; + if (!current.find((g) => g.id === giftWrap.id)) { + this.giftWraps$.next([...current, giftWrap]); + } + } } + +/** + * Singleton instance for shared state across the app + * All components should use this to ensure gift wraps are shared + */ +export const nip17Adapter = new Nip17Adapter(); diff --git a/src/services/hub.ts b/src/services/hub.ts index cb88765..3c643e3 100644 --- a/src/services/hub.ts +++ b/src/services/hub.ts @@ -8,30 +8,40 @@ import type { NostrEvent } from "nostr-tools/core"; import accountManager from "./accounts"; /** - * Publishes a Nostr event to relays using the author's outbox relays + * Publishes a Nostr event to relays + * If relays are provided, uses those; otherwise uses author's outbox relays * Falls back to seen relays from the event if no relay list found * * @param event - The signed Nostr event to publish + * @param relays - Optional specific relays to publish to (used for gift wraps to inbox relays) */ -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, + relays?: string[], +): Promise { + let targetRelays = relays; - // 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 no specific relays provided, use author's outbox relays + if (!targetRelays || targetRelays.length === 0) { + targetRelays = + (await relayListCache.getOutboxRelays(event.pubkey)) ?? undefined; + + // Fallback to relays from the event itself (where it was seen) + if (!targetRelays || targetRelays.length === 0) { + const seenRelays = getSeenRelays(event); + targetRelays = seenRelays ? Array.from(seenRelays) : []; + } } // If still no relays, throw error - if (relays.length === 0) { + if (targetRelays.length === 0) { throw new Error( "No relays found for publishing. Please configure relay list (kind 10002) or ensure event has relay hints.", ); } // Publish to relay pool - await pool.publish(relays, event); + await pool.publish(targetRelays, event); // Add to EventStore for immediate local availability eventStore.add(event);