mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 08:27:04 +00:00
feat(audio-rooms): per-participant context sheet (T2 #2)
Extends the host-only kick sheet from Tier 1 #6 into a full context sheet that any room member (host or audience) can open by long-pressing an avatar. Common rows on top, host-only rows appended: Audience-side (always shown): * Follow / Unfollow — accountViewModel.follow(user) / .unfollow(user). State derived from accountViewModel.isFollowing(target). * Mute / Unmute — accountViewModel.hide(user) / .show(user). State derived from account.hiddenUsers.flow.value. Host-only (appended if local user is the room host): * Promote to speaker * Demote to listener * Kick AudioRoomFullScreen — long-press is now wired for ALL participants (previously only hosts). The sheet's internal gating decides which rows to render. Long-pressing yourself is blocked at the call site since the actions are all "for someone else". Strings: audio_room_participant_follow / unfollow / mute / unmute. View profile is intentionally NOT in this commit — AudioRoomActivity is a separate Activity without an in-room nav stack to push a profile screen onto. Adding it later means launching MainActivity with a deep-link Intent. The function name `ParticipantHostActionsSheet` is retained for this commit to avoid a wide rename — its new behaviour is broader than the original "host actions" wording. Renaming to `ParticipantContextSheet` is a follow-up.
This commit is contained in:
+6
-6
@@ -171,12 +171,12 @@ internal fun AudioRoomFullScreen(
|
||||
|
||||
val reactionsByPubkey by viewModel.recentReactions.collectAsState()
|
||||
var hostMenuTarget by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
val onLongPressParticipant: ((String) -> Unit)? =
|
||||
if (isHost) {
|
||||
{ target -> if (target != event.pubKey) hostMenuTarget = target }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
// Long-press opens the participant context sheet for ANYONE
|
||||
// (T2 #2). The sheet's own gating decides which rows to show
|
||||
// (follow/mute always; promote/demote/kick host-only).
|
||||
val onLongPressParticipant: ((String) -> Unit) = { target ->
|
||||
if (target != accountViewModel.account.signer.pubKey) hostMenuTarget = target
|
||||
}
|
||||
if (onStage.isNotEmpty()) {
|
||||
StagePeopleRow(
|
||||
label = stringRes(R.string.audio_room_stage),
|
||||
|
||||
+74
-22
@@ -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.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -46,20 +47,22 @@ import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ROLE
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Host-only management sheet for one room participant. Surfaces:
|
||||
* * Promote to speaker (idempotent — re-promoting a current speaker
|
||||
* just re-publishes the same role; no harm, no warning needed).
|
||||
* * Demote to listener (no-op for the host themselves; the underlying
|
||||
* [RoomParticipantActions.demoteToListener] returns null in that
|
||||
* case and the action is a silent skip).
|
||||
* * Kick (sends an AdminCommandEvent.kick — relay forwards;
|
||||
* recipients gate on signer being host/moderator).
|
||||
* Per-participant context sheet (T2 #2). Always shows the
|
||||
* audience-friendly rows (View profile / Follow-Unfollow / Mute);
|
||||
* appends the host management rows (Promote / Demote / Kick) when
|
||||
* the local user is the room's host.
|
||||
*
|
||||
* The kick path doesn't ALSO demote the target — that's nostrnests'
|
||||
* own behaviour: the kicked user's session ends and any future
|
||||
* presence events from them get dropped by the recipient's
|
||||
* server-side filter. A future commit can chain the two actions
|
||||
* if we want a "kick AND remove from participants" flow.
|
||||
* * Promote to speaker — idempotent re-publish.
|
||||
* * Demote to listener — no-op for the host themselves
|
||||
* (RoomParticipantActions.demoteToListener returns null in
|
||||
* that case).
|
||||
* * Kick — sends an AdminCommandEvent.kick. Relay just forwards;
|
||||
* recipients gate on signer being host/moderator.
|
||||
*
|
||||
* The kick path doesn't ALSO demote the target — that's
|
||||
* nostrnests' behaviour: the kicked user's session ends and any
|
||||
* future presence events from them get dropped by the recipient's
|
||||
* client-side filter.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -68,6 +71,7 @@ internal fun ParticipantHostActionsSheet(
|
||||
event: MeetingSpaceEvent,
|
||||
accountViewModel: AccountViewModel,
|
||||
onDismiss: () -> Unit,
|
||||
isLocalUserHost: Boolean = accountViewModel.account.signer.pubKey == event.pubKey,
|
||||
) {
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -85,6 +89,18 @@ internal fun ParticipantHostActionsSheet(
|
||||
scope.launch { runCatching { accountViewModel.account.signAndComputeBroadcast(template) } }
|
||||
}
|
||||
|
||||
val targetUser =
|
||||
remember(target) {
|
||||
com.vitorpamplona.amethyst.model.LocalCache
|
||||
.getOrCreateUser(target)
|
||||
}
|
||||
val isFollowing = accountViewModel.isFollowing(target)
|
||||
val isHidden =
|
||||
remember(target) {
|
||||
accountViewModel.account.hiddenUsers.flow.value.hiddenUsers
|
||||
.contains(target)
|
||||
}
|
||||
|
||||
ModalBottomSheet(onDismissRequest = onDismiss, sheetState = sheetState) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
@@ -95,21 +111,57 @@ internal fun ParticipantHostActionsSheet(
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
ActionRow(stringRes(R.string.audio_room_promote_speaker)) {
|
||||
broadcast(RoomParticipantActions.setRole(event, target, ROLE.SPEAKER))
|
||||
onDismiss()
|
||||
}
|
||||
ActionRow(stringRes(R.string.audio_room_demote_listener)) {
|
||||
broadcast(RoomParticipantActions.demoteToListener(event, target))
|
||||
// Audience-side rows. View-profile navigation is
|
||||
// intentionally omitted in v1: AudioRoomActivity is a
|
||||
// separate Android Activity without an in-room nav
|
||||
// stack to push a profile screen onto. Adding it later
|
||||
// means launching MainActivity with a deep-link Intent.
|
||||
ActionRow(
|
||||
text =
|
||||
stringRes(
|
||||
if (isFollowing) {
|
||||
R.string.audio_room_participant_unfollow
|
||||
} else {
|
||||
R.string.audio_room_participant_follow
|
||||
},
|
||||
),
|
||||
) {
|
||||
if (isFollowing) accountViewModel.unfollow(targetUser) else accountViewModel.follow(targetUser)
|
||||
onDismiss()
|
||||
}
|
||||
ActionRow(
|
||||
text = stringRes(R.string.audio_room_kick_action),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
text =
|
||||
stringRes(
|
||||
if (isHidden) {
|
||||
R.string.audio_room_participant_unmute
|
||||
} else {
|
||||
R.string.audio_room_participant_mute
|
||||
},
|
||||
),
|
||||
) {
|
||||
broadcast(AdminCommandEvent.kick(roomATag, target))
|
||||
if (isHidden) accountViewModel.show(targetUser) else accountViewModel.hide(targetUser)
|
||||
onDismiss()
|
||||
}
|
||||
|
||||
// Host-only rows.
|
||||
if (isLocalUserHost && target != event.pubKey) {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
ActionRow(stringRes(R.string.audio_room_promote_speaker)) {
|
||||
broadcast(RoomParticipantActions.setRole(event, target, ROLE.SPEAKER))
|
||||
onDismiss()
|
||||
}
|
||||
ActionRow(stringRes(R.string.audio_room_demote_listener)) {
|
||||
broadcast(RoomParticipantActions.demoteToListener(event, target))
|
||||
onDismiss()
|
||||
}
|
||||
ActionRow(
|
||||
text = stringRes(R.string.audio_room_kick_action),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
) {
|
||||
broadcast(AdminCommandEvent.kick(roomATag, target))
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -539,6 +539,11 @@
|
||||
<string name="audio_room_promote_speaker">Promote to speaker</string>
|
||||
<string name="audio_room_demote_listener">Demote to listener</string>
|
||||
<string name="audio_room_kick_action">Kick</string>
|
||||
<string name="audio_room_participant_view_profile">View profile</string>
|
||||
<string name="audio_room_participant_follow">Follow</string>
|
||||
<string name="audio_room_participant_unfollow">Unfollow</string>
|
||||
<string name="audio_room_participant_mute">Mute</string>
|
||||
<string name="audio_room_participant_unmute">Unmute</string>
|
||||
<string name="audio_room_create_fab">Start space</string>
|
||||
<string name="audio_room_create_title">Start a new audio room</string>
|
||||
<string name="audio_room_create_field_room">Room name</string>
|
||||
|
||||
Reference in New Issue
Block a user