fix: Fix NIP-17 message sending and display

Two critical fixes for gift-wrapped DMs:

1. Fix publishEvent to accept optional relays parameter
   - SendWrappedMessage was passing inbox relays but they were ignored
   - Now gift wraps are correctly published to inbox relays

2. Make NIP-17 adapter a singleton
   - All components now share the same giftWraps$ state
   - Sent messages appear immediately as the state is shared
   - Export nip17Adapter singleton from the module
   - Update all usages in ChatViewer, InboxViewer, RelaysDropdown, chat-parser
This commit is contained in:
Claude
2026-01-14 13:10:55 +00:00
parent 2a1a479bf5
commit 3cb9459488
6 changed files with 47 additions and 20 deletions

View File

@@ -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":

View File

@@ -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;

View File

@@ -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 };

View File

@@ -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

View File

@@ -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();

View File

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