import { useEffect, useRef, useState } from "react"; import { NDKEvent, nip19 } from "@nostr-dev-kit/ndk"; import { useUserStore } from "../../stores/user"; import { useUIStore } from "../../stores/ui"; import { fetchDMConversations, fetchDMThread, sendDM, decryptDM, getNDK } from "../../lib/nostr"; import { useProfile } from "../../hooks/useProfile"; import { timeAgo, shortenPubkey } from "../../lib/utils"; // ── Helpers ────────────────────────────────────────────────────────────────── function partnerOf(event: NDKEvent, myPubkey: string): string { return event.pubkey === myPubkey ? (event.tags.find((t) => t[0] === "p")?.[1] ?? "") : event.pubkey; } function groupConversations(events: NDKEvent[], myPubkey: string): Map { const map = new Map(); for (const e of events) { const partner = partnerOf(e, myPubkey); if (!partner) continue; const list = map.get(partner) ?? []; list.push(e); map.set(partner, list); } // Sort each conversation newest-first for (const [k, list] of map) { map.set(k, list.sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0))); } return map; } // ── Conversation list row ───────────────────────────────────────────────────── function ConvRow({ partnerPubkey, lastEvent, selected, onSelect, }: { partnerPubkey: string; lastEvent: NDKEvent; selected: boolean; onSelect: () => void; }) { const profile = useProfile(partnerPubkey); const name = profile?.displayName || profile?.name || shortenPubkey(partnerPubkey); const time = lastEvent.created_at ? timeAgo(lastEvent.created_at) : ""; return ( ); } // ── Message bubble ──────────────────────────────────────────────────────────── function MessageBubble({ event, myPubkey }: { event: NDKEvent; myPubkey: string }) { const isMine = event.pubkey === myPubkey; const [text, setText] = useState(null); const [error, setError] = useState(false); useEffect(() => { decryptDM(event, myPubkey) .then(setText) .catch(() => setError(true)); }, [event.id]); const time = event.created_at ? timeAgo(event.created_at) : ""; return (
{error ? ( Could not decrypt ) : text === null ? ( ) : ( text )}
{time}
); } // ── Thread panel ────────────────────────────────────────────────────────────── function ThreadPanel({ partnerPubkey, myPubkey, }: { partnerPubkey: string; myPubkey: string; }) { const { openProfile } = useUIStore(); const profile = useProfile(partnerPubkey); const name = profile?.displayName || profile?.name || shortenPubkey(partnerPubkey); const [messages, setMessages] = useState([]); const [loading, setLoading] = useState(true); const [text, setText] = useState(""); const [sending, setSending] = useState(false); const [sendError, setSendError] = useState(null); const bottomRef = useRef(null); const textareaRef = useRef(null); useEffect(() => { setLoading(true); setMessages([]); fetchDMThread(myPubkey, partnerPubkey) .then(setMessages) .finally(() => setLoading(false)); }, [partnerPubkey, myPubkey]); useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); const handleSend = async () => { const content = text.trim(); if (!content || sending) return; setSending(true); setSendError(null); try { await sendDM(partnerPubkey, content); setText(""); // Re-fetch thread to include the sent message const updated = await fetchDMThread(myPubkey, partnerPubkey); setMessages(updated); textareaRef.current?.focus(); } catch (err) { setSendError(String(err)); } finally { setSending(false); } }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) handleSend(); }; return (
{/* Header */}
{profile?.picture && ( )}
{/* Messages */}
{loading && (
Loading messages…
)} {!loading && messages.length === 0 && (
No messages yet. Say hello!
)} {messages.map((e) => ( ))}
{/* Compose */}
{sendError &&

{sendError}

}