NIP-04 encryption for sending DMs is now deprecated. All new messages
are sent using NIP-17 (gift-wrapped sealed messages). Reading NIP-04
messages remains supported for backward compatibility.
When a recipient lacks both a DM relay list (kind 10050) and NIP-65
inbox relays, the send button is disabled and a warning is shown
explaining that messages cannot be delivered.
Changes:
- ChatNewMessageState: Remove nip17/requiresNip17 toggles, add
recipientsMissingDmRelays state, always send via NIP-17
- ChatNewMessageViewModel: Always use NIP-17, remove NIP-04 send
paths, add recipient relay status checking
- ToggleNip17Button -> Nip17Indicator: Replace toggle with static
NIP-17 indicator (always on)
- PrivateMessageEditFieldRow: Show warning when recipients lack
DM relay lists, hide message input
- ChatFileSender: sendAll() always uses NIP-17
- Desktop ChatPane: Remove NIP-17 toggle, show relay warning
- ChatroomView: Reactively check recipient DM relay availability
https://claude.ai/code/session_01T7QhUW9cZogk4DxDXbbbJJ
Show a small lock icon next to the timestamp on each DM message:
- NIP-17 (ChatMessageEvent, ChatMessageEncryptedFileHeaderEvent): filled
lock in primary color — relay can't see sender/recipient
- NIP-04 (PrivateDmEvent): open lock in muted gray — legacy encryption,
metadata visible to relays
Matches Android's IncognitoBadge pattern. Both NIP-04 and NIP-17 messages
in the same 1-on-1 conversation produce identical ChatroomKeys, so they
merge into one conversation — the badge is the only way to tell them apart.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The GiftWrap inner event handler only processed ChatMessageEvent (kind 14)
and ReactionEvent. ChatMessageEncryptedFileHeaderEvent (kind 15) was
silently dropped, so encrypted file attachments never appeared in DMs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Right-click on encrypted file attachments in DMs now shows a context menu:
- "Save to disk" — decrypts and saves via native file dialog
- "Forward" — placeholder callback for forwarding to another conversation
Also caches decrypted bytes (not just image bitmap) so save doesn't
re-download. Non-image files also get decrypted on load for save support.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Attach button always visible in DM input (not gated on NIP-17 mode)
- Drag-and-drop always enabled
- When files are attached in NIP-04 mode, auto-switches to NIP-17
with snackbar: "Switched to NIP-17 — file attachments require
encrypted messaging"
- NIP-17 toggle remains for manual control
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
compactMode was hardcoded in the Messages branch of RootContent, which is
shared between deck and single-pane. Now compactMode is a RootContent
parameter: deck passes true, single-pane defaults to false.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The new ByteArray upload overload on DesktopBlossomClient made the
positional any() mock call ambiguous. Specify explicit type parameters.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
In multi-deck mode, Messages column now uses stacked navigation instead
of side-by-side split pane. Full-width contact list OR full-width chat —
clicking a conversation navigates to chat, back arrow returns to list.
Single-pane mode keeps the existing split layout (280dp list + flex chat).
Changes:
- Add compactMode param to DesktopMessagesScreen (default false)
- Extract SplitMessagesContent and CompactMessagesContent composables
- Add onBack callback to ChatPane with back arrow in header
- Remove hardcoded 280dp from ConversationListPane (caller controls width)
- Pass compactMode=true from deck RootContent
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
DeckColumnContainer was creating its own DesktopIAccount (and ChatroomList)
for the Messages column, while Main.kt created a separate one for DM relay
subscriptions. Events were added to one ChatroomList but the UI read from
the other, so DMs never appeared.
Fix: thread the single iAccount from Main.kt through DeckLayout →
DeckColumnContainer → RootContent → DesktopMessagesScreen. Removed the
duplicate DesktopIAccount and DmSendTracker creation from RootContent.
Audited all other deck column types — no similar instance duplication found.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add full send + receive encrypted media support in desktop DM chat:
- Paperclip attach button and drag-and-drop in ChatPane (NIP-17 mode only)
- AES-GCM encryption before upload to Blossom server
- ChatMessageEncryptedFileHeaderEvent (kind 15) wrapped in GiftWrap
- sendNip17EncryptedFile() added to IAccount interface and implementations
- DesktopUploadOrchestrator.uploadEncrypted() with proper encrypted hash
- DesktopBlossomClient ByteArray upload overload for encrypted blobs
- LRU cache in EncryptedMediaService to avoid re-downloading
- Error handling: retry on failure, disable send during upload
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Code reuse
- Extracted remuxTracks() helper to deduplicate video/audio remux loop (~40 lines)
- Extracted stripAfterCompression() to deduplicate identical blocks in upload()/uploadEncrypted()
- Replaced 8 identical showStrippingFailureDialog() copy-pastes with shared SuspendableConfirmation utility
Privacy
- 4 metadata ViewModels now error on strip failure instead of silently uploading unstripped media
Efficiency
- Eliminated temp input file copy for video/audio — MediaExtractor reads URIs directly
Bug fixes
- Fixed Long.toInt() overflow for MP3 files >2GB
- Fixed MP3 temp file leak on exception and null InputStream paths
Cleanup
- Mutex field and wrapping the suspendCancellableCoroutine in mutex.withLock. Concurrent callers now queued
- Converted MetadataStripper from class to object (stateless)
- Moved StrippingFailureState from service layer to UI as generic ConfirmationCallbacks