Commit Graph

24 Commits

Author SHA1 Message Date
Claude 8b5af5d496 docs(audio-rooms): refresh against shipped state + nostrnests gap audit
Existing plan docs were written before the moq-lite swap, the
create-space + kind-10112 work, and the harness / submodule findings.
This refresh aligns them with what's actually live on the branch and
captures the work still ahead.

  - 2026-04-26-audio-rooms-completion.md — flipped to a STATUS-FIRST
    layout: implementation table for every protocol/transport/UI
    surface, "pending" table for the remaining items (reconnect,
    level meters, Desktop / iOS, Nests parity), pointers section
    refreshed.
  - 2026-04-26-moq-lite-gap.md — marked DONE with the commit range
    that landed it (fb47a4c71cf99d015b0d7); "When picking up"
    section now points at the shipped surface first, raw protocol
    references second.
  - 2026-04-22-nip-audio-rooms-draft.md — major surgery to match
    today's nostrnests reality:
      * status banner up top calling out the revision
      * dependencies dropped IETF MoQ-transport, added moq-lite
        Lite-03 + ALPN "moq-lite-03"
      * HTTP control plane: GET <service>/<room-d-tag> → POST /auth
        with {namespace, publish}, returning {token}; documented the
        JWT claim shape (root, get, put), 600 s lifetime, regex
        on `namespace`, JWKS endpoint, error matrix
      * Audio transport: replaced IETF SETUP / TrackNamespace tuples
        / OBJECT_DATAGRAM with moq-lite Lite-03 (ControlType varint,
        per-bidi message types, group uni streams, audio/data track,
        no in-band SETUP, FIN-as-unsubscribe semantics)
      * New event-kind sections: kind 4312 (admin command / kick),
        kind 10112 (audio-room server list)
      * Reconciliation section explaining what changed from the
        original draft and why
  - NEW: 2026-04-26-nostrnests-integration-audit.md — punchlist of
    every nostrnests/NestsUI feature we don't yet ship, sourced from
    a code-walk of the React app + moq-auth + API.md (which is
    LiveKit-era and dead). Tier 1 (low-effort, visible): chat,
    reactions, role parsing + promotion, hand-raise queue, kick
    (kind 4312), edit/close room, scheduled rooms, listener counter.
    Tier 2: participant grid, augmented presence tags
    (publishing/onstage), per-avatar context menu + zap, share via
    naddr. Tier 3: room theming. Tier 4: token-refresh +
    Connection.Reload sanity checks.

Verified `:nestsClient:jvmTest` + `:amethyst:compilePlayDebugKotlin`
both still green after the doc changes (no code touched).
2026-04-26 19:23:41 +00:00
Vitor Pamplona 6d8b75c7a2 Merge pull request #2519 from nrobi144/feat/relay-power-tools
feat(desktop): Relay Power Tools — Dashboard, Config Editors, Subscription Wiring
2026-04-23 08:41:22 -04:00
nrobi144 835947d611 feat(desktop): relay config persistence, correct counts, per-screen picker
- Persist relay list events (kinds 10050/10007/10006) as JSON to
  java.util.prefs.Preferences with per-account key isolation
- Load persisted relay configs on startup before bootstrap subscription
- Validate loaded events (kind + pubkey check), 8KB guard on writes
- Fix SearchScreen relay count: "0 of 1" not "0 of 7" — uses searchRelays
- Fix FeedScreen relay count: shows feed relay count, not all connected
- Per-screen relay picker dialogs: Dns icon on Feed and Search screens
  opens AlertDialog wrapping existing editors (Nip65RelayEditor,
  SearchRelayEditor) — no new composable files
- Fix created_at dedup: use >= for replaceable event semantics
- Fix setters: use TimeUtils.now() not Long.MAX_VALUE
- Add consumePublishedEvent() for local immediate update after publish
- Remove stale "not loaded" warnings from Search/Blocked editors
- Fix FeedHeader type: Set<NormalizedRelayUrl> not Set<Any>
- Fix picker LaunchedEffect(Unit) to not overwrite user edits

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 12:00:03 +03:00
Claude ac1e751bec docs: draft NIP-XX — Interactive Audio Rooms Join Protocol
Captures the gap between NIP-53's room discovery and what audio-capable
clients/servers must actually agree on to interop. NIP-53 defines the
30312 event but leaves the HTTP control plane + MoQ namespacing +
audio codec params entirely to individual implementations. With Nests
going generic-server, this is the moment to standardize.

What the draft covers:

1. HTTP control plane:
   - Path convention: GET <service>/<d-tag>
   - NIP-98 `Authorization: Nostr <base64>` header required
   - JSON response shape (endpoint, token, transport, codec,
     sample_rate, frame_duration_ms, moq_version)
   - Canonical error-status map (401/403/404/410/503)
2. WebTransport + MoQ handshake requirements (Extended CONNECT,
   required HTTP/3 settings, Bearer token passing).
3. MoQ track naming — vendor-neutral one-element namespace
   `[<d-tag>]` with track-name = speaker-pubkey-hex. Explicit
   rejection of the `["nests", <d-tag>]` prefix for new deployments.
4. Audio object format: raw Opus packets (no Ogg, no TOC), 48 kHz
   mono, 20 ms default, mono PCM 16-bit decode target. Both
   OBJECT_DATAGRAM and STREAM_HEADER_SUBGROUP accepted; listeners
   MUST handle both.
5. Per-track access control: server MUST verify publishing pubkey is
   a host/speaker in the current 30312 event (which is replaceable —
   revocation cascade is spec'd).
6. Leave procedure (UNSUBSCRIBE, UNANNOUNCE, final 10312 presence,
   WT_CLOSE_SESSION capsule).
7. Presence extension: the `["muted", "1"|"0"]` tag we already ship
   in nestsClient's MeetingRoomPresenceEvent overload, promoted from
   Amethyst-specific to NIP-defined.
8. Server + client requirements summaries.
9. Known divergences from current nostrnests/nests servers (two-
   element `["nests", <d-tag>]` namespace + `/api/v1/nests/<d-tag>`
   path) with a transition strategy via a `"nip_xx": true` flag in
   room-info responses.
10. Security considerations (bearer-token handling, NIP-98 `u` tag
    binding, audio-replay attack surface, server-impersonation VU
    meter recommendation).

Deliberately out of scope: E2E-signed audio objects (future NIP),
federation between audio servers, room recording/transcription.

Intended workflow: share with the Nests team while they're designing
the generic server, land changes against their feedback, then open a
PR on nostr-protocol/nips.

https://claude.ai/code/session_013nVLALALKaHVgHm9u5Cg8D
2026-04-22 13:27:18 +00:00
Claude 2b44e9b06b docs(quic-plan): lock in new top-level :quic module (Option A)
Updates the Architecture section to reflect the decision to house the
pure-Kotlin QUIC + HTTP/3 + WebTransport code in a new top-level
Gradle module `:quic`, sibling to `:quartz`, `:commons`, `:nestsClient`.

Key changes:

- Module placement: new KMP module `:quic` with commonMain +
  jvmAndroid + jvmMain + androidMain source sets (mirrors Quartz).
  `:quic` takes `api(project(":quartz"))` so all crypto primitives
  are in-scope without re-export.
- settings.gradle delta spelled out.
- Package layout shifts from
  `nestsClient/src/jvmAndroid/...transport/quic/` to
  `:quic/src/commonMain/com/vitorpamplona/quic/`, with UDP
  socket-specific bits in `jvmAndroid/`.
- Varint.kt migration called out: moves from
  `nestsClient/moq/Varint.kt` to `:quic` at
  `com.vitorpamplona.quic.Varint`. Mechanical, one commit.
- Rename KwikWebTransportFactory stub → QuicWebTransportFactory
  (real), living in `:quic` but implementing `:nestsClient`'s
  existing `WebTransportFactory` interface. AudioRoomConnectionViewModel
  changes one ctor call.
- Rationale section documenting why Option A beats putting it in
  `:nestsClient` (single responsibility / reusability / security
  boundary / test isolation / build graph / charter fit).
- Phase A now explicitly includes the module-creation +
  Varint-migration step; test suite must stay green across the move.

Timeline unchanged (17-19 weeks). Dependency story unchanged
(no external libs; everything piggybacks on Quartz).

https://claude.ai/code/session_013nVLALALKaHVgHm9u5Cg8D
2026-04-22 13:20:33 +00:00
Claude 1ff951dbb4 docs(quic-plan): drop BouncyCastle — Quartz already has every primitive
Audited Quartz's crypto surface and found every primitive the QUIC
plan called out for BouncyCastle is already present in commonMain:

- `utils/ciphers/AESGCM.kt` — AES-GCM with AAD (QUIC-TLS AEAD)
- `nip44Encryption/crypto/ChaCha20Poly1305.kt` — pure-Kotlin AEAD
- `nip44Encryption/crypto/Hkdf.kt` + `utils/mac/MacInstance.kt` — HKDF
- `utils/sha256/Sha256.kt` — SHA-256
- `marmot/mls/crypto/X25519.kt` — X25519 ECDH (TLS 1.3 key exchange)
- `marmot/mls/crypto/Ed25519.kt` — Ed25519 signatures
- `utils/SecureRandom.kt`

Plan update highlights:
- "What we delegate" section rewritten to point at Quartz primitives.
- Phase B renamed from "TLS via BouncyCastle" to "TLS 1.3 client state
  machine on Quartz primitives"; bumped from 2 to 3 weeks because we
  write the state machine ourselves, but eliminates the entire
  BC-adapter integration risk.
- "Dependencies to add" section zeroed out — nothing goes in
  gradle/libs.versions.toml. The only new primitive is a thin
  HKDF-Expand-Label helper on top of existing `MacInstance`, which
  we can upstream to Quartz's `Hkdf` as a general `expand(prk,
  info, length)`.
- Risk table rewritten: removed BC-specific risks, added
  Quartz-specific ones (verify `X25519.dh` against RFC 7748 vector
  on Phase B day-1).
- Cert chain signature verification uses JDK `Signature` for RSA +
  ECDSA plus Quartz `Ed25519` for Ed25519 leaves.

Total timeline moves from 16-18 weeks to 17-19 weeks — same band,
but with zero external dependencies and zero Maven-resolution risk.

https://claude.ai/code/session_013nVLALALKaHVgHm9u5Cg8D
2026-04-22 12:25:58 +00:00
Claude 40308dc917 docs: pure-Kotlin QUIC + WebTransport implementation plan
Captures the plan to unblock Phase 3b-2 (the WebTransport handshake)
by writing a Kotlin QUIC client rather than depending on a Java QUIC
library. Triggered by exhausting the off-the-shelf options:

- tech.kwik:* coords don't exist on Maven Central we can resolve.
- Netty incubator HTTP/3 needs an Android quiche-native that isn't
  published.
- Cronet doesn't expose WebTransport.
- WebView JS bridge rejected by product.

The plan delegates TLS 1.3 + crypto primitives to BouncyCastle (bcprov
+ bcpkix already cached, bctls to add) and has us writing the QUIC
packet/frame/state-machine layer + HTTP/3 + WebTransport on top.

Realistic estimate: 16-18 weeks one developer full-time, or 5-6
months at a normal cadence, plus a security review before shipping.

Hard abandonment trigger documented: if the RFC 9001 Appendix A
Initial-packet test vectors don't bit-match by end of Phase D
(~6 weeks in), abandon and wait for an Android-compatible upstream.

The plan is committed as a doc rather than executed in this session
because each phase is multi-week and would block the rest of the
audio-rooms feature in the meantime.

https://claude.ai/code/session_013nVLALALKaHVgHm9u5Cg8D
2026-04-22 08:35:09 +00:00
nrobi144 66962d360d feat(desktop): relay power tools — dashboard, config editors, subscription wiring
- Relay Dashboard as new DeckColumnType.Relays with Monitor + Configure tabs
- Monitor tab: live 1Hz session metrics, NIP-11 detail panels, reconnect
- Configure tab: collapsible editors for Connected, NIP-65 (R/W/Both toggles),
  DM (kind 10050), Search (kind 10007), Blocked (kind 10006)
- Compose Relay Picker: expandable relay selection in note compose dialog
- Nip11Fetcher: fail-closed HTTP client, 256KB limit, Mutex dedup
- RelayMetrics: separate StateFlow (1Hz) to avoid relayStatuses churn
- Bootstrap subscription: fetches user's relay config on login (kinds 10002/10050/10007/10006)
- DesktopRelayCategories aggregator: feedRelays, searchRelays, notificationRelays, dmRelays
  with fallback logic, blocked subtraction, 1s debounce
- LocalRelayCategories CompositionLocal (matches LocalTorState pattern)
- FeedScreen uses NIP-65 outbox relays, SearchScreen uses search relays
- "X relays connected" on feed is clickable → opens Relay Dashboard
- URL validation: requires domain with dot, blocks ws:// unless .onion
- Auto-add pending input on Save, empty list protection
- Thread-safe created_at dedup (AtomicLong) in DesktopAccountRelays
- DisposableEffect cleanup for bootstrap subscription

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 10:15:44 +03:00
nrobi144 01fc5f3ac6 fix(tor): remove broken runtime reconnect, add evictConnections
Remove the LaunchedEffect/scope.launch reconnect code that didn't
work (relay subscriptions lost after disconnect+connect cycle).
Tor toggle will use app rebuild via key() instead (next commit).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 07:25:45 +03:00
nrobi144 11d0a657ac fix(tor): close 8 network traffic leaks — fail-closed when Tor expected
SECURITY: With Tor ON, all HTTP traffic now routes through SOCKS proxy.
Previously only relay WebSocket connections used Tor; 8 other egress
paths created bare OkHttpClient() instances bypassing Tor entirely.

Fail-closed behavior:
- When Tor expected but bootstrapping → dead SOCKS proxy on port 1
  (requests fail instead of leaking IP)
- lateinit var crashes on misconfiguration (loud failure vs silent leak)

Leak sites fixed (all use DesktopHttpClient.currentClient()):
- AnimatedGifImage: GIF fetch
- SaveMediaAction: media downloads
- EncryptedMediaService: NIP-17 DM media
- ServerHealthCheck: Blossom server probes
- NoteActions: zap/lightning LNURL resolution
- DesktopBlossomClient: media uploads
- AccountManager: NIP-46 bunker relay connections
- Coil image loader: TODO (OkHttpNetworkFetcher import issue)

Also:
- Relay reconnect on Tor Active (prevents stale clearnet connections)
- isTorExpected() for fail-closed logic
- Tests updated for new torTypeProvider parameter

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 07:25:45 +03:00
nrobi144 61119e6916 docs: deepen cache plan — BoundedLargeCache, FeedNoteCard rewrite details
- Resolve LruCache vs LargeCache: use LargeCache (lock-free ConcurrentSkipListMap)
  with BoundedLargeCache wrapper for size enforcement on put()
- Confirm LargeCache available on desktop via quartz jvmAndroid source set
- Detail FeedNoteCard rewrite: Note model field mapping, 5 subscription removals
- Fix filter examples to use filterIntoSet (LargeCache API)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:12:46 +02:00
nrobi144 9c160006fa docs: desktop cache architecture brainstorm and plan
Cache-centric architecture for navigation persistence, mirroring
Android Amethyst's pattern. Three-phase incremental migration:
Phase 1 - Store events in DesktopLocalCache with LRU eviction
Phase 2 - Create FeedFilter implementations
Phase 3 - Migrate screens to FeedViewModel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:12:45 +02:00
nrobi144 efa294ea72 feat(highlights): add article highlights and note-taking system
Phase 1: HighlightData model + DesktopHighlightStore (Preferences-based)
Phase 2: Text selection highlight via Cmd+H/Cmd+Shift+H, inline bold/italic
         rendering, HighlightAnnotationDialog, SelectionContainer wrapping
Phase 3: MyHighlightsScreen with grouped view, HighlightPublishAction for
         NIP-84 (kind 9802), deck/sidebar/menu wiring

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 12:16:54 +02:00
nrobi144 328fcf86d7 refactor(reads): address all review findings — security, performance, simplicity
P1 Critical:
- Add 300ms debounce on editor preview to prevent AST re-parse per keystroke
- Hoist Regex constants in ReadingTimeCalculator and TableOfContents
- Add URI scheme allowlist to editor onLinkClick (was missing vs reader)
- Add MAX_CONTENT_BYTES (100KB) validation before publish

P2 Important:
- Delete MarkdownSpikeScreen (278 LOC spike artifact)
- Delete HighlightCreator (95 LOC, zero callers — YAGNI)
- Delete DraftLongTextNoteEvent (162 LOC, no consumers — YAGNI)
- Replace ArticleMediaRenderer interface with onLinkClick lambda
- Extract inline subscription to FilterBuilders.longFormByAddress()
- Replace SimpleDateFormat with thread-safe DateTimeFormatter
- Remove dead placeholders (bookmark button, reactions row, unused param)
- Cache draft index in memory to avoid redundant disk reads
- Add TODO for publish-before-relay-ack issue

P3 Nice-to-have:
- Remove timestamp from subscription subId for stability
- Remove redundant slug ".." removal (regex handles it)
- Add metadata field length limits (title 256, summary 1024)
- Fix identical isMacOS branches in editor
- Validate image URLs (http/https only) in ArticleHeader

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 11:03:29 +02:00
Vitor Pamplona 439b392aa0 Moves EmptyNostrClient to a class to avoid auto-import issues 2026-03-22 15:14:49 -04:00
nrobi144 e06287acb3 feat(chats): stacked messages layout in deck columns
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>
2026-03-19 07:49:36 +02:00
nrobi144 24d4073b9b feat(media): encrypted file sharing in desktop DMs (NIP-17 Phase 6)
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>
2026-03-19 06:53:36 +02:00
nrobi144 c90bf8f610 feat(media): add Note/Picture post type selector; fix gallery crash
- Post type toggle (Note vs Picture/kind 20) in compose dialog, only
  shown when image files are attached. Text input disabled in picture mode.
- Fix LazyVerticalGrid crash in GalleryTab: bounded height via
  fillParentMaxHeight() when nested inside LazyColumn.
- Phase 1 testing plan: all pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 14:42:44 +02:00
nrobi144 cf17dea53f feat(media): add Note/Picture post type selector in compose dialog
When images are attached, a Note/Picture toggle appears letting the user
publish as kind 20 (PictureEvent) instead of kind 1. Text input is
disabled in picture mode. Selector only shows for image file types.
Updates testing plan: Phase 1 all pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 14:37:48 +02:00
nrobi144 1d1d32da0e feat(media): add server selector to compose dialog; update testing plan
Server selector dropdown appears when files are attached, letting the
user choose which Blossom server to upload to. Updates testing plan:
Phase 2 & 3 all pass, Phase 9 audio tested with volume bug tracked.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 14:26:47 +02:00
nrobi144 836e024bd7 feat(media): use VLC :start-volume for initial audio; document volume bug
Passes :start-volume media option to VLC on play. Removes polling/delay
volume hacks. Documents 9.7 volume bug in testing plan — VLC ignores
initial volume on macOS, needs further investigation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 13:26:52 +02:00
nrobi144 2a6087af27 feat(media): global media player — persistent playback across navigation
Media playback now survives navigation. A GlobalMediaPlayer singleton owns
VLC players and exposes StateFlows. Composables are thin viewports.
NowPlayingBar has full controls (volume, mute, save, fullscreen).
GlobalFullscreenOverlay renders video fullscreen above all screens.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 12:27:13 +02:00
nrobi144 5a437ed5ac feat(search): collapsible section headers + sorting + search improvements
- Collapsible sticky section headers with animated chevron
- Full header row clickable to toggle collapse
- SearchResultSorter, SearchSortOrder, pseudo-kind filtering
- TextFieldValue cursor preservation, relay timeout improvements

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 13:23:27 +02:00
nrobi144 5475d6c2bc fix(desktop): verify user pubkey via get_public_key after nostrconnect login
The nostrconnect flow was trusting params[0] from the signer's connect
message as the user's pubkey. Some signers (e.g. nsec.app) don't put
the actual user pubkey there, causing all relay subscriptions to query
the wrong identity — contact lists, profiles, and DMs all returned empty.

Now calls remoteSigner.getPublicKey() after the handshake to get the
verified pubkey from the signer. Also stabilizes FeedScreen relay
subscriptions with distinctUntilChanged() and adds relay.primal.net
to defaults.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 10:20:18 +02:00