From c0710b794f874ceeb0ba8a4f2b852d8d4885cf14 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 11 May 2026 21:02:57 +0000 Subject: [PATCH] refactor(nests): route host actions through AccountViewModel.launchSigner Replace the raw viewModelScope.launch + runCatching with launchSigner, matching the rest of the codebase. Same survival-past-dismiss guarantee, plus signer errors (read-only key, NIP-46 unauthorized / timeout, signer not found) now surface as toasts instead of being silently swallowed. --- .../ParticipantHostActionsSheet.kt | 15 +++++---------- .../nests/room/stage/HandRaiseQueueSection.kt | 19 ++++++++----------- 2 files changed, 13 insertions(+), 21 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 c2dc3268b6..8ff9d05cbf 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 @@ -42,7 +42,6 @@ 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 @@ -56,7 +55,6 @@ import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ROLE -import kotlinx.coroutines.launch /** * Per-participant context sheet (T2 #2). Always shows the @@ -97,16 +95,13 @@ 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. + // Go through accountViewModel.launchSigner so the broadcast runs on + // viewModelScope (survives onDismiss() removing the sheet from + // composition) and signer errors surface as toasts instead of being + // silently swallowed. fun broadcast(template: com.vitorpamplona.quartz.nip01Core.signers.EventTemplate?) { template ?: return - accountViewModel.viewModelScope.launch { runCatching { accountViewModel.account.signAndComputeBroadcast(template) } } + accountViewModel.launchSigner { 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 a4e6baef20..bfb741a6a5 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 @@ -40,7 +40,6 @@ import androidx.compose.runtime.remember 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 @@ -53,7 +52,6 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size35dp import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ROLE -import kotlinx.coroutines.launch /** * Host-only queue of audience members whose latest kind-10312 @@ -111,16 +109,15 @@ internal fun HandRaiseQueueSection( hand = hand, accountViewModel = accountViewModel, onApprove = { - // 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 { + // launchSigner runs on viewModelScope (+ Dispatchers.IO) + // and surfaces signer errors as toasts. Composition-scoped + // alternatives get cancelled when this row leaves the tree: + // approving flips `canSpeak()` true, the hand is filtered + // out, and if it was the last hand the section disposes + // entirely — killing the in-flight sign(...) before it ran. + accountViewModel.launchSigner { val template = RoomParticipantActions.setRole(event, hand.pubkey, ROLE.SPEAKER) - template?.let { runCatching { accountViewModel.account.signAndComputeBroadcast(it) } } + template?.let { accountViewModel.account.signAndComputeBroadcast(it) } } }, )