mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-06-16 17:48:34 +02:00
feat: improve NIP-17 inbox with relay fallback and better UX
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
This commit is contained in:
@@ -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<string, never>;
|
||||
|
||||
@@ -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) {
|
||||
</div>
|
||||
|
||||
{/* DM Relays Panel */}
|
||||
<div className="border-b bg-muted/20 px-4 py-3">
|
||||
<h3 className="mb-2 text-sm font-semibold">DM Relays (Kind 10050)</h3>
|
||||
{dmRelays.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{dmRelays.map((relay) => (
|
||||
<span
|
||||
key={relay}
|
||||
className="rounded bg-muted px-2 py-1 text-xs font-mono"
|
||||
>
|
||||
{relay}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No DM relays configured (using general relays)
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Collapsible
|
||||
open={relaysOpen}
|
||||
onOpenChange={setRelaysOpen}
|
||||
className="border-b bg-muted/20"
|
||||
>
|
||||
<div className="px-4 py-3">
|
||||
<CollapsibleTrigger className="flex w-full items-center justify-between hover:opacity-70">
|
||||
<div className="flex items-center gap-2">
|
||||
<Radio className="h-4 w-4" />
|
||||
<h3 className="text-sm font-semibold">
|
||||
DM Relays ({dmRelays.length})
|
||||
</h3>
|
||||
</div>
|
||||
<ChevronDown
|
||||
className={`h-4 w-4 transition-transform ${relaysOpen ? "rotate-180" : ""}`}
|
||||
/>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="mt-3 space-y-1.5">
|
||||
{dmRelays.length > 0 ? (
|
||||
dmRelays.map((relay) => (
|
||||
<RelayLink key={relay} url={relay} showInboxOutbox={false} />
|
||||
))
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No DM relays configured. Using general relays from kind 10002 or
|
||||
kind 3.
|
||||
</p>
|
||||
)}
|
||||
</CollapsibleContent>
|
||||
</div>
|
||||
</Collapsible>
|
||||
|
||||
{/* Conversations List */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
@@ -281,10 +306,51 @@ export function InboxViewer(_props: InboxViewerProps) {
|
||||
{conversationsList.length === 0 ? (
|
||||
<div className="py-8 text-center text-muted-foreground">
|
||||
<MessageSquare className="mx-auto mb-2 h-12 w-12 opacity-50" />
|
||||
<p>No conversations yet</p>
|
||||
<p className="text-xs">
|
||||
Start a chat using: <code>chat npub...</code>
|
||||
</p>
|
||||
{!pubkey ? (
|
||||
<>
|
||||
<p>No account active</p>
|
||||
<p className="text-xs mt-1">
|
||||
Login to view your encrypted messages
|
||||
</p>
|
||||
</>
|
||||
) : !syncEnabled ? (
|
||||
<>
|
||||
<p>Gift wrap sync is disabled</p>
|
||||
<p className="text-xs mt-1">
|
||||
Enable sync in settings to receive NIP-17 messages
|
||||
</p>
|
||||
</>
|
||||
) : dmRelays.length === 0 && stats.totalGiftWraps === 0 ? (
|
||||
<>
|
||||
<p>No relays configured</p>
|
||||
<p className="text-xs mt-1">
|
||||
Configure kind 10050 (DM relays) or kind 10002 (general
|
||||
relays)
|
||||
</p>
|
||||
<p className="text-xs mt-1">
|
||||
Try clicking "Load Older" to fetch messages from available
|
||||
relays
|
||||
</p>
|
||||
</>
|
||||
) : stats.totalGiftWraps === 0 ? (
|
||||
<>
|
||||
<p>No gift wraps received yet</p>
|
||||
<p className="text-xs mt-1">
|
||||
Waiting for encrypted messages on {dmRelays.length} relay
|
||||
{dmRelays.length !== 1 ? "s" : ""}
|
||||
</p>
|
||||
<p className="text-xs mt-1">
|
||||
Try "Load Older" to fetch older messages
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p>No conversations yet</p>
|
||||
<p className="text-xs mt-1">
|
||||
Start a chat using: <code>chat npub...</code>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -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<string[]> {
|
||||
// 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 [];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user