feat(nests): bigger avatars + clearer mute/speaking state in participant grid
100dp avatars across stage and audience tabs, names centered under each cell, and the avatar border now distinguishes actively speaking (green) from publishing-but-muted (red) at a glance — the existing mic-pill keeps its semantics underneath. UsernameDisplay gains an optional textAlign so the grid can opt in to centered labels without disturbing other call sites.
This commit is contained in:
@@ -31,6 +31,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||||
@@ -100,6 +101,7 @@ fun UsernameDisplay(
|
|||||||
weight: Modifier = Modifier,
|
weight: Modifier = Modifier,
|
||||||
fontWeight: FontWeight = FontWeight.Bold,
|
fontWeight: FontWeight = FontWeight.Bold,
|
||||||
textColor: Color = Color.Unspecified,
|
textColor: Color = Color.Unspecified,
|
||||||
|
textAlign: TextAlign? = null,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
) {
|
) {
|
||||||
val userMetadata by observeUserInfo(baseUser, accountViewModel)
|
val userMetadata by observeUserInfo(baseUser, accountViewModel)
|
||||||
@@ -107,9 +109,9 @@ fun UsernameDisplay(
|
|||||||
CrossfadeIfEnabled(targetState = userMetadata, modifier = weight, label = "UsernameDisplay", accountViewModel = accountViewModel) {
|
CrossfadeIfEnabled(targetState = userMetadata, modifier = weight, label = "UsernameDisplay", accountViewModel = accountViewModel) {
|
||||||
val name = it?.info?.bestName()
|
val name = it?.info?.bestName()
|
||||||
if (name != null) {
|
if (name != null) {
|
||||||
UserDisplay(name, it.tags, weight, fontWeight, textColor)
|
UserDisplay(name, it.tags, weight, fontWeight, textColor, textAlign)
|
||||||
} else {
|
} else {
|
||||||
NPubDisplay(baseUser, weight, fontWeight, textColor)
|
NPubDisplay(baseUser, weight, fontWeight, textColor, textAlign)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,6 +122,7 @@ private fun NPubDisplay(
|
|||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
fontWeight: FontWeight = FontWeight.Bold,
|
fontWeight: FontWeight = FontWeight.Bold,
|
||||||
textColor: Color = Color.Unspecified,
|
textColor: Color = Color.Unspecified,
|
||||||
|
textAlign: TextAlign? = null,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = remember { user.pubkeyDisplayHex() },
|
text = remember { user.pubkeyDisplayHex() },
|
||||||
@@ -128,6 +131,7 @@ private fun NPubDisplay(
|
|||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
color = textColor,
|
color = textColor,
|
||||||
|
textAlign = textAlign,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,11 +142,13 @@ private fun UserDisplay(
|
|||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
fontWeight: FontWeight = FontWeight.Bold,
|
fontWeight: FontWeight = FontWeight.Bold,
|
||||||
textColor: Color = Color.Unspecified,
|
textColor: Color = Color.Unspecified,
|
||||||
|
textAlign: TextAlign? = null,
|
||||||
) {
|
) {
|
||||||
CreateTextWithEmoji(
|
CreateTextWithEmoji(
|
||||||
text = bestDisplayName,
|
text = bestDisplayName,
|
||||||
tags = tags,
|
tags = tags,
|
||||||
fontWeight = fontWeight,
|
fontWeight = fontWeight,
|
||||||
|
textAlign = textAlign,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|||||||
+25
-15
@@ -51,6 +51,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
@@ -62,21 +63,22 @@ import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
|||||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size40dp
|
|
||||||
import kotlinx.collections.immutable.ImmutableSet
|
import kotlinx.collections.immutable.ImmutableSet
|
||||||
import kotlinx.collections.immutable.persistentSetOf
|
import kotlinx.collections.immutable.persistentSetOf
|
||||||
|
|
||||||
private val STAGE_CELL_MIN = 88.dp
|
private val STAGE_CELL_MIN = 112.dp
|
||||||
private val STAGE_AVATAR = 48.dp
|
private val STAGE_AVATAR = 100.dp
|
||||||
private val AUDIENCE_CELL_MIN = 76.dp
|
private val AUDIENCE_CELL_MIN = 112.dp
|
||||||
private val AUDIENCE_AVATAR = Size40dp
|
private val AUDIENCE_AVATAR = 100.dp
|
||||||
private val GRID_SPACING = 6.dp
|
private val GRID_SPACING = 6.dp
|
||||||
|
private val AVATAR_RING_WIDTH = 3.dp
|
||||||
|
|
||||||
// Two rows visible by default. Each cell is roughly avatar + name +
|
// Two rows visible by default. Each cell is roughly avatar + name +
|
||||||
// reaction headroom, so this lifts to ~200dp on most phones — tall
|
// reaction headroom; with a 100dp avatar one row lifts to ~140dp, so
|
||||||
// enough to show 6-8 speakers without scrolling but small enough to
|
// two rows + label/padding lands near 320dp — tall enough to show a
|
||||||
// leave the chat / audience tab the majority of the viewport.
|
// handful of speakers without scrolling but small enough to leave the
|
||||||
private val STAGE_MAX_HEIGHT = 220.dp
|
// chat / audience tab the majority of the viewport.
|
||||||
|
private val STAGE_MAX_HEIGHT = 320.dp
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vertical adaptive grid for the on-stage section. Used as the
|
* Vertical adaptive grid for the on-stage section. Used as the
|
||||||
@@ -194,14 +196,21 @@ private fun MemberCell(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
onLongPressParticipant: ((String) -> Unit)?,
|
onLongPressParticipant: ((String) -> Unit)?,
|
||||||
) {
|
) {
|
||||||
val ringColor = MaterialTheme.colorScheme.primary
|
val mutedRingColor = MaterialTheme.colorScheme.error
|
||||||
val avatarModifier =
|
// Tri-state ring around the avatar so a glance distinguishes:
|
||||||
|
// - green ring: actively speaking right now
|
||||||
|
// - red ring: on stage and broadcasting but mic flagged muted
|
||||||
|
// - no ring: idle, audience, or unknown mute state
|
||||||
|
val ringColor =
|
||||||
when {
|
when {
|
||||||
isSpeaking && member.absent -> Modifier.border(2.dp, ringColor, CircleShape).then(Modifier.alpha(0.5f))
|
isSpeaking -> NEST_SPEAKING_COLOR
|
||||||
isSpeaking -> Modifier.border(2.dp, ringColor, CircleShape)
|
showMicBadge && member.publishing && member.muted == true -> mutedRingColor
|
||||||
member.absent -> Modifier.alpha(0.5f)
|
else -> null
|
||||||
else -> Modifier
|
|
||||||
}
|
}
|
||||||
|
val avatarModifier =
|
||||||
|
Modifier
|
||||||
|
.let { if (ringColor != null) it.border(AVATAR_RING_WIDTH, ringColor, CircleShape) else it }
|
||||||
|
.let { if (member.absent) it.alpha(0.5f) else it }
|
||||||
val user =
|
val user =
|
||||||
remember(member.pubkey) {
|
remember(member.pubkey) {
|
||||||
com.vitorpamplona.amethyst.model.LocalCache
|
com.vitorpamplona.amethyst.model.LocalCache
|
||||||
@@ -251,6 +260,7 @@ private fun MemberCell(
|
|||||||
UsernameDisplay(
|
UsernameDisplay(
|
||||||
baseUser = user,
|
baseUser = user,
|
||||||
weight = Modifier.fillMaxWidth().padding(top = 4.dp),
|
weight = Modifier.fillMaxWidth().padding(top = 4.dp),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
)
|
)
|
||||||
if (reactions.isNotEmpty()) {
|
if (reactions.isNotEmpty()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user