Problem
Currently, reactions (likes, boosts, zaps, bookmarks) use a fire-and-forget model with optimistic UI updates. Users have no visibility into:
- Whether their event was actually delivered to relays
- Which relays accepted/rejected the event
- Why a relay might have rejected it (AUTH_REQUIRED, rate-limited, etc.)
This creates a "happy path heuristic" where the UI always shows success, even when events fail to propagate.
Proposed Solution
Add transparent broadcasting feedback when FeatureSetType.COMPLETE is enabled:
1. Global Progress Banner (Above Bottom Nav)
┌─────────────────────────────────────────────┐
│ Feed Content │
├─────────────────────────────────────────────┤
│ 🔄 Broadcasting Boost (kind 6) [2/5] │ ← Compact banner
│ ████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ ← Determinate progress
├─────────────────────────────────────────────┤
│ [Bottom Navigation] │
└─────────────────────────────────────────────┘
- Shows event type and kind number
- Live progress as relays respond (animated transitions)
- Collapses when multiple events in-flight: "Broadcasting 3 events... [5/15]"
2. Result Snackbar
Success (partial or full):
┌─────────────────────────────────────────────┐
│ ✓ Boost sent to 4/5 relays [View] │
└─────────────────────────────────────────────┘
Failure (0 relays):
┌─────────────────────────────────────────────┐
│ ✗ Boost failed - 0/5 relays [Retry] │
└─────────────────────────────────────────────┘
- If 0/5 succeed: don't mark action as complete (button stays unpressed)
- Tappable action opens detail sheet
3. Broadcast Details BottomSheet
┌─────────────────────────────────────────────┐
│ ────── Broadcast Results │
├─────────────────────────────────────────────┤
│ Boost (kind 6) • 4/5 relays │
├─────────────────────────────────────────────┤
│ ✓ relay.damus.io │
│ ✓ nos.lol │
│ ✓ relay.nostr.band │
│ ✓ purplepag.es │
│ ✗ relay.snort.social [AUTH_REQUIRED] ↻ │
├─────────────────────────────────────────────┤
│ [Dismiss] [Retry Failed] │
└─────────────────────────────────────────────┘
- Shows each relay with status (animated state transitions)
- Error code/message for failures
- Optional: Retry individual relays or all failed
- Optional: Undo/Delete action (sends kind 5 deletion)
Technical Implementation
Existing Infrastructure (No Changes Needed)
| Component |
Location |
Purpose |
OkMessage |
quartz/.../relay/commands/toClient/OkMessage.kt |
NIP-20 OK response parsing |
RelayInsertConfirmationCollector |
quartz/.../relay/client/accessories/ |
Tracks relay confirmations |
sendAndWaitForResponse() |
quartz/.../NostrClientSendAndWaitExt.kt |
15s timeout, waits for all relays |
DeletionEvent |
quartz/.../nip09Deletions/DeletionEvent.kt |
Kind 5 delete events |
New Components
1. BroadcastTracker (State Management)
// Location: amethyst/src/main/java/com/vitorpamplona/amethyst/service/broadcast/
data class BroadcastEvent(
val id: String, // Tracking ID
val eventId: HexKey, // Nostr event ID
val eventName: String, // "Boost", "Reaction", "Bookmark"
val kind: Int,
val targetRelays: List<NormalizedRelayUrl>,
val results: Map<NormalizedRelayUrl, RelayResult> = emptyMap(),
val status: BroadcastStatus = BroadcastStatus.IN_PROGRESS,
val startedAt: Long = System.currentTimeMillis()
)
sealed class RelayResult {
object Success : RelayResult()
data class Error(val code: String, val message: String?) : RelayResult()
object Timeout : RelayResult()
}
enum class BroadcastStatus { IN_PROGRESS, SUCCESS, PARTIAL, FAILED }
class BroadcastTracker {
private val _activeBroadcasts = MutableStateFlow<List<BroadcastEvent>>(emptyList())
val activeBroadcasts: StateFlow<List<BroadcastEvent>> = _activeBroadcasts.asStateFlow()
private val _completedBroadcast = MutableSharedFlow<BroadcastEvent>()
val completedBroadcast: SharedFlow<BroadcastEvent> = _completedBroadcast
suspend fun trackBroadcast(
event: Event,
eventName: String,
relays: Set<NormalizedRelayUrl>,
client: INostrClient
): BroadcastResult
fun getDetails(trackingId: String): BroadcastEvent?
suspend fun retry(trackingId: String, relay: NormalizedRelayUrl? = null)
fun dismiss(trackingId: String)
}
2. UI Components
| Component |
File |
Description |
BroadcastBanner |
ui/broadcast/BroadcastBanner.kt |
Floating progress indicator |
BroadcastSnackbar |
ui/broadcast/BroadcastSnackbar.kt |
Result notification |
BroadcastDetailsSheet |
ui/broadcast/BroadcastDetailsSheet.kt |
Modal bottom sheet with relay list |
RelayResultRow |
ui/broadcast/RelayResultRow.kt |
Single relay status row |
Scope
In Scope
- Reactions (kind 7)
- Boosts/Reposts (kind 6)
- Zaps (kind 9735 request)
- Bookmarks (kind 30001)
- Progress indicator with live animated updates
- Result snackbar with tap-to-expand
- Relay status detail sheet
- Retry failed relays
- Only when
FeatureSetType.COMPLETE
- 15s timeout for relay responses
Optional (Nice to Have)
- Undo/Delete action (kind 5)
- Persist broadcast history across sessions
Out of Scope
- SIMPLIFIED and PERFORMANCE UI modes (keep fire-and-forget)
- Desktop app (separate implementation later)
- NWC zap response tracking changes
Implementation Phases
Phase 1: Core Infrastructure
- Create
BroadcastTracker class
- Create
BroadcastEvent and RelayResult data classes
- Add to
AccountViewModel
Phase 2: UI Components
- Create
BroadcastBanner composable (with animations)
- Create
BroadcastSnackbar composable
- Create
BroadcastDetailsSheet composable
Phase 3: Integration
- Modify
Account publish methods to support tracked mode
- Wire banner into
MainScaffold
- Add
FeatureSetType.COMPLETE gates
Phase 4: Polish
- Add retry functionality
- Handle edge cases (offline, rapid actions)
- Optional: Add delete/undo support
References
Problem
Currently, reactions (likes, boosts, zaps, bookmarks) use a fire-and-forget model with optimistic UI updates. Users have no visibility into:
This creates a "happy path heuristic" where the UI always shows success, even when events fail to propagate.
Proposed Solution
Add transparent broadcasting feedback when
FeatureSetType.COMPLETEis enabled:1. Global Progress Banner (Above Bottom Nav)
2. Result Snackbar
Success (partial or full):
Failure (0 relays):
3. Broadcast Details BottomSheet
Technical Implementation
Existing Infrastructure (No Changes Needed)
OkMessagequartz/.../relay/commands/toClient/OkMessage.ktRelayInsertConfirmationCollectorquartz/.../relay/client/accessories/sendAndWaitForResponse()quartz/.../NostrClientSendAndWaitExt.ktDeletionEventquartz/.../nip09Deletions/DeletionEvent.ktNew Components
1. BroadcastTracker (State Management)
2. UI Components
BroadcastBannerui/broadcast/BroadcastBanner.ktBroadcastSnackbarui/broadcast/BroadcastSnackbar.ktBroadcastDetailsSheetui/broadcast/BroadcastDetailsSheet.ktRelayResultRowui/broadcast/RelayResultRow.ktScope
In Scope
FeatureSetType.COMPLETEOptional (Nice to Have)
Out of Scope
Implementation Phases
Phase 1: Core Infrastructure
BroadcastTrackerclassBroadcastEventandRelayResultdata classesAccountViewModelPhase 2: UI Components
BroadcastBannercomposable (with animations)BroadcastSnackbarcomposableBroadcastDetailsSheetcomposablePhase 3: Integration
Accountpublish methods to support tracked modeMainScaffoldFeatureSetType.COMPLETEgatesPhase 4: Polish
References
NostrClientSendAndWaitExt.kt,RelayInsertConfirmationCollector.kt