fix(nests): promote/demote/kick actions no longer cancelled by sheet dismiss

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.
This commit is contained in:
Claude
2026-05-11 20:43:56 +00:00
parent e82cd0f6f8
commit 5bda97db04
2 changed files with 18 additions and 6 deletions
@@ -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<out com.vitorpamplona.quartz.nip01Core.core.Event>?) {
template ?: return
scope.launch { runCatching { accountViewModel.account.signAndComputeBroadcast(template) } }
accountViewModel.viewModelScope.launch { runCatching { accountViewModel.account.signAndComputeBroadcast(template) } }
}
val targetUser =
@@ -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) } }
}