Display user status next to display names across rooms and profiles

This commit is contained in:
ganfra
2026-06-24 17:59:08 +02:00
parent 18034e02a2
commit 06a903718b
46 changed files with 386 additions and 89 deletions
@@ -19,6 +19,7 @@ import androidx.compose.runtime.Immutable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.components.avatar.AvatarData
@@ -50,7 +51,7 @@ fun CheckableUserRow(
UserRow(
modifier = rowModifier,
avatarData = data.avatarData,
name = data.name,
name = AnnotatedString(data.name),
subtext = data.subtext,
enabled = enabled,
)
@@ -0,0 +1,62 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.ui.components
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.theme.components.Text
/**
* Component to display side by side a display name and an optional status.
*/
@Composable
fun DisplayNameWithStatus(
name: String,
status: String?,
style: TextStyle,
modifier: Modifier = Modifier,
nameColor: Color = Color.Unspecified,
nameFontStyle: FontStyle? = null,
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = name,
modifier = Modifier
.weight(1f, fill = false)
.clipToBounds(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = style,
color = nameColor,
fontStyle = nameFontStyle,
)
if (status != null) {
Spacer(Modifier.width(4.dp))
Text(
text = status,
style = style,
color = ElementTheme.colors.textSecondary,
)
}
}
}
@@ -32,6 +32,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.matrix.ui.model.getBestName
import io.element.android.libraries.matrix.ui.model.toEmojiText
@Composable
fun MatrixUserHeader(
@@ -58,13 +59,11 @@ fun MatrixUserHeader(
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
// Name
Text(
modifier = Modifier.clipToBounds(),
text = matrixUser.getBestName(),
maxLines = 1,
style = ElementTheme.typography.fontHeadingMdRegular,
overflow = TextOverflow.Ellipsis,
color = ElementTheme.colors.textPrimary,
DisplayNameWithStatus(
name = matrixUser.getBestName(),
status = matrixUser.displayedStatus?.toEmojiText(),
nameColor = ElementTheme.colors.textPrimary,
style = ElementTheme.typography.fontHeadingMdRegular
)
// Id
if (matrixUser.displayName.isNullOrEmpty().not()) {
@@ -21,6 +21,7 @@ import io.element.android.libraries.designsystem.preview.USER_NAME_SUSIE
import io.element.android.libraries.designsystem.preview.USER_NAME_VICTOR
import io.element.android.libraries.designsystem.preview.USER_NAME_WALTER
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.api.user.MatrixUser
open class MatrixUserProvider : PreviewParameterProvider<MatrixUser> {
@@ -28,6 +29,7 @@ open class MatrixUserProvider : PreviewParameterProvider<MatrixUser> {
get() = sequenceOf(
aMatrixUser(),
aMatrixUser(displayName = null),
aMatrixUser(displayedStatus = DisplayedStatus.InCall(0L) ),
)
}
@@ -43,10 +45,12 @@ fun aMatrixUser(
id: String? = null,
displayName: String? = USER_NAME_ALICE,
avatarUrl: String? = null,
displayedStatus: DisplayedStatus? = null,
) = MatrixUser(
userId = UserId(id ?: "@${displayName?.lowercase()?.replace(" ", "_") ?: "id"}:server.org"),
displayName = displayName,
avatarUrl = avatarUrl,
displayedStatus = displayedStatus,
)
fun aMatrixUserList() = listOf(
@@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.ui.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import io.element.android.compound.theme.ElementTheme
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -19,6 +20,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.matrix.ui.model.getBestName
import io.element.android.libraries.matrix.ui.model.toEmojiText
@Composable
fun MatrixUserRow(
@@ -29,7 +31,14 @@ fun MatrixUserRow(
trailingContent: @Composable (() -> Unit)? = null,
) = UserRow(
avatarData = matrixUser.getAvatarData(avatarSize),
name = matrixUser.getBestName(),
nameContent = {
DisplayNameWithStatus(
name = matrixUser.getBestName(),
status = matrixUser.displayedStatus?.toEmojiText(),
nameColor = ElementTheme.colors.textPrimary,
style = ElementTheme.typography.fontBodyLgRegular,
)
},
subtext = if (matrixUser.displayName.isNullOrEmpty()) null else matrixUser.userId.value,
modifier = modifier,
verticalSpaceWidth = verticalSpaceWidth,
@@ -18,6 +18,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -30,7 +31,36 @@ import io.element.android.libraries.designsystem.theme.components.Text
@Composable
internal fun UserRow(
avatarData: AvatarData,
name: String,
name: AnnotatedString,
subtext: String?,
modifier: Modifier = Modifier,
enabled: Boolean = true,
verticalSpaceWidth: Dp = 12.dp,
trailingContent: @Composable (() -> Unit)? = null,
) {
UserRow(
avatarData = avatarData,
nameContent = {
Text(
modifier = Modifier.clipToBounds(),
text = name,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = ElementTheme.typography.fontBodyLgRegular,
)
},
subtext = subtext,
modifier = modifier,
enabled = enabled,
verticalSpaceWidth = verticalSpaceWidth,
trailingContent = trailingContent,
)
}
@Composable
internal fun UserRow(
avatarData: AvatarData,
nameContent: @Composable () -> Unit,
subtext: String?,
modifier: Modifier = Modifier,
enabled: Boolean = true,
@@ -52,14 +82,7 @@ internal fun UserRow(
modifier = Modifier.weight(1f),
) {
// Name
Text(
modifier = Modifier.clipToBounds(),
text = name,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = if (enabled) ElementTheme.colors.textPrimary else ElementTheme.colors.textDisabled,
style = ElementTheme.typography.fontBodyLgRegular,
)
nameContent()
// Id
subtext?.let {
Text(
@@ -13,6 +13,7 @@ import dev.zacsweers.metro.SingleIn
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.drop
@@ -31,4 +32,9 @@ class RoomMemberProfilesCache {
fun getDisplayName(userId: UserId): String? {
return cache.value[userId]?.disambiguatedDisplayName
}
fun getDisplayedStatus(userId: UserId): DisplayedStatus? {
return cache.value[userId]?.displayedStatus
}
}
@@ -167,4 +167,5 @@ fun aProfileDetailsReady(
displayName = displayName,
displayNameAmbiguous = displayNameAmbiguous,
avatarUrl = avatarUrl,
displayedStatus = null,
)
@@ -24,6 +24,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails
import io.element.android.libraries.matrix.ui.model.toText
// https://www.figma.com/file/Ni6Ii8YKtmXCKYNE90cC67/Timeline-(new)?type=design&node-id=917-80169&mode=design&t=A0CJCBbMqR8NOwUQ-0
@Composable
@@ -54,6 +55,19 @@ fun SenderName(
SecondaryText(text = senderId.value, mode = senderNameMode)
}
}
val userStatus = senderProfile.displayedStatus
if (senderNameMode is SenderNameMode.Timeline && userStatus != null) {
Text(
modifier = Modifier
.clipToBounds()
.alignByBaseline(),
text = userStatus.toText(),
style = ElementTheme.typography.fontBodyMdMedium,
color = ElementTheme.colors.textSecondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
}
@@ -74,13 +88,16 @@ private fun RowScope.MainText(
SenderNameMode.ActionList,
SenderNameMode.Reply -> Modifier
}
val color = when (mode) {
is SenderNameMode.Timeline -> mode.mainColor
SenderNameMode.ActionList,
SenderNameMode.Reply -> ElementTheme.colors.textPrimary
}
Text(
modifier = modifier.clipToBounds(),
modifier = modifier
.clipToBounds()
.weight(1f, fill = false),
text = text,
style = style,
color = color,
@@ -12,6 +12,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails
import io.element.android.libraries.matrix.api.user.DisplayedStatus
data class SenderNameData(
val userId: UserId,
@@ -47,12 +48,14 @@ open class SenderNameDataProvider : PreviewParameterProvider<SenderNameData> {
private fun aSenderNameData(
senderNameMode: SenderNameMode,
displayNameAmbiguous: Boolean = false,
displayedStatus: DisplayedStatus? = null,
) = SenderNameData(
userId = UserId("@alice:${senderNameMode.javaClass.simpleName.lowercase()}"),
profileDetails = ProfileDetails.Ready(
displayName = "Alice ${senderNameMode.javaClass.simpleName}",
displayNameAmbiguous = displayNameAmbiguous,
avatarUrl = null
avatarUrl = null,
displayedStatus = displayedStatus,
),
senderNameMode = senderNameMode,
)
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.ui.model
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.ui.R
fun DisplayedStatus.toEmojiText(): String = when (this) {
is DisplayedStatus.UserSet -> status.emoji
is DisplayedStatus.InCall -> "🎧"
}
@Composable
fun DisplayedStatus.toText(): String = when (this) {
is DisplayedStatus.UserSet -> "${status.emoji} ${status.text}"
is DisplayedStatus.InCall -> "🎧 ${stringResource(R.string.common_user_status_on_a_call)}"
}
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2026 Element Creations Ltd.
~
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
~ Please see LICENSE files in the repository root for full details.
-->
<resources>
<string name="common_user_status_on_a_call">On a call</string>
</resources>