From a70101f5e59aef164da25889929748ab16709bad Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 9 Feb 2026 20:28:35 -0500 Subject: [PATCH] cleans up broadcasting tracker --- .../service/broadcast/BroadcastModels.kt | 5 +- .../amethyst/ui/broadcast/BroadcastBanner.kt | 14 +- .../ui/broadcast/BroadcastDetailsSheet.kt | 57 ++----- .../ui/broadcast/BroadcastSnackbar.kt | 145 ------------------ .../ui/broadcast/DisplayBroadcastProgress.kt | 20 ++- amethyst/src/main/res/values/strings.xml | 20 +++ 6 files changed, 57 insertions(+), 204 deletions(-) delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/broadcast/BroadcastSnackbar.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/broadcast/BroadcastModels.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/broadcast/BroadcastModels.kt index 9482b8370..931726d90 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/broadcast/BroadcastModels.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/broadcast/BroadcastModels.kt @@ -141,7 +141,4 @@ data class BroadcastEvent( data class BroadcastResult( val broadcast: BroadcastEvent, val isSuccess: Boolean, -) { - val successCount: Int get() = broadcast.successCount - val totalRelays: Int get() = broadcast.totalRelays -} +) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/broadcast/BroadcastBanner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/broadcast/BroadcastBanner.kt index 4c5b3d441..b2f1773ec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/broadcast/BroadcastBanner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/broadcast/BroadcastBanner.kt @@ -51,7 +51,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.broadcast.BroadcastEvent +import com.vitorpamplona.amethyst.ui.stringRes /** * Banner showing active broadcast progress. @@ -102,7 +104,7 @@ private fun SingleBroadcastContent(broadcast: BroadcastEvent) { ) { Icon( imageVector = Icons.Default.Sync, - contentDescription = "Broadcasting", + contentDescription = stringRes(R.string.broadcasting), tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(18.dp), ) @@ -113,7 +115,7 @@ private fun SingleBroadcastContent(broadcast: BroadcastEvent) { modifier = Modifier.fillMaxWidth(), ) { Text( - text = "Broadcasting ${broadcast.eventName} (kind ${broadcast.kind})", + text = stringRes(R.string.broadcasting_name, broadcast.eventName), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurface, maxLines = 1, @@ -124,7 +126,7 @@ private fun SingleBroadcastContent(broadcast: BroadcastEvent) { Spacer(Modifier.width(8.dp)) Text( - text = "[${broadcast.results.size}/${broadcast.totalRelays}]", + text = stringRes(R.string.share_of, broadcast.results.size, broadcast.totalRelays), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary, ) @@ -160,7 +162,7 @@ private fun MultipleBroadcastsContent(broadcasts: List) { ) { Icon( imageVector = Icons.Default.Sync, - contentDescription = "Broadcasting", + contentDescription = stringRes(R.string.broadcasting), tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(18.dp), ) @@ -171,13 +173,13 @@ private fun MultipleBroadcastsContent(broadcasts: List) { modifier = Modifier.fillMaxWidth(), ) { Text( - text = "Broadcasting ${broadcasts.size} events...", + text = stringRes(R.string.broadcasting_number_events, broadcasts.size), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurface, ) Text( - text = "[$completedResponses/$totalRelays]", + text = stringRes(R.string.share_of, completedResponses, totalRelays), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/broadcast/BroadcastDetailsSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/broadcast/BroadcastDetailsSheet.kt index d57c88079..f6c9c193c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/broadcast/BroadcastDetailsSheet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/broadcast/BroadcastDetailsSheet.kt @@ -70,42 +70,20 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.broadcast.BroadcastEvent import com.vitorpamplona.amethyst.service.broadcast.BroadcastStatus import com.vitorpamplona.amethyst.service.broadcast.RelayResult +import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl private const val MAX_EXPANDED_SECTIONS = 2 -/** - * Modal bottom sheet showing detailed relay results for broadcasts. - * Shows up to 2 expanded sections, with a summary for additional broadcasts. - */ -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun BroadcastDetailsSheet( - broadcast: BroadcastEvent, - onDismiss: () -> Unit, - onRetryRelay: (NormalizedRelayUrl) -> Unit, - onRetryAllFailed: () -> Unit, - sheetState: SheetState = - rememberModalBottomSheetState( - skipPartiallyExpanded = true, - ), -) { - MultiBroadcastDetailsSheet( - broadcasts = listOf(broadcast), - onDismiss = onDismiss, - onRetryRelay = { _, relay -> onRetryRelay(relay) }, - onRetryAllFailed = { onRetryAllFailed() }, - sheetState = sheetState, - ) -} - /** * Modal bottom sheet showing detailed relay results for multiple broadcasts. */ @@ -164,7 +142,7 @@ fun MultiBroadcastDetailsSheet( ) { // Header Text( - text = if (broadcasts.size == 1) "Broadcast Results" else "Broadcasts (${broadcasts.size})", + text = if (broadcasts.size == 1) stringRes(R.string.broadcast_results) else stringRes(R.string.broadcasts_number, broadcasts.size), style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.onSurface, ) @@ -220,7 +198,7 @@ fun MultiBroadcastDetailsSheet( onClick = onDismiss, modifier = Modifier.fillMaxWidth(), ) { - Text("Dismiss") + Text(stringRes(R.string.dismiss)) } } } @@ -259,22 +237,17 @@ private fun BroadcastSection( maxLines = 1, overflow = TextOverflow.Ellipsis, ) - Text( - text = "kind ${broadcast.kind}", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) } Text( - text = "${broadcast.successCount}/${broadcast.totalRelays}", + text = stringResource(R.string.share_of, broadcast.successCount, broadcast.totalRelays), style = MaterialTheme.typography.labelLarge, color = statusColor(broadcast.status), ) Icon( imageVector = if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore, - contentDescription = if (isExpanded) "Collapse" else "Expand", + contentDescription = if (isExpanded) stringRes(R.string.collapse) else stringRes(R.string.expand), tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.size(24.dp), ) @@ -315,7 +288,7 @@ private fun BroadcastSection( modifier = Modifier.size(18.dp), ) Spacer(Modifier.width(4.dp)) - Text("Retry Failed (${broadcast.failedRelays.size})") + Text(stringRes(R.string.retry_failed_number, broadcast.failedRelays.size)) } } } @@ -349,16 +322,16 @@ private fun OverflowSummary(broadcasts: List) { Column(modifier = Modifier.weight(1f)) { Text( - text = "+${broadcasts.size} more broadcasts", + text = stringRes(R.string.more_broadcasts_number, broadcasts.size), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurface, ) Text( text = buildString { - append("$totalSuccess/$totalRelays relays") + append(stringResource(R.string.share_of_relays, totalSuccess, totalRelays)) if (inProgressCount > 0) { - append(" ($inProgressCount in progress)") + append(stringResource(R.string.number_in_progress, inProgressCount)) } }, style = MaterialTheme.typography.bodySmall, @@ -479,7 +452,7 @@ private fun RelayResultRow( is RelayResult.Timeout -> { Text( - text = "Timeout", + text = stringRes(R.string.timeout), style = MaterialTheme.typography.bodySmall, color = warningColor, ) @@ -487,7 +460,7 @@ private fun RelayResultRow( is RelayResult.Retrying -> { Text( - text = "Retrying...", + text = stringRes(R.string.retrying), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.primary, ) @@ -498,14 +471,14 @@ private fun RelayResultRow( } // Retry button for failed relays (not when already retrying) - if ((result is RelayResult.Error || result is RelayResult.Timeout) && result !is RelayResult.Retrying) { + if (result is RelayResult.Error || result is RelayResult.Timeout) { IconButton( onClick = onRetry, modifier = Modifier.size(32.dp), ) { Icon( imageVector = Icons.Default.Refresh, - contentDescription = "Retry", + contentDescription = stringResource(R.string.retry), tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(18.dp), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/broadcast/BroadcastSnackbar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/broadcast/BroadcastSnackbar.kt deleted file mode 100644 index 88aaf44be..000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/broadcast/BroadcastSnackbar.kt +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.broadcast - -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Snackbar -import androidx.compose.material3.SnackbarData -import androidx.compose.material3.SnackbarDuration -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.SnackbarResult -import androidx.compose.material3.SnackbarVisuals -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.service.broadcast.BroadcastEvent -import com.vitorpamplona.amethyst.service.broadcast.BroadcastStatus -import kotlinx.coroutines.flow.SharedFlow - -/** - * Custom snackbar visuals for broadcast results. - */ -data class BroadcastSnackbarVisuals( - val broadcast: BroadcastEvent, - override val actionLabel: String? = "View", - override val duration: SnackbarDuration = SnackbarDuration.Short, - override val withDismissAction: Boolean = true, -) : SnackbarVisuals { - override val message: String - get() = - when (broadcast.status) { - BroadcastStatus.SUCCESS -> "${broadcast.eventName} sent to ${broadcast.successCount}/${broadcast.totalRelays} relays" - BroadcastStatus.PARTIAL -> "${broadcast.eventName} sent to ${broadcast.successCount}/${broadcast.totalRelays} relays" - BroadcastStatus.FAILED -> "${broadcast.eventName} failed - 0/${broadcast.totalRelays} relays" - BroadcastStatus.IN_PROGRESS -> "Broadcasting ${broadcast.eventName}..." - } -} - -/** - * Snackbar host that listens to completed broadcasts and shows result notifications. - */ -@Composable -fun BroadcastSnackbarHost( - completedBroadcast: SharedFlow, - onViewDetails: (BroadcastEvent) -> Unit, - onRetry: (BroadcastEvent) -> Unit, - modifier: Modifier = Modifier, -) { - val snackbarHostState = remember { SnackbarHostState() } - - LaunchedEffect(completedBroadcast) { - completedBroadcast.collect { broadcast -> - val visuals = BroadcastSnackbarVisuals(broadcast) - val result = snackbarHostState.showSnackbar(visuals) - - when (result) { - SnackbarResult.ActionPerformed -> { - if (broadcast.status == BroadcastStatus.FAILED) { - onRetry(broadcast) - } else { - onViewDetails(broadcast) - } - } - - SnackbarResult.Dismissed -> { /* No action */ } - } - } - } - - SnackbarHost( - hostState = snackbarHostState, - modifier = modifier.padding(bottom = 8.dp), - ) { snackbarData -> - BroadcastSnackbarContent(snackbarData) - } -} - -@Composable -private fun BroadcastSnackbarContent(snackbarData: SnackbarData) { - val visuals = snackbarData.visuals - val broadcastVisuals = visuals as? BroadcastSnackbarVisuals - - val containerColor = - when (broadcastVisuals?.broadcast?.status) { - BroadcastStatus.FAILED -> Color(0xFF7F1D1D) - - // Dark red - BroadcastStatus.PARTIAL -> Color(0xFF78350F) - - // Dark amber - else -> Color(0xFF1E3A5F) // Dark blue (default) - } - - val actionLabel = - when (broadcastVisuals?.broadcast?.status) { - BroadcastStatus.FAILED -> "Retry" - else -> "View" - } - - Snackbar( - action = { - snackbarData.visuals.actionLabel?.let { - TextButton(onClick = { snackbarData.performAction() }) { - Text(actionLabel) - } - } - }, - dismissAction = - if (visuals.withDismissAction) { - { - TextButton(onClick = { snackbarData.dismiss() }) { - Text("Dismiss") - } - } - } else { - null - }, - containerColor = containerColor, - ) { - Text(visuals.message) - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/broadcast/DisplayBroadcastProgress.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/broadcast/DisplayBroadcastProgress.kt index 99eff9066..8792ebbcf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/broadcast/DisplayBroadcastProgress.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/broadcast/DisplayBroadcastProgress.kt @@ -33,6 +33,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons @@ -58,10 +59,12 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.broadcast.BroadcastEvent import com.vitorpamplona.amethyst.service.broadcast.BroadcastStatus import com.vitorpamplona.amethyst.service.broadcast.BroadcastTracker import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -80,7 +83,6 @@ fun DisplayBroadcastProgress(accountViewModel: AccountViewModel) { // Only show in COMPLETE UI mode if (!accountViewModel.settings.isCompleteUIMode()) return - val scope = rememberCoroutineScope() val activeBroadcasts by accountViewModel.broadcastTracker.activeBroadcasts.collectAsStateWithLifecycle() // State for completed broadcast (with auto-dismiss) @@ -116,11 +118,13 @@ fun DisplayBroadcastProgress(accountViewModel: AccountViewModel) { Modifier .fillMaxWidth() .align(Alignment.BottomCenter) - .padding(bottom = 56.dp), + .navigationBarsPadding() + .padding(bottom = 50.dp), ) // Completed broadcast indicator (when no active broadcasts) if (activeBroadcasts.isEmpty()) { + val scope = rememberCoroutineScope() CompletedBroadcastIndicator( broadcast = completedBroadcast, onTap = { broadcast -> @@ -142,13 +146,15 @@ fun DisplayBroadcastProgress(accountViewModel: AccountViewModel) { Modifier .fillMaxWidth() .align(Alignment.BottomCenter) - .padding(bottom = 56.dp), + .navigationBarsPadding() + .padding(bottom = 50.dp), ) } } // Details sheet - show all broadcasts when opened if (selectedBroadcast != null) { + val scope = rememberCoroutineScope() // Combine active broadcasts with the selected/completed one val allBroadcasts = (activeBroadcasts + listOfNotNull(completedBroadcast)) @@ -238,7 +244,7 @@ private fun CompletedBroadcastIndicator( modifier = Modifier.fillMaxWidth(), ) { Text( - text = "${b.eventName} sent", + text = stringRes(R.string.event_sent, b.eventName), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurface, maxLines = 1, @@ -247,7 +253,7 @@ private fun CompletedBroadcastIndicator( ) Text( - text = "[${b.successCount}/${b.totalRelays}]", + text = stringRes(R.string.share_of, b.successCount, b.totalRelays), style = MaterialTheme.typography.labelMedium, color = iconTint, ) @@ -255,7 +261,7 @@ private fun CompletedBroadcastIndicator( if (b.failedRelays.isNotEmpty()) { Text( - text = "Tap to view details", + text = stringRes(R.string.tap_to_view_details), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -270,7 +276,7 @@ private fun CompletedBroadcastIndicator( ) { Icon( imageVector = Icons.Default.Refresh, - contentDescription = "Retry failed", + contentDescription = stringRes(R.string.retry_failed), tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(18.dp), ) diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index b93fa4d57..1798a428c 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1475,4 +1475,24 @@ Delete List Delete Pack Kinds + Retry failed + Tap to view details + %1$s sent + Broadcast Results + Broadcasts (%1$d) + Collapse + Expand + Retry Failed (%1$d) + +%1$d more broadcasts + " (%1$d in progress)" + %1$d/%2$d relays + Timeout + Retrying… + Retry + View + Dismiss + %1$d/%2$d + Broadcasting + Broadcasting %1$s + Broadcasting %1$d events...