From a4f302f0d67d2498617db81c33fddf73fdb3807f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Jan 2026 09:48:30 +0000 Subject: [PATCH] feat: improve NIP-17 inbox with relay fallback and better UX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed Gift Wrap Sync Issues: - Add relay fallback: kind 10050 → kind 10002 → kind 3 - Fix loadOlderGiftWraps to work on first use (no existing gift wraps) - Add comprehensive logging for debugging sync issues Improved UI/UX: - Use RelayLink component for relay display (with icons, hover cards) - Show relays in collapsible dropdown with icon + count - Add contextual error messages: - No account active - Sync disabled - No relays configured - No gift wraps received - Waiting for messages - Better visual hierarchy with Radio icon Technical Details: - getDMRelays now tries kind 10050, then 10002, then 3 - loadOlderGiftWraps uses lastSyncTimestamp or 90 days ago when empty - Collapsible relay panel with ChevronDown animation - RelayLink integration with showInboxOutbox={false} Before: Silent failures when no DM relays configured After: Clear messaging and automatic fallback to general relays --- src/components/InboxViewer.tsx | 114 ++++++++++++++++++++++++++------- src/services/gift-wrap.ts | 64 ++++++++++++++++-- 2 files changed, 147 insertions(+), 31 deletions(-) diff --git a/src/components/InboxViewer.tsx b/src/components/InboxViewer.tsx index 41a6317..1ba351e 100644 --- a/src/components/InboxViewer.tsx +++ b/src/components/InboxViewer.tsx @@ -20,10 +20,22 @@ import { import { useProfile } from "@/hooks/useProfile"; import eventStore from "@/services/event-store"; import { getDisplayName } from "@/lib/nostr-utils"; -import { Copy, Settings, MessageSquare, ChevronDown } from "lucide-react"; +import { + Copy, + Settings, + MessageSquare, + ChevronDown, + Radio, +} from "lucide-react"; import { useCopy } from "@/hooks/useCopy"; import { toast } from "sonner"; import giftWrapManager from "@/services/gift-wrap"; +import { RelayLink } from "@/components/nostr/RelayLink"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; type InboxViewerProps = Record; @@ -37,6 +49,7 @@ export function InboxViewer(_props: InboxViewerProps) { const [showSettings, setShowSettings] = useState(false); const [conversationsPage, setConversationsPage] = useState(1); const [isLoadingOlder, setIsLoadingOlder] = useState(false); + const [relaysOpen, setRelaysOpen] = useState(false); const syncEnabled = state.giftWrapSettings?.syncEnabled ?? true; const autoDecrypt = state.giftWrapSettings?.autoDecrypt ?? true; @@ -249,25 +262,37 @@ export function InboxViewer(_props: InboxViewerProps) { {/* DM Relays Panel */} -
-

DM Relays (Kind 10050)

- {dmRelays.length > 0 ? ( -
- {dmRelays.map((relay) => ( - - {relay} - - ))} -
- ) : ( -

- No DM relays configured (using general relays) -

- )} -
+ +
+ +
+ +

+ DM Relays ({dmRelays.length}) +

+
+ +
+ + {dmRelays.length > 0 ? ( + dmRelays.map((relay) => ( + + )) + ) : ( +

+ No DM relays configured. Using general relays from kind 10002 or + kind 3. +

+ )} +
+
+
{/* Conversations List */}
@@ -281,10 +306,51 @@ export function InboxViewer(_props: InboxViewerProps) { {conversationsList.length === 0 ? (
-

No conversations yet

-

- Start a chat using: chat npub... -

+ {!pubkey ? ( + <> +

No account active

+

+ Login to view your encrypted messages +

+ + ) : !syncEnabled ? ( + <> +

Gift wrap sync is disabled

+

+ Enable sync in settings to receive NIP-17 messages +

+ + ) : dmRelays.length === 0 && stats.totalGiftWraps === 0 ? ( + <> +

No relays configured

+

+ Configure kind 10050 (DM relays) or kind 10002 (general + relays) +

+

+ Try clicking "Load Older" to fetch messages from available + relays +

+ + ) : stats.totalGiftWraps === 0 ? ( + <> +

No gift wraps received yet

+

+ Waiting for encrypted messages on {dmRelays.length} relay + {dmRelays.length !== 1 ? "s" : ""} +

+

+ Try "Load Older" to fetch older messages +

+ + ) : ( + <> +

No conversations yet

+

+ Start a chat using: chat npub... +

+ + )}
) : ( <> diff --git a/src/services/gift-wrap.ts b/src/services/gift-wrap.ts index f3b3700..5dc3457 100644 --- a/src/services/gift-wrap.ts +++ b/src/services/gift-wrap.ts @@ -270,18 +270,29 @@ class GiftWrapManager { const { pubkey } = account; - // Get oldest gift wrap timestamp + // Get oldest gift wrap timestamp, or use lastSyncTimestamp if no gift wraps yet const decryptions = await db.giftWrapDecryptions .where("recipientPubkey") .equals(pubkey) .sortBy("lastAttempt"); + let oldestTimestamp: number; if (decryptions.length === 0) { - console.log("[GiftWrap] No gift wraps to paginate from"); - return 0; + // No gift wraps yet - use lastSyncTimestamp or current time + if (this.lastSyncTimestamp > 0) { + oldestTimestamp = this.lastSyncTimestamp; + console.log("[GiftWrap] No gift wraps yet, using last sync timestamp"); + } else { + // First time - start from 90 days ago + const now = Math.floor(Date.now() / 1000); + oldestTimestamp = + now - GIFT_WRAP_CONFIG.MAX_STORAGE_DAYS * 24 * 60 * 60; + console.log("[GiftWrap] No gift wraps yet, starting from 90 days ago"); + } + } else { + oldestTimestamp = decryptions[0].lastAttempt; } - const oldestTimestamp = decryptions[0].lastAttempt; const cutoff = oldestTimestamp - 30 * 24 * 60 * 60; // 30 days before oldest console.log( @@ -381,10 +392,10 @@ class GiftWrapManager { } /** - * Get DM relays from user's kind 10050 event + * Get DM relays from user's kind 10050 event, with fallback to general relays */ private async getDMRelays(pubkey: string): Promise { - // Try to get kind 10050 from event store + // Try to get kind 10050 (DM relay list) from event store const dmRelayEvent = eventStore.getReplaceable(10050, pubkey, ""); if (dmRelayEvent) { @@ -394,11 +405,50 @@ class GiftWrapManager { .map((t: string[]) => t[1]); if (relays.length > 0) { + console.log( + `[GiftWrap] Using ${relays.length} DM relays from kind 10050`, + ); return relays; } } - // TODO: Fall back to general relay list (kind 10002 or kind 3) + console.log( + "[GiftWrap] No kind 10050 found, falling back to general relays", + ); + + // Fall back to general relay list (kind 10002) + const relayListEvent = eventStore.getReplaceable(10002, pubkey, ""); + if (relayListEvent) { + const relays = relayListEvent.tags + .filter((t: string[]) => t[0] === "r" && t[1]) + .map((t: string[]) => t[1]); + + if (relays.length > 0) { + console.log(`[GiftWrap] Using ${relays.length} relays from kind 10002`); + return relays; + } + } + + console.log("[GiftWrap] No kind 10002 found, falling back to contact list"); + + // Final fallback to contact list relays (kind 3) + const contactsEvent = eventStore.getReplaceable(3, pubkey, ""); + if (contactsEvent) { + try { + const content = JSON.parse(contactsEvent.content); + if (typeof content === "object" && content !== null) { + const relays = Object.keys(content); + if (relays.length > 0) { + console.log(`[GiftWrap] Using ${relays.length} relays from kind 3`); + return relays; + } + } + } catch (error) { + console.warn("[GiftWrap] Failed to parse kind 3 content:", error); + } + } + + console.warn("[GiftWrap] No relays found for user"); return []; }