diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/signing/SigningAwareButton.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/signing/SigningAwareButton.kt index 8001a68b43..8e8d821fab 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/signing/SigningAwareButton.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/signing/SigningAwareButton.kt @@ -43,7 +43,7 @@ fun SigningAwareButton( tint: Color = MaterialTheme.colorScheme.onSurfaceVariant, ) { when (signingState.state) { - is SigningOpState.Pending -> { + is SigningOpState.Pending, is SigningOpState.Progress -> { Box(modifier = modifier.size(32.dp), contentAlignment = Alignment.Center) { CircularProgressIndicator( modifier = Modifier.size(16.dp), diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/signing/SigningState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/signing/SigningState.kt index d76efe5b99..6bc82cb1bb 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/signing/SigningState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/signing/SigningState.kt @@ -37,11 +37,29 @@ sealed class SigningOpState { data object Pending : SigningOpState() + /** + * Signing is in flight AND has a known step count — typically a NIP-17 + * group send via a remote signer (bunker), where the UI can usefully show + * "Encrypting via remote signer ([current] of [total])". + * + * Treated as Pending for all is-pending checks via [isPending] below; + * existing callers that branch on `is Pending` keep working unchanged. + * New callers can render the counter when [SigningOpState] is `Progress`. + */ + data class Progress( + val current: Int, + val total: Int, + val label: String? = null, + ) : SigningOpState() + data class Error( val message: String, ) : SigningOpState() } +/** True when signing is in flight, regardless of whether step counts are known. */ +fun SigningOpState.isPending(): Boolean = this is SigningOpState.Pending || this is SigningOpState.Progress + /** * Global signing status — any [SigningState] instance updates this when signing starts/ends. * Observe [globalState] from a screen-level composable to show a persistent status bar. @@ -86,7 +104,7 @@ class SigningState { private set suspend fun execute(block: suspend () -> T): T? { - if (state is SigningOpState.Pending) return null + if (state.isPending()) return null state = SigningOpState.Pending GlobalSigningStatus.onPending() errorMessage = null @@ -114,6 +132,23 @@ class SigningState { } } + /** + * Update the in-flight signing state with a progress counter. Use during + * multi-step operations (NIP-17 group send via bunker, batch zaps) to + * show the user how far the in-flight op has progressed. + * + * Only takes effect while [state] is Pending or Progress — no-op + * otherwise so callers don't have to gate on Idle/Error themselves. + */ + fun updateProgress( + current: Int, + total: Int, + label: String? = null, + ) { + if (!state.isPending()) return + state = SigningOpState.Progress(current, total, label) + } + private fun setError(message: String) { errorMessage = message state = SigningOpState.Error(message) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/signing/SigningStatusBar.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/signing/SigningStatusBar.kt index 2c6abd43b2..2ac91ec022 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/signing/SigningStatusBar.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/signing/SigningStatusBar.kt @@ -87,6 +87,21 @@ fun SigningStatusBar( } } + is SigningOpState.Progress -> { + Snackbar( + shape = RoundedCornerShape(8.dp), + containerColor = MaterialTheme.colorScheme.inverseSurface, + contentColor = MaterialTheme.colorScheme.inverseOnSurface, + modifier = Modifier.padding(horizontal = 16.dp), + ) { + val label = opState.label ?: "Signing" + Text( + text = "$label (${opState.current} of ${opState.total})", + style = MaterialTheme.typography.bodyMedium, + ) + } + } + is SigningOpState.Error -> { Snackbar( shape = RoundedCornerShape(8.dp),