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) } } }