Add Direct Messages — NIP-04 encrypted DMs (roadmap #13)

Nostr layer:
- fetchDMConversations: fetches all kind-4 events to/from the user
  (both directions in parallel), deduplicates, newest-first
- fetchDMThread: fetches both directions for a specific conversation,
  sorted oldest-first for display
- sendDM: NIP-04 encrypts content via NDK signer, publishes kind 4
- decryptDM: decrypts regardless of direction (ECDH shared secret is
  symmetric — always pass the other party to signer.decrypt)

UI (DMView):
- Two-panel layout: conversation list (w-56) + active thread
- ConvRow: avatar, name, time; shows "🔒 encrypted" preview to avoid
  decrypting the whole inbox on load
- MessageBubble: decrypts lazily on mount; mine right-aligned,
  theirs left-aligned; shows "Could not decrypt" on failure
- ThreadPanel: loads full thread, auto-scrolls to bottom, re-fetches
  after send; Ctrl+Enter to send
- NewConvInput: start a new conversation by pasting an npub1 or hex
  pubkey; validates and resolves before opening thread
- Read-only (npub) accounts see a clear "nsec required" message

Navigation:
- ✉ messages added to sidebar nav
- openDM(pubkey) in UI store → navigates to dm view with pending pubkey
- ProfileView: "✉ message" button in action row opens DM thread

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jure
2026-03-10 20:29:05 +01:00
parent 65d103f252
commit 6464ba2a79
7 changed files with 459 additions and 3 deletions
+7 -1
View File
@@ -212,7 +212,7 @@ function EditProfileForm({ pubkey, onSaved }: { pubkey: string; onSaved: () => v
}
export function ProfileView() {
const { selectedPubkey, goBack } = useUIStore();
const { selectedPubkey, goBack, openDM } = useUIStore();
const { pubkey: ownPubkey, profile: ownProfile, loggedIn, follows, follow, unfollow } = useUserStore();
const pubkey = selectedPubkey!;
const isOwn = pubkey === ownPubkey;
@@ -310,6 +310,12 @@ export function ProfileView() {
>
{isMuted ? "unmute" : "mute"}
</button>
<button
onClick={() => openDM(pubkey)}
className="text-[11px] px-3 py-1 border border-border text-text-muted hover:text-accent hover:border-accent/40 transition-colors"
>
message
</button>
</div>
)}
</header>