Files
amethyst/docs/plans/2026-04-21-feat-relay-config-parity-plan.md
T
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

9.8 KiB

title, type, status, date, parent
title type status date parent
feat: Relay Config Parity — All Desktop-Relevant Categories feat draft 2026-04-21 docs/plans/2026-04-20-feat-relay-power-tools-plan.md

feat: Relay Config Parity — All Desktop-Relevant Categories

Context

Phase 1-2 of Relay Power Tools shipped a dashboard with Monitor tab, a Configure tab with only Connected Relays (NIP-65/DM as placeholders), and a compose relay picker. This plan fills in all relay categories that desktop features actually consume.

Scope: Feature-Driven Categories

Only categories where desktop has a feature that reads/writes them:

Category Kind Desktop Feature State Class Priority
NIP-65 Inbox/Outbox 10002 Feeds, notifications, outbox publishing Nip65RelayListState (commons) P0
DM Relays 10050 DMs (DesktopMessagesScreen) DesktopAccountRelays._dmRelayList P0
Search Relays 10007 Search (SearchScreen, NIP-50) — (new) P1
Blocked Relays 10006 Privacy/moderation — (new) P2
Connected Relays runtime Everything (fallback pool) RelayConnectionManager Done

Explicitly Out of Scope

Category Kind Why
Private Outbox 10013 Desktop drafts are local-only (DesktopDraftStore)
Indexer Relays 10086 Desktop uses connected relays for indexing
Proxy Relays 10087 No proxy relay feature on desktop
Broadcast Relays 10088 Compose picker already handles per-action relay selection
Trusted Relays 10089 No trust-scoring UI on desktop
Key Package MIP-00 No MLS/group messaging on desktop
Relay Sets 30002 No custom grouping UI
Wiki Relays 10102 No wiki feature
Relay Feeds 10012 No relay feeds feature
Local Relays custom No local relay feature

Technical Approach

Architecture

Existing (Phase 1-2):
├── RelayConfigTab.kt          # MODIFY: replace placeholders with real editors
├── RelayListEditor.kt         # REUSE: generic add/remove/validate
├── DesktopAccountRelays.kt    # MODIFY: add NIP-65 + search relay state
├── DesktopIAccount.kt         # Already has Nip65RelayListState

New:
├── Nip65RelayEditor.kt        # NEW: inbox/outbox editor with read/write toggles
├── DmRelayEditor.kt           # NEW: DM relay editor with block-send-if-empty
├── SearchRelayEditor.kt       # NEW: search relay editor
├── BlockedRelayEditor.kt      # NEW: blocked relay editor
└── DesktopSearchRelayState.kt # NEW: search relay state (kind 10007)

Phase 3a: NIP-65 Inbox/Outbox Editor (P0)

Why P0: Feeds, notifications, and publishing all depend on NIP-65. Without this, desktop uses hardcoded default relays.

State: Nip65RelayListState already exists in commons and is instantiated in DesktopIAccount. Desktop already has outboxFlow and inboxFlow.

UI: Nip65RelayEditor.kt

@Composable
fun Nip65RelayEditor(
    nip65State: Nip65RelayListState,
    relayManager: RelayConnectionManager,
    onPublish: (AdvertisedRelayListEvent) -> Unit,
)
  • Two-column or tagged list: each relay has Read/Write/Both toggle
  • Uses AdvertisedRelayInfo with type (READ, WRITE, BOTH)
  • "Save" button calls nip65State.saveRelayList(relays) → returns signed event → onPublish broadcasts
  • Shows "Published to X of Y relays" confirmation
  • "Reset to defaults" option restores defaultOutboxRelays/defaultInboxRelays

Data flow:

  1. Read current: nip65State.getNIP65RelayList()?.relays() → list of AdvertisedRelayInfo
  2. User edits in mutable local state
  3. Save: nip65State.saveRelayList(editedRelays) → signed AdvertisedRelayListEvent
  4. Broadcast: relayManager.publish(event, connectedRelays)

Integration:

  • Replace "NIP-65 Inbox/Outbox — coming soon" placeholder in RelayConfigTab
  • Thread nip65State from DesktopIAccount through to RelayConfigTab

Phase 3b: DM Relay Editor (P0)

Why P0: Desktop has full DM support. DM relays control where encrypted messages are sent/received.

State: DesktopAccountRelays._dmRelayList already tracks kind 10050.

UI: DmRelayEditor.kt

@Composable
fun DmRelayEditor(
    dmRelays: StateFlow<Set<NormalizedRelayUrl>>,
    connectedRelays: StateFlow<Set<NormalizedRelayUrl>>,
    signer: NostrSigner,
    onPublish: (Event) -> Unit,
)
  • Simple relay list (no read/write split — DM relays are all-or-nothing)
  • Warning banner if empty: "No DM relays configured — DMs will use connected relays as fallback"
  • "Save" builds ChatMessageRelayListEvent → sign → publish
  • Security: highlight that these relays see your DM metadata

Data flow:

  1. Read current: accountRelays.dmRelayList StateFlow
  2. Save: build ChatMessageRelayListEvent.create(relays, signer) → publish

Integration:

  • Replace "DM Relays — coming soon" placeholder in RelayConfigTab
  • Thread accountRelays + signer through to RelayConfigTab

Phase 3c: Search Relay Editor (P1)

Why P1: Desktop has a full search screen with NIP-50 support but currently searches all connected relays. Dedicated search relays improve result quality.

State: New DesktopSearchRelayState needed — simple StateFlow<Set<NormalizedRelayUrl>> backed by kind 10007 events.

UI: SearchRelayEditor.kt

  • Same pattern as DM relay editor
  • Explain to user: "These relays support NIP-50 full-text search"
  • Default suggestion: wss://relay.nostr.band (common NIP-50 relay)
  • "Save" builds SearchRelayListEvent → sign → publish

Integration:

  • New section in RelayConfigTab after DM Relays
  • Wire search relay state into DesktopRelaySubscriptionsCoordinator for search queries

Phase 3d: Blocked Relay Editor (P2)

Why P2: Privacy feature — user can maintain a list of relays they don't want to connect to. Lower priority but simple to implement since pattern is identical.

State: New — kind 10006 BlockedRelayListEvent.

UI: BlockedRelayEditor.kt

  • List of blocked relay URLs
  • "Save" publishes kind 10006 event
  • Integration: filter blocked relays from connection pool (future)

Implementation Order

Phase What Files Depends On
3a NIP-65 editor Nip65RelayEditor.kt, modify RelayConfigTab, modify DeckColumnContainer (thread state) Nip65RelayListState (exists)
3b DM relay editor DmRelayEditor.kt, modify RelayConfigTab DesktopAccountRelays (exists)
3c Search relay editor SearchRelayEditor.kt, DesktopSearchRelayState.kt, modify RelayConfigTab, modify subscriptions coordinator New state class
3d Blocked relay editor BlockedRelayEditor.kt, modify RelayConfigTab BlockedRelayListEvent (exists in quartz)

State Threading

RelayConfigTab currently receives only relayManager. It needs:

@Composable
fun RelayConfigTab(
    relayManager: DesktopRelayConnectionManager,
    nip65State: Nip65RelayListState,        // Phase 3a
    accountRelays: DesktopAccountRelays,     // Phase 3b (DM relays)
    signer: NostrSigner,                     // For signing relay list events
    onPublish: (Event) -> Unit,              // Broadcast signed events
    modifier: Modifier = Modifier,
)

Thread through: Main.ktMainContentDeckLayout/SinglePaneLayoutDeckColumnContainerRootContentRelayDashboardScreenRelayConfigTab

Alternative: pass DesktopIAccount (already threaded) which has nip65State + signer, and accountRelays (already created alongside).

Shared RelayListEditor Pattern

All editors follow the same pattern — reuse RelayListEditor for the add/remove/validate part. Each editor wraps it with:

  1. Category-specific header + description
  2. Optional per-relay toggles (read/write for NIP-65)
  3. Save button that builds the right event kind
  4. Publish feedback

Event Kind Reference (from quartz)

Kind Event Class Tag Format
10002 AdvertisedRelayListEvent ["r", "wss://...", "read"|"write"]
10050 ChatMessageRelayListEvent ["relay", "wss://..."]
10007 SearchRelayListEvent ["relay", "wss://..."]
10006 BlockedRelayListEvent ["relay", "wss://..."]

Acceptance Criteria

Phase 3a: NIP-65

  • NIP-65 section shows current inbox/outbox relays from user's kind 10002
  • Each relay has Read/Write/Both toggle
  • Can add/remove relays
  • Save signs + publishes AdvertisedRelayListEvent
  • Shows "Published to X of Y" confirmation
  • Reset to defaults option

Phase 3b: DM Relays

  • DM section shows current kind 10050 relays
  • Warning if empty
  • Save signs + publishes ChatMessageRelayListEvent
  • Shows publish confirmation

Phase 3c: Search Relays

  • Search section shows current kind 10007 relays
  • Suggests relay.nostr.band if empty
  • Save signs + publishes SearchRelayListEvent
  • Search screen uses configured search relays

Phase 3d: Blocked Relays

  • Blocked section shows kind 10006 relays
  • Save signs + publishes BlockedRelayListEvent

Unanswered Questions

  1. Should NIP-65 editor show relays from the user's existing event, or from defaultOutboxRelays/defaultInboxRelays if no event exists?
  2. Should DM relay editor block fallback to connected relays (per original plan security decision) or just warn?
  3. For search relays — should we auto-detect NIP-50 support via NIP-11 supported_nips field and suggest capable relays from the connected pool?
  4. Should publishing relay list events use connectedRelays or the NIP-65 outbox relays? (Bootstrap problem if NIP-65 is empty.)
  5. How to handle the threading of 4+ state objects through the composable tree — pass DesktopIAccount directly or keep explicit params?