feat: Enhanced inbox UI with relay status, conversations list, and debugging

This commit adds comprehensive UI improvements and debugging capabilities to the NIP-59 inbox:

InboxViewer enhancements:
- Added "Decrypted" count stat (4th column)
- Added "DM Relays" section showing which relays are being used for gift wraps
- Added "Conversations" list displaying decrypted messages with sender, preview, timestamp, and unread count
- Added "Debug Info" section with loader state (enabled, auto-decrypt, loading, error count, last sync)
- Added proper RxJS subscription to loader state using useEffect

Gift-wrap-loader improvements:
- Added `relays` field to GiftWrapLoaderState to track which relays are in use
- Update state with relay list during sync
- Enhanced console logging to show relay URLs

Gift-wrap debugging:
- Added console.log when creating new conversations to help debug conversation creation

This makes it much easier to:
- See which DM relays are being used
- View decrypted conversations and messages
- Debug issues with message decryption and relay connectivity
- Track loader state and sync status
This commit is contained in:
Claude
2026-01-15 21:53:52 +00:00
parent 7a0149d997
commit f9356e228e
3 changed files with 191 additions and 3 deletions

View File

@@ -1,4 +1,4 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import { useLiveQuery } from "dexie-react-hooks";
import {
Mail,
@@ -8,15 +8,23 @@ import {
Loader2,
CheckCircle,
XCircle,
MessageSquare,
Radio,
Database,
} from "lucide-react";
import { useGrimoire } from "@/core/state";
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
import { Button } from "./ui/button";
import { Switch } from "./ui/switch";
import giftWrapLoader from "@/services/gift-wrap-loader";
import { getConversations } from "@/services/gift-wrap";
import db from "@/services/db";
import { toast } from "sonner";
import { use$ } from "applesauce-react/hooks";
import accounts from "@/services/accounts";
import { useProfile } from "@/hooks/useProfile";
import { getDisplayName } from "@/lib/nostr-utils";
import { formatDistanceToNow } from "date-fns";
export function InboxViewer() {
const { state, setPrivateMessagesEnabled, setAutoDecryptGiftWraps } =
@@ -51,6 +59,30 @@ export function InboxViewer() {
return giftWrapLoader.getUnreadCount(activeAccount.pubkey);
}, [activeAccount?.pubkey]);
// Get conversations
const conversations = useLiveQuery(async () => {
if (!activeAccount?.pubkey) return [];
return getConversations(activeAccount.pubkey);
}, [activeAccount?.pubkey]);
// Get decrypted messages count
const decryptedCount = useLiveQuery(async () => {
if (!activeAccount?.pubkey) return 0;
return db.decryptedRumors
.where("recipientPubkey")
.equals(activeAccount.pubkey)
.count();
}, [activeAccount?.pubkey]);
// Get loader state for relay info
const [loaderState, setLoaderState] = useState<any>(null);
// Subscribe to loader state
useEffect(() => {
const subscription = giftWrapLoader.state.subscribe(setLoaderState);
return () => subscription.unsubscribe();
}, []);
const handleTogglePrivateMessages = (enabled: boolean) => {
setPrivateMessagesEnabled(enabled);
@@ -124,7 +156,7 @@ export function InboxViewer() {
</div>
{/* Stats */}
<div className="grid grid-cols-3 gap-4">
<div className="grid grid-cols-4 gap-4">
<Card>
<CardHeader className="p-4">
<CardTitle className="text-sm font-medium text-muted-foreground">
@@ -136,6 +168,17 @@ export function InboxViewer() {
</CardContent>
</Card>
<Card>
<CardHeader className="p-4">
<CardTitle className="text-sm font-medium text-muted-foreground">
Decrypted
</CardTitle>
</CardHeader>
<CardContent className="p-4 pt-0">
<div className="text-2xl font-bold">{decryptedCount ?? 0}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="p-4">
<CardTitle className="text-sm font-medium text-muted-foreground">
@@ -295,6 +338,104 @@ export function InboxViewer() {
</Card>
)}
{/* Relay Status */}
{privateMessagesEnabled &&
loaderState?.relays &&
loaderState.relays.length > 0 && (
<Card>
<CardHeader className="p-4">
<div className="flex items-center gap-2">
<Radio className="h-4 w-4" />
<CardTitle className="text-base">DM Relays</CardTitle>
</div>
</CardHeader>
<CardContent className="p-4 pt-0">
<div className="space-y-1">
{loaderState.relays.map((relay: string) => (
<div
key={relay}
className="text-sm font-mono text-muted-foreground flex items-center gap-2"
>
<div className="h-2 w-2 rounded-full bg-green-500" />
{relay}
</div>
))}
</div>
</CardContent>
</Card>
)}
{/* Debug Info */}
{privateMessagesEnabled && (
<Card>
<CardHeader className="p-4">
<div className="flex items-center gap-2">
<Database className="h-4 w-4" />
<CardTitle className="text-base">Debug Info</CardTitle>
</div>
</CardHeader>
<CardContent className="p-4 pt-0 space-y-2">
<div className="text-sm space-y-1">
<div className="flex justify-between">
<span className="text-muted-foreground">Loader Enabled:</span>
<span className="font-mono">
{loaderState?.enabled ? "Yes" : "No"}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Auto-Decrypt:</span>
<span className="font-mono">
{loaderState?.autoDecrypt ? "Yes" : "No"}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Loading:</span>
<span className="font-mono">
{loaderState?.loading ? "Yes" : "No"}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Error Count:</span>
<span className="font-mono">
{loaderState?.errorCount ?? 0}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Last Sync:</span>
<span className="font-mono text-xs">
{loaderState?.lastSync
? formatDistanceToNow(loaderState.lastSync, {
addSuffix: true,
})
: "Never"}
</span>
</div>
</div>
</CardContent>
</Card>
)}
{/* Conversations List */}
{privateMessagesEnabled &&
conversations &&
conversations.length > 0 && (
<Card>
<CardHeader className="p-4">
<div className="flex items-center gap-2">
<MessageSquare className="h-4 w-4" />
<CardTitle className="text-base">Conversations</CardTitle>
</div>
</CardHeader>
<CardContent className="p-4 pt-0">
<div className="space-y-2">
{conversations.map((conv) => (
<ConversationItem key={conv.id} conversation={conv} />
))}
</div>
</CardContent>
</Card>
)}
{/* Help Text */}
{!privateMessagesEnabled && (
<div className="text-sm text-muted-foreground space-y-2">
@@ -316,3 +457,37 @@ export function InboxViewer() {
</div>
);
}
function ConversationItem({ conversation }: { conversation: any }) {
const profile = useProfile(conversation.senderPubkey);
const displayName = getDisplayName(conversation.senderPubkey, profile);
return (
<div className="flex items-start gap-3 p-3 rounded-lg border hover:bg-accent cursor-pointer">
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-1">
<div className="font-medium truncate">{displayName}</div>
<div className="text-xs text-muted-foreground">
{formatDistanceToNow(conversation.lastMessageCreatedAt * 1000, {
addSuffix: true,
})}
</div>
</div>
<div className="text-sm text-muted-foreground truncate">
{conversation.lastMessagePreview}
</div>
<div className="flex items-center gap-2 mt-1">
<span className="text-xs text-muted-foreground">
{conversation.messageCount} message
{conversation.messageCount === 1 ? "" : "s"}
</span>
{conversation.unreadCount > 0 && (
<span className="text-xs px-1.5 py-0.5 rounded-full bg-primary text-primary-foreground font-medium">
{conversation.unreadCount} new
</span>
)}
</div>
</div>
</div>
);
}

View File

@@ -35,6 +35,7 @@ interface GiftWrapLoaderState {
recipientPubkey?: string;
lastSync?: number;
errorCount: number;
relays: string[]; // Relays being used for gift wraps
}
/**
@@ -47,6 +48,7 @@ class GiftWrapLoader {
autoDecrypt: false,
loading: false,
errorCount: 0,
relays: [],
});
private subscription?: { unsubscribe: () => void };
@@ -151,9 +153,16 @@ class GiftWrapLoader {
}
console.log(
`[GiftWrapLoader] Syncing from ${inboxRelays.length} inbox relays`,
`[GiftWrapLoader] Syncing from ${inboxRelays.length} inbox relays:`,
inboxRelays,
);
// Update state with relays being used
this.state$.next({
...this.state$.value,
relays: inboxRelays,
});
// Subscribe to kind 1059 events for this user using timeline loader
const filter = {
kinds: [1059 as number],

View File

@@ -322,6 +322,10 @@ async function updateConversationMetadata(
updatedAt: Date.now(),
};
await db.conversations.put(conversation);
console.log(
`[GiftWrap] Created new conversation ${conversationId}`,
conversation,
);
} else {
// Update existing conversation if this is newer
if (rumor.rumorCreatedAt > existing.lastMessageCreatedAt) {