feat(nests): empty-stage hint, local hush, moderator/force-mute moderation
Three improvements stacked into one commit since they share files: #3 Empty-stage hint: StageGrid no longer disappears when nobody is on stage. The "Stage" label stays and a quiet "Waiting for speakers…" line keeps the strip visible so the room doesn't look broken before the first speaker arrives. #5 Local per-speaker hush: AudioPlayer gains a setVolume(Float) default-no-op method; AudioTrackPlayer composes mute and volume multiplicatively into AudioTrack.setVolume so a hushed stream stays silent regardless of mute state. NestViewModel exposes locallyHushed: ImmutableSet<String> in NestUiState plus setLocalHushed(pubkey, hushed) — applied at attach time so a re-subscribe of an already-hushed speaker stays silent. New "Hush this speaker" / "Restore this speaker" row in ParticipantHostActionsSheet, available to anyone (it affects only our own playback, nothing on the wire). #4 Host moderation gaps: wires Promote-to-Moderator using the existing RoomParticipantActions.setRole(ROLE.MODERATOR) builder — UI gap only, no protocol change. Adds a new AdminCommandEvent.Action.MUTE variant + AdminCommandEvent.forceMute(room, target) builder; the sheet emits it on "Force-mute speaker" and the AdminCommandsCollector dispatches incoming MUTE actions to a new NestViewModel.onForceMuted() that routes through the existing setMicMuted(true) path. Honor-based, same trust model as KICK — relays don't enforce signer authority, the client checks the signer is host or moderator on the active kind-30312.
This commit is contained in:
+5
-2
@@ -173,13 +173,16 @@ private fun AdminCommandsCollector(
|
||||
tags = mapOf("a" to listOf(roomATag), "p" to listOf(localPubkey)),
|
||||
)
|
||||
LocalCache.observeNewEvents<AdminCommandEvent>(filter).collect { cmd ->
|
||||
if (cmd.action() != AdminCommandEvent.Action.KICK) return@collect
|
||||
if (cmd.targetPubkey() != localPubkey) return@collect
|
||||
val signerIsAuthorised =
|
||||
cmd.pubKey == event.pubKey ||
|
||||
event.participants().any { it.pubKey == cmd.pubKey && (it.isHost() || it.isModerator()) }
|
||||
if (!signerIsAuthorised) return@collect
|
||||
viewModel.onKick()
|
||||
when (cmd.action()) {
|
||||
AdminCommandEvent.Action.KICK -> viewModel.onKick()
|
||||
AdminCommandEvent.Action.MUTE -> viewModel.onForceMuted()
|
||||
null -> Unit // unknown verb, ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+35
@@ -33,6 +33,7 @@ import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -82,6 +83,7 @@ internal fun ParticipantHostActionsSheet(
|
||||
event: MeetingSpaceEvent,
|
||||
accountViewModel: AccountViewModel,
|
||||
onDismiss: () -> Unit,
|
||||
nestViewModel: com.vitorpamplona.amethyst.commons.viewmodels.NestViewModel? = null,
|
||||
isLocalUserHost: Boolean = accountViewModel.account.signer.pubKey == event.pubKey,
|
||||
catalog: com.vitorpamplona.amethyst.commons.viewmodels.RoomSpeakerCatalog? = null,
|
||||
) {
|
||||
@@ -252,6 +254,28 @@ internal fun ParticipantHostActionsSheet(
|
||||
onDismiss()
|
||||
}
|
||||
|
||||
// Local hush — silences only this speaker in our own
|
||||
// playback. Available to anyone (not host-only) since it
|
||||
// affects nothing on the wire. Skip self because we don't
|
||||
// subscribe to our own broadcast loopback.
|
||||
if (nestViewModel != null && target != accountViewModel.account.signer.pubKey) {
|
||||
val nestUi by nestViewModel.uiState.collectAsState()
|
||||
val isHushedLocally = target in nestUi.locallyHushed
|
||||
ActionRow(
|
||||
text =
|
||||
stringRes(
|
||||
if (isHushedLocally) {
|
||||
R.string.nest_hush_local_restore
|
||||
} else {
|
||||
R.string.nest_hush_local
|
||||
},
|
||||
),
|
||||
) {
|
||||
nestViewModel.setLocalHushed(target, !isHushedLocally)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
// Host-only rows.
|
||||
if (isLocalUserHost && target != event.pubKey) {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
@@ -259,10 +283,21 @@ internal fun ParticipantHostActionsSheet(
|
||||
broadcast(RoomParticipantActions.setRole(event, target, ROLE.SPEAKER))
|
||||
onDismiss()
|
||||
}
|
||||
ActionRow(stringRes(R.string.nest_promote_moderator)) {
|
||||
broadcast(RoomParticipantActions.setRole(event, target, ROLE.MODERATOR))
|
||||
onDismiss()
|
||||
}
|
||||
ActionRow(stringRes(R.string.nest_demote_listener)) {
|
||||
broadcast(RoomParticipantActions.demoteToListener(event, target))
|
||||
onDismiss()
|
||||
}
|
||||
ActionRow(
|
||||
text = stringRes(R.string.nest_force_mute),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
) {
|
||||
broadcast(AdminCommandEvent.forceMute(roomATag, target))
|
||||
onDismiss()
|
||||
}
|
||||
ActionRow(
|
||||
text = stringRes(R.string.nest_kick_action),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
|
||||
+1
@@ -290,6 +290,7 @@ internal fun NestFullScreen(
|
||||
target = target,
|
||||
event = event,
|
||||
accountViewModel = accountViewModel,
|
||||
nestViewModel = viewModel,
|
||||
onDismiss = { hostMenuTarget = null },
|
||||
catalog = speakerCatalogs[target],
|
||||
)
|
||||
|
||||
+17
-1
@@ -120,7 +120,6 @@ internal fun StageGrid(
|
||||
connectingSpeakers: ImmutableSet<String> = persistentSetOf(),
|
||||
onLongPressParticipant: ((String) -> Unit)? = null,
|
||||
) {
|
||||
if (members.isEmpty()) return
|
||||
// Float currently-speaking members to the top so the listener can
|
||||
// see who they're hearing without scrolling. sortedBy is stable in
|
||||
// Kotlin — speakers retain relative order among themselves, as do
|
||||
@@ -137,6 +136,23 @@ internal fun StageGrid(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 6.dp),
|
||||
)
|
||||
if (members.isEmpty()) {
|
||||
// Keep the strip visible so the room doesn't look broken
|
||||
// when nobody is on stage yet — the title plus a quiet
|
||||
// hint signals "waiting state" without taking the chat /
|
||||
// audience tab's space below.
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 24.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = stringRes(R.string.nest_stage_empty),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
return@Column
|
||||
}
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Adaptive(STAGE_CELL_MIN),
|
||||
modifier = Modifier.fillMaxWidth().heightIn(max = STAGE_MAX_HEIGHT),
|
||||
|
||||
@@ -492,6 +492,7 @@
|
||||
<string name="meeting_room_in_space">In %1$s</string>
|
||||
<string name="meeting_room_in_space_unknown">a Nest</string>
|
||||
<string name="nest_stage">Stage</string>
|
||||
<string name="nest_stage_empty">Waiting for speakers…</string>
|
||||
<string name="nest_audience">Audience</string>
|
||||
<string name="nest_raise_hand">Raise hand</string>
|
||||
<string name="nest_lower_hand">Lower hand</string>
|
||||
@@ -569,8 +570,12 @@
|
||||
<string name="nest_hand_raise_queue_title">Raised hands</string>
|
||||
<string name="nest_hand_raise_approve">Approve</string>
|
||||
<string name="nest_promote_speaker">Promote to speaker</string>
|
||||
<string name="nest_promote_moderator">Promote to moderator</string>
|
||||
<string name="nest_demote_listener">Demote to listener</string>
|
||||
<string name="nest_force_mute">Force-mute speaker</string>
|
||||
<string name="nest_kick_action">Kick</string>
|
||||
<string name="nest_hush_local">Hush this speaker</string>
|
||||
<string name="nest_hush_local_restore">Restore this speaker</string>
|
||||
<string name="nest_participant_view_profile">View profile</string>
|
||||
<string name="nest_participant_zap">Send zap</string>
|
||||
<string name="nest_participant_zap_split_unsupported">Split zaps are not supported from inside a nest. Open the profile screen to send.</string>
|
||||
|
||||
Reference in New Issue
Block a user