Display the user status emoji beside the avatar

This commit is contained in:
ganfra
2026-09-30 17:58:03 +02:00
parent df81f5c0f7
commit e50e6e599e
@@ -12,6 +12,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.WindowInsetsSides
@@ -43,18 +44,10 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.appconfig.RoomListConfig import io.element.android.appconfig.RoomListConfig
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
@@ -296,11 +289,15 @@ private fun NavigationIcon(
onAccountSwitch: (SessionId) -> Unit, onAccountSwitch: (SessionId) -> Unit,
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
// Reserve the status emoji slot for all the accounts, else the width of the navigation icon,
// and so the position of the title, would change while scrolling the pager.
val reserveStatusEmojiSlot = currentUserAndNeighbors.any { it.displayedStatus != null }
if (currentUserAndNeighbors.size == 1) { if (currentUserAndNeighbors.size == 1) {
AccountIcon( AccountIcon(
matrixUser = currentUserAndNeighbors.single(), matrixUser = currentUserAndNeighbors.single(),
isCurrentAccount = true, isCurrentAccount = true,
showAvatarIndicator = showAvatarIndicator, showAvatarIndicator = showAvatarIndicator,
reserveStatusEmojiSlot = reserveStatusEmojiSlot,
onClick = onClick, onClick = onClick,
) )
} else { } else {
@@ -321,6 +318,7 @@ private fun NavigationIcon(
matrixUser = currentUserAndNeighbors[page], matrixUser = currentUserAndNeighbors[page],
isCurrentAccount = page == 1, isCurrentAccount = page == 1,
showAvatarIndicator = page == 1 && showAvatarIndicator, showAvatarIndicator = page == 1 && showAvatarIndicator,
reserveStatusEmojiSlot = reserveStatusEmojiSlot,
onClick = if (page == 1) { onClick = if (page == 1) {
onClick onClick
} else { } else {
@@ -336,95 +334,69 @@ private fun AccountIcon(
matrixUser: MatrixUser, matrixUser: MatrixUser,
isCurrentAccount: Boolean, isCurrentAccount: Boolean,
showAvatarIndicator: Boolean, showAvatarIndicator: Boolean,
reserveStatusEmojiSlot: Boolean,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val testTag = if (isCurrentAccount) Modifier.testTag(TestTags.homeScreenSettings) else Modifier val testTag = if (isCurrentAccount) Modifier.testTag(TestTags.homeScreenSettings) else Modifier
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
Box( Row(
modifier = modifier modifier = modifier,
.then(testTag) verticalAlignment = Alignment.CenterVertically,
.minimumInteractiveComponentSize()
.clickable(
interactionSource = interactionSource,
onClick = onClick,
indication = ripple(bounded = false),
),
contentAlignment = Alignment.Center,
) { ) {
val avatarData by remember(matrixUser) { Box(
derivedStateOf { modifier = Modifier
matrixUser.getAvatarData(size = AvatarSize.CurrentUserTopBar) .then(testTag)
.minimumInteractiveComponentSize()
.clickable(
interactionSource = interactionSource,
onClick = onClick,
indication = ripple(bounded = false),
),
contentAlignment = Alignment.Center,
) {
val avatarData by remember(matrixUser) {
derivedStateOf {
matrixUser.getAvatarData(size = AvatarSize.CurrentUserTopBar)
}
}
Avatar(
avatarData = avatarData,
avatarType = AvatarType.User,
contentDescription = if (isCurrentAccount) {
if (showAvatarIndicator) {
stringResource(CommonStrings.a11y_settings_with_required_action)
} else {
stringResource(CommonStrings.common_settings)
}
} else {
null
},
)
if (showAvatarIndicator) {
RedIndicatorAtom(
modifier = Modifier.align(Alignment.TopEnd)
)
} }
} }
val statusEmoji = matrixUser.displayedStatus?.toEmojiText() if (reserveStatusEmojiSlot) {
val avatarModifier = if (statusEmoji != null) { StatusEmoji(
Modifier.eraseStatusEmojiBackground( emoji = matrixUser.displayedStatus?.toEmojiText(),
parentSize = AvatarSize.CurrentUserTopBar.dp, modifier = Modifier.offset(x = statusEmojiStartOffset),
layoutDirection = LocalLayoutDirection.current,
)
} else {
Modifier
}
Avatar(
avatarData = avatarData,
avatarType = AvatarType.User,
modifier = avatarModifier,
contentDescription = if (isCurrentAccount) {
if (showAvatarIndicator) {
stringResource(CommonStrings.a11y_settings_with_required_action)
} else {
stringResource(CommonStrings.common_settings)
}
} else {
null
},
)
if (statusEmoji != null) {
StatusEmojiBadge(
emoji = statusEmoji,
modifier = Modifier.align(Alignment.BottomEnd),
)
}
if (showAvatarIndicator) {
RedIndicatorAtom(
modifier = Modifier.align(Alignment.TopEnd)
) )
} }
} }
} }
private val statusEmojiBadgeSize = 20.dp private val statusEmojiSize = 20.dp
private val statusEmojiBadgeRadius = statusEmojiBadgeSize / 2
private val statusEmojiBadgeOffset = 8.dp
private fun Modifier.eraseStatusEmojiBackground( // The avatar is centered inside a 48dp touch target, so there is already 8dp of free space
parentSize: Dp, // on its trailing side. Compensate it to end up with a 4dp gap after the avatar itself.
layoutDirection: LayoutDirection, private val statusEmojiStartOffset = 4.dp - (48.dp - AvatarSize.CurrentUserTopBar.dp) / 2
): Modifier = this
.graphicsLayer {
compositingStrategy = CompositingStrategy.Offscreen
}
.drawWithContent {
drawContent()
drawCircle(
color = Color.Black,
center = Offset(
x = if (layoutDirection == LayoutDirection.Ltr) {
(parentSize - statusEmojiBadgeRadius + statusEmojiBadgeOffset).toPx()
} else {
(statusEmojiBadgeRadius - statusEmojiBadgeOffset).toPx()
},
y = size.height - statusEmojiBadgeRadius.toPx(),
),
radius = statusEmojiBadgeRadius.toPx(),
blendMode = BlendMode.Clear,
)
}
@Composable @Composable
private fun StatusEmojiBadge( private fun StatusEmoji(
emoji: String, emoji: String?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val density = LocalDensity.current val density = LocalDensity.current
@@ -433,16 +405,16 @@ private fun StatusEmojiBadge(
Density(density = density.density, fontScale = 1f) Density(density = density.density, fontScale = 1f)
} }
Box( Box(
modifier = modifier modifier = modifier.size(statusEmojiSize),
.offset(x = statusEmojiBadgeOffset)
.size(statusEmojiBadgeSize),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
CompositionLocalProvider(LocalDensity provides fixedFontScaleDensity) { if (emoji != null) {
Text( CompositionLocalProvider(LocalDensity provides fixedFontScaleDensity) {
text = emoji, Text(
style = ElementTheme.typography.fontBodyMdRegular, text = emoji,
) style = ElementTheme.typography.fontBodyMdRegular,
)
}
} }
} }
} }