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

233 lines
9.8 KiB
Markdown

---
title: "feat: Relay Config Parity — All Desktop-Relevant Categories"
type: feat
status: draft
date: 2026-04-21
parent: 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`**
```kotlin
@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`**
```kotlin
@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:
```kotlin
@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.kt``MainContent``DeckLayout`/`SinglePaneLayout``DeckColumnContainer``RootContent``RelayDashboardScreen``RelayConfigTab`
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?