From 5bda97db04177a425389a62f8ed8afcfba4ec756 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 11 May 2026 20:43:56 +0000 Subject: [PATCH] fix(nests): promote/demote/kick actions no longer cancelled by sheet dismiss MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-participant host actions (Promote to Speaker, Promote to Moderator, Demote, Force Mute, Kick) and the Hand-Raise queue's Approve button all launched their sign+broadcast on a rememberCoroutineScope() bound to the local composable. Because every action row calls onDismiss() (and the hand-raise row may disappear when canSpeak() flips true), the composable leaves the tree synchronously after the click, cancelling the scope before the launched coroutine ever runs sign(template). The user saw the sheet close and nothing else — the kind-30312 republish / kind-4312 admin event was never actually emitted. Launch on accountViewModel.viewModelScope instead so the broadcast outlives the dismissing UI. --- .../room/participants/ParticipantHostActionsSheet.kt | 12 +++++++++--- .../nests/room/stage/HandRaiseQueueSection.kt | 12 +++++++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/participants/ParticipantHostActionsSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/participants/ParticipantHostActionsSheet.kt index 8c76c30b4..c2dc3268b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/participants/ParticipantHostActionsSheet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/participants/ParticipantHostActionsSheet.kt @@ -37,12 +37,12 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.MainActivity import com.vitorpamplona.amethyst.ui.components.toasts.multiline.UserBasedErrorMessage @@ -88,7 +88,6 @@ internal fun ParticipantHostActionsSheet( catalog: com.vitorpamplona.amethyst.commons.viewmodels.RoomSpeakerCatalog? = null, ) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - val scope = rememberCoroutineScope() val roomATag = ATag( @@ -98,9 +97,16 @@ internal fun ParticipantHostActionsSheet( relay = null, ) + // Use the AccountViewModel's scope so the launched broadcast + // survives the onDismiss() that every action row triggers. A + // composition-scoped rememberCoroutineScope() gets cancelled the + // moment hostMenuTarget flips to null and the sheet leaves the + // tree — which races (and reliably loses) against the suspending + // signer.sign(template) call, so the promote/demote/kick events + // never actually went out. fun broadcast(template: com.vitorpamplona.quartz.nip01Core.signers.EventTemplate?) { template ?: return - scope.launch { runCatching { accountViewModel.account.signAndComputeBroadcast(template) } } + accountViewModel.viewModelScope.launch { runCatching { accountViewModel.account.signAndComputeBroadcast(template) } } } val targetUser = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/stage/HandRaiseQueueSection.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/stage/HandRaiseQueueSection.kt index 2d1421f82..a4e6baef2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/stage/HandRaiseQueueSection.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/stage/HandRaiseQueueSection.kt @@ -37,10 +37,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.viewmodels.NestViewModel import com.vitorpamplona.amethyst.commons.viewmodels.RoomPresence @@ -96,7 +96,6 @@ internal fun HandRaiseQueueSection( if (hands.isEmpty()) return - val scope = rememberCoroutineScope() Column(modifier = modifier.fillMaxSize().padding(top = 12.dp)) { Text( text = stringRes(R.string.nest_hand_raise_queue_title), @@ -112,7 +111,14 @@ internal fun HandRaiseQueueSection( hand = hand, accountViewModel = accountViewModel, onApprove = { - scope.launch { + // Approving makes `canSpeak()` true for this user, which + // removes them from `hands` on the next recompose. If the + // queue empties (no other raised hands), the whole + // section leaves composition — a rememberCoroutineScope() + // bound here would cancel the in-flight signer.sign(...) + // and the promote never goes out. Launch on the + // AccountViewModel's scope so signing outlives the row. + accountViewModel.viewModelScope.launch { val template = RoomParticipantActions.setRole(event, hand.pubkey, ROLE.SPEAKER) template?.let { runCatching { accountViewModel.account.signAndComputeBroadcast(it) } } }