feat(nests): self highlight, floating reactions, kick drops p-tag, tap-self mute
#1 Local user's cell tints the username in colorScheme.primary so you can find yourself in a busy stage. Threads myPubkey through StageGrid and AudienceGrid; cells receive isSelf and pass MaterialTheme primary to UsernameDisplay's textColor. #2 SpeakerReactionOverlay moves from below the username (which reflowed the column when reactions arrived and shoved neighbors down) to inside the avatar Box, anchored bottom-end with a small outward offset so it overlaps the corner without colliding with the bottom-center mic badge. Cell height stays constant whether the speaker has 0 or 5 active reactions. #3 Kick now mirrors nostrnests' two-step path: the existing ephemeral kind-4312 ['action','kick'] kicks the target off the audio plane, followed by a re-published kind-30312 with their p-tag dropped so the participant grid stops rendering them. New RoomParticipantActions.removeParticipant builds the second event; refuses to drop the host (same protection as setRole demoting host). #7 Tap-to-mute on your own avatar. Self-cell receives onTapSelf which, when broadcasting, toggles BroadcastUiState.Broadcasting.isMuted via the existing setMicMuted path. Falls back to no-op when not broadcasting so a non-broadcasting tap doesn't surface a useless action — the avatar simply isn't tappable.
This commit is contained in:
+7
@@ -302,7 +302,14 @@ internal fun ParticipantHostActionsSheet(
|
||||
text = stringRes(R.string.nest_kick_action),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
) {
|
||||
// Two-step kick mirroring nostrnests' ProfileCard.tsx:
|
||||
// 1. ephemeral kind-4312 ["action","kick"] kicks the
|
||||
// user off the audio plane
|
||||
// 2. re-published kind-30312 with the target's p-tag
|
||||
// dropped removes them from the participant grid
|
||||
// so future presence events don't re-render them
|
||||
broadcast(AdminCommandEvent.kick(roomATag, target))
|
||||
broadcast(RoomParticipantActions.removeParticipant(event, target))
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
+22
@@ -90,6 +90,28 @@ internal object RoomParticipantActions {
|
||||
targetPubkey: String,
|
||||
): EventTemplate<MeetingSpaceEvent>? = setRole(original, targetPubkey, ROLE.PARTICIPANT)
|
||||
|
||||
/**
|
||||
* Drop [targetPubkey]'s p-tag from the room event entirely.
|
||||
* Mirrors nostrnests' kick follow-up (`updateRoomParticipant(_,
|
||||
* null)` after `kickUser` in `ProfileCard.tsx`): the kind-4312
|
||||
* boots them off the audio plane, this re-published kind-30312
|
||||
* keeps them out of the participant grid.
|
||||
*
|
||||
* Refuses to drop the host — same reasoning as [setRole]. Returns
|
||||
* null if the target isn't currently in the participant list (a
|
||||
* pure-audience kick has no list-mutation to do).
|
||||
*/
|
||||
fun removeParticipant(
|
||||
original: MeetingSpaceEvent,
|
||||
targetPubkey: String,
|
||||
): EventTemplate<MeetingSpaceEvent>? {
|
||||
val all = original.participants()
|
||||
val target = all.firstOrNull { it.pubKey == targetPubkey } ?: return null
|
||||
if (target.role.equals(ROLE.HOST.code, ignoreCase = true)) return null
|
||||
val remaining = all.filterNot { it.pubKey == targetPubkey }
|
||||
return rebuild(original, remaining, original.status() ?: StatusTag.STATUS.OPEN)
|
||||
}
|
||||
|
||||
private fun rebuild(
|
||||
original: MeetingSpaceEvent,
|
||||
participants: List<ParticipantTag>,
|
||||
|
||||
+11
@@ -223,6 +223,14 @@ internal fun NestFullScreen(
|
||||
summary = event.summary(),
|
||||
listenerCount = presences.size,
|
||||
)
|
||||
// Self-cell tap toggles mic-mute when broadcasting; null
|
||||
// when not broadcasting so the avatar falls back to the
|
||||
// default no-op tap (rather than offering a button that
|
||||
// does nothing).
|
||||
val onTapSelf: (() -> Unit)? =
|
||||
(ui.broadcast as? com.vitorpamplona.amethyst.commons.viewmodels.BroadcastUiState.Broadcasting)?.let { state ->
|
||||
{ viewModel.setMicMuted(!state.isMuted) }
|
||||
}
|
||||
StageGrid(
|
||||
members = participantGrid.onStage,
|
||||
speakingNow = ui.speakingNow,
|
||||
@@ -231,6 +239,8 @@ internal fun NestFullScreen(
|
||||
reactionsByPubkey = reactionsByPubkey,
|
||||
connectingSpeakers = ui.connectingSpeakers,
|
||||
onLongPressParticipant = onLongPressParticipant,
|
||||
myPubkey = myPubkey,
|
||||
onTapSelf = onTapSelf,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
NestTabRow(
|
||||
@@ -258,6 +268,7 @@ internal fun NestFullScreen(
|
||||
members = participantGrid.audience,
|
||||
accountViewModel = accountViewModel,
|
||||
onLongPressParticipant = onLongPressParticipant,
|
||||
myPubkey = myPubkey,
|
||||
modifier =
|
||||
Modifier
|
||||
.weight(1f)
|
||||
|
||||
+35
-6
@@ -119,6 +119,8 @@ internal fun StageGrid(
|
||||
reactionsByPubkey: Map<String, List<RoomReaction>> = emptyMap(),
|
||||
connectingSpeakers: ImmutableSet<String> = persistentSetOf(),
|
||||
onLongPressParticipant: ((String) -> Unit)? = null,
|
||||
myPubkey: String? = null,
|
||||
onTapSelf: (() -> Unit)? = null,
|
||||
) {
|
||||
// Float currently-speaking members to the top so the listener can
|
||||
// see who they're hearing without scrolling. sortedBy is stable in
|
||||
@@ -160,6 +162,7 @@ internal fun StageGrid(
|
||||
verticalArrangement = Arrangement.spacedBy(GRID_SPACING),
|
||||
) {
|
||||
items(items = sortedMembers, key = { it.pubkey }) { member ->
|
||||
val isSelf = myPubkey != null && member.pubkey == myPubkey
|
||||
MemberCell(
|
||||
member = member,
|
||||
avatarSize = STAGE_AVATAR,
|
||||
@@ -170,6 +173,8 @@ internal fun StageGrid(
|
||||
reactions = reactionsByPubkey[member.pubkey].orEmpty(),
|
||||
accountViewModel = accountViewModel,
|
||||
onLongPressParticipant = onLongPressParticipant,
|
||||
isSelf = isSelf,
|
||||
onTapSelf = if (isSelf) onTapSelf else null,
|
||||
modifier = Modifier.animateItem(),
|
||||
)
|
||||
}
|
||||
@@ -194,6 +199,7 @@ internal fun AudienceGrid(
|
||||
accountViewModel: AccountViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
onLongPressParticipant: ((String) -> Unit)? = null,
|
||||
myPubkey: String? = null,
|
||||
) {
|
||||
if (members.isEmpty()) {
|
||||
Box(
|
||||
@@ -235,6 +241,7 @@ internal fun AudienceGrid(
|
||||
reactions = emptyList(),
|
||||
accountViewModel = accountViewModel,
|
||||
onLongPressParticipant = onLongPressParticipant,
|
||||
isSelf = myPubkey != null && member.pubkey == myPubkey,
|
||||
modifier = Modifier.animateItem(),
|
||||
)
|
||||
}
|
||||
@@ -252,6 +259,8 @@ private fun MemberCell(
|
||||
reactions: List<RoomReaction>,
|
||||
accountViewModel: AccountViewModel,
|
||||
onLongPressParticipant: ((String) -> Unit)?,
|
||||
isSelf: Boolean = false,
|
||||
onTapSelf: (() -> Unit)? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val mutedRingColor = MaterialTheme.colorScheme.error
|
||||
@@ -292,6 +301,17 @@ private fun MemberCell(
|
||||
remember(member.pubkey, onLongPressParticipant) {
|
||||
onLongPressParticipant?.let { cb -> { hex: String -> cb(hex) } }
|
||||
}
|
||||
// Self-cell tap shortcut: tap your own avatar to toggle mic-mute
|
||||
// when broadcasting. For other cells we leave tap unhandled so
|
||||
// the existing nav-to-profile path stays consistent (and there's
|
||||
// nothing visually different from a non-clickable avatar).
|
||||
val onClick =
|
||||
if (isSelf && onTapSelf != null) {
|
||||
remember(onTapSelf) { { _: String -> onTapSelf() } }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val selfTint = if (isSelf) MaterialTheme.colorScheme.primary else Color.Unspecified
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier.fillMaxWidth().padding(vertical = 4.dp),
|
||||
@@ -302,6 +322,7 @@ private fun MemberCell(
|
||||
size = avatarSize,
|
||||
accountViewModel = accountViewModel,
|
||||
modifier = avatarModifier,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
)
|
||||
if (isConnecting) {
|
||||
@@ -330,19 +351,27 @@ private fun MemberCell(
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
)
|
||||
}
|
||||
// Reactions float over the avatar's bottom-right corner so
|
||||
// a 👏 burst no longer pushes the username down and reflows
|
||||
// neighbouring cells. The mic badge sits at BottomCenter,
|
||||
// so BottomEnd + a small outward offset keeps them clear.
|
||||
if (reactions.isNotEmpty()) {
|
||||
SpeakerReactionOverlay(
|
||||
reactions = reactions,
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.offset(x = 6.dp, y = 6.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
UsernameDisplay(
|
||||
baseUser = user,
|
||||
weight = Modifier.fillMaxWidth().padding(top = 4.dp),
|
||||
textColor = selfTint,
|
||||
textAlign = TextAlign.Center,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
if (reactions.isNotEmpty()) {
|
||||
SpeakerReactionOverlay(
|
||||
reactions = reactions,
|
||||
modifier = Modifier.padding(top = 2.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user