From 4a3cea85cbf476f9eac080483c65f4fcdcad737c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 14 May 2026 13:19:44 +0000 Subject: [PATCH] fix(nests): clear zap spinner on wallet handoff; move overlay off the hand badge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NestZapButton had no ObserveZapIconState fallback (unlike NoteCompose's ZapReaction), so after onPayViaIntent handed the invoice to an external wallet the progress spinner stayed up indefinitely in the long-lived NestActivity. Reset zappingProgress to 0 on every onPayViaIntent path. The floating zap overlay was anchored TopEnd, colliding with the hand-raise badge — and since the merge made zaps float from the zapper's avatar (a likely hand-raiser), that overlap would be common. Moved it to TopCenter, the only badge-free anchor. Also dropped dead state (zapStartingTime / the non-animated animatedProgress alias) and corrected stale comments left over from the sender-grouping merge. https://claude.ai/code/session_01EW11kUdiEYPuPti7vtD2AR --- .../nests/room/screen/NestActionBar.kt | 35 ++++++++++++++----- .../nests/room/stage/ParticipantsGrid.kt | 23 ++++++------ .../nests/room/stage/SpeakerZapOverlay.kt | 18 +++++----- 3 files changed, 49 insertions(+), 27 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/screen/NestActionBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/screen/NestActionBar.kt index a2df46dc1c..623c717a85 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/screen/NestActionBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/screen/NestActionBar.kt @@ -54,7 +54,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -83,7 +82,6 @@ import com.vitorpamplona.amethyst.ui.note.zapClick import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.reactions.RoomReactionPopup import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlin.uuid.ExperimentalUuidApi @@ -457,11 +455,15 @@ private fun NestZapButton( var wantsToZap by remember { mutableStateOf(false) } var wantsToSetCustomZap by remember { mutableStateOf(false) } + // 0f when idle. Driven 0f → 1f by the zap flow's progress + // callbacks; reset to 0f on error, on dismiss, and when the flow + // hands off to an external wallet via `onPayViaIntent` (the + // button can't observe payment completion past that handoff, so + // it must not leave the spinner up — unlike NoteCompose's + // ZapReaction, this button has no ObserveZapIconState fallback). var zappingProgress by remember { mutableFloatStateOf(0f) } - var zapStartingTime by remember { mutableLongStateOf(0L) } - val animatedProgress = zappingProgress - val isZapping = animatedProgress > 0.00001f && animatedProgress < 0.99999f + val isZapping = zappingProgress > 0.00001f && zappingProgress < 0.99999f FilledTonalIconButton( onClick = { @@ -470,7 +472,7 @@ private fun NestZapButton( baseNote = roomNote, accountViewModel = accountViewModel, context = context, - onZapStarts = { zapStartingTime = TimeUtils.now() }, + onZapStarts = {}, onZappingProgress = { progress -> scope.launch { zappingProgress = progress } }, onMultipleChoices = { scope.launch { wantsToZap = true } }, onError = { _, message, user -> @@ -480,6 +482,11 @@ private fun NestZapButton( } }, onPayViaIntent = { + // Handing off to an external wallet (or the + // manual-split screen) ends the in-app phase — + // clear the spinner since nothing downstream + // tracks payment completion for this button. + zappingProgress = 0f if (it.size == 1) { val payable = it.first() payViaIntent(payable.invoice, context, { }) { error -> @@ -502,7 +509,7 @@ private fun NestZapButton( ) { if (isZapping) { CircularProgressIndicator( - progress = { animatedProgress }, + progress = { zappingProgress }, strokeWidth = 2.dp, color = MaterialTheme.colorScheme.primary, ) @@ -519,7 +526,7 @@ private fun NestZapButton( baseNote = roomNote, popupYOffset = 48.dp, accountViewModel = accountViewModel, - onZapStarts = { zapStartingTime = TimeUtils.now() }, + onZapStarts = {}, onDismiss = { wantsToZap = false zappingProgress = 0f @@ -538,6 +545,11 @@ private fun NestZapButton( }, onProgress = { scope.launch(Dispatchers.Main) { zappingProgress = it } }, onPayViaIntent = { + // Handing off to an external wallet (or the manual- + // split screen) ends the in-app phase — clear the + // spinner since nothing downstream tracks payment + // completion for this button. + zappingProgress = 0f if (it.size == 1) { val payable = it.first() payViaIntent(payable.invoice, context, { }) { error -> @@ -558,7 +570,7 @@ private fun NestZapButton( if (wantsToSetCustomZap) { ZapCustomDialog( - onZapStarts = { zapStartingTime = TimeUtils.now() }, + onZapStarts = {}, onClose = { wantsToSetCustomZap = false }, onError = { _, message, user -> scope.launch { @@ -568,6 +580,11 @@ private fun NestZapButton( }, onProgress = { scope.launch(Dispatchers.Main) { zappingProgress = it } }, onPayViaIntent = { + // Handing off to an external wallet (or the manual- + // split screen) ends the in-app phase — clear the + // spinner since nothing downstream tracks payment + // completion for this button. + zappingProgress = 0f if (it.size == 1) { val payable = it.first() payViaIntent(payable.invoice, context, { }) { error -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/stage/ParticipantsGrid.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/stage/ParticipantsGrid.kt index 4df5b94064..a06a2ada0a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/stage/ParticipantsGrid.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/stage/ParticipantsGrid.kt @@ -561,21 +561,24 @@ private fun MemberCell( .offset(x = -ringPadding + 6.dp, y = -ringPadding + 6.dp), ) } - // Zaps animate in the avatar's TOP-right corner — same - // outer-Box sibling placement and reasoning as the - // reaction overlay above, just the opposite vertical - // corner so the two streams never stack on each other - // (reactions BottomEnd, mic badge BottomCenter). End- - // anchored like reactions so the chip extends LEFTWARD - // into the cell as `⚡ Nsats` widens, rather than bleeding - // into the neighbouring column. + // Zaps animate just above the avatar, TOP-CENTER — the + // only badge-free anchor: the role badge sits TopStart, + // the hand-raise badge TopEnd, the mic badge BottomCenter + // and the reaction overlay BottomEnd. Anchoring TopEnd + // would collide with the hand-raise badge, and now that + // zaps float from the *zapper's* avatar (a likely + // hand-raiser), that overlap would be common. Center- + // anchored so the `⚡ Nsats` chip extends symmetrically + // and stays within the cell rather than bleeding into the + // neighbouring column. Same outer-Box sibling placement + // and layout-stability reasoning as the reaction overlay. if (zaps.isNotEmpty()) { SpeakerZapOverlay( zaps = zaps, modifier = Modifier - .align(Alignment.TopEnd) - .offset(x = -ringPadding + 6.dp, y = ringPadding - 6.dp), + .align(Alignment.TopCenter) + .offset(y = ringPadding - 6.dp), ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/stage/SpeakerZapOverlay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/stage/SpeakerZapOverlay.kt index d99191b320..ccbc735b27 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/stage/SpeakerZapOverlay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/stage/SpeakerZapOverlay.kt @@ -53,11 +53,13 @@ import kotlinx.coroutines.delay /** * Floating zap-chip overlay drawn over a participant's avatar — the - * zap counterpart to [SpeakerReactionOverlay]. Each chip is keyed by - * the zap event id; consecutive zaps to the same target stack into a - * row, with the chip life-cycle (`fadeIn + scaleIn` on arrival, - * upward drift + `fadeOut` over [REACTION_WINDOW_SEC]) matching - * reactions so both streams visually feel like the same animation. + * zap counterpart to [SpeakerReactionOverlay]. The aggregator groups + * zaps by sender, so this renders the zaps a single participant has + * *sent*: consecutive zaps from that sender stack into a row, each + * chip keyed by its event id. The chip life-cycle (`fadeIn + scaleIn` + * on arrival, upward drift + `fadeOut` over [REACTION_WINDOW_SEC]) + * matches reactions so both streams visually feel like the same + * animation. * * Renders an "⚡ Nsats" pill in [BitcoinOrange] so zaps are distinct * from emoji reactions at a glance. @@ -141,7 +143,7 @@ private fun ZapChip(zap: RoomZap) { private const val ZAP_WINDOW_MS = REACTION_WINDOW_SEC * 1000L -// Cap the row at a small number so a burst of zaps to the same -// participant doesn't overflow the avatar's bottom-right corner — -// the eviction sweep clears them within the window anyway. +// Cap the row at a small number so a burst of zaps from the same +// sender doesn't overflow the avatar's top-center anchor — the +// eviction sweep clears them within the window anyway. private const val MAX_VISIBLE_ZAPS = 3