66962d360d
- 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>
233 lines
9.8 KiB
Markdown
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?
|