change (room avatar) : use TextAvatar from InitialLetterAvatar

This commit is contained in:
ganfra
2025-06-13 18:34:55 +02:00
parent 08bbd7219c
commit f6dc8da6ad
4 changed files with 24 additions and 44 deletions

View File

@@ -370,7 +370,7 @@ fun Modifier.avatarBloom(
val initialsBitmap = initialsBitmap(
width = BloomDefaults.ENCODE_SIZE_PX.toDp(),
height = BloomDefaults.ENCODE_SIZE_PX.toDp(),
text = avatarData.initial,
text = avatarData.initialLetter,
textColor = avatarColors.foreground,
backgroundColor = avatarColors.background,
)

View File

@@ -7,11 +7,9 @@
package io.element.android.libraries.designsystem.components.avatar
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
@@ -21,21 +19,16 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImagePainter
import coil3.compose.SubcomposeAsyncImage
import coil3.compose.SubcomposeAsyncImageContent
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.text.toSp
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.CommonDrawables
import timber.log.Timber
@@ -50,21 +43,18 @@ fun Avatar(
// If true, will show initials even if avatarData.url is not null
hideImage: Boolean = false,
) {
val commonModifier = modifier
.size(forcedAvatarSize ?: avatarData.size.dp)
.clip(CircleShape)
if (avatarData.url.isNullOrBlank() || hideImage) {
InitialsAvatar(
InitialLetterAvatar(
avatarData = avatarData,
forcedAvatarSize = forcedAvatarSize,
modifier = commonModifier,
modifier = modifier,
contentDescription = contentDescription,
)
} else {
ImageAvatar(
avatarData = avatarData,
forcedAvatarSize = forcedAvatarSize,
modifier = commonModifier,
modifier = modifier,
contentDescription = contentDescription,
)
}
@@ -77,11 +67,14 @@ private fun ImageAvatar(
modifier: Modifier = Modifier,
contentDescription: String? = null,
) {
val size = forcedAvatarSize ?: avatarData.size.dp
SubcomposeAsyncImage(
model = avatarData,
contentDescription = contentDescription,
contentScale = ContentScale.Crop,
modifier = modifier
.requiredSize(size)
.clip(CircleShape)
) {
val collectedState by painter.state.collectAsState()
when (val state = collectedState) {
@@ -90,13 +83,13 @@ private fun ImageAvatar(
SideEffect {
Timber.e(state.result.throwable, "Error loading avatar $state\n${state.result}")
}
InitialsAvatar(
InitialLetterAvatar(
avatarData = avatarData,
forcedAvatarSize = forcedAvatarSize,
contentDescription = contentDescription,
)
}
else -> InitialsAvatar(
else -> InitialLetterAvatar(
avatarData = avatarData,
forcedAvatarSize = forcedAvatarSize,
contentDescription = contentDescription,
@@ -106,33 +99,20 @@ private fun ImageAvatar(
}
@Composable
private fun InitialsAvatar(
private fun InitialLetterAvatar(
avatarData: AvatarData,
forcedAvatarSize: Dp?,
contentDescription: String?,
modifier: Modifier = Modifier,
) {
val avatarColors = AvatarColorsProvider.provide(avatarData.id)
Box(
modifier.background(color = avatarColors.background)
) {
val fontSize = (forcedAvatarSize ?: avatarData.size.dp).toSp() / 2
val originalFont = ElementTheme.typography.fontHeadingMdBold
val ratio = fontSize.value / originalFont.fontSize.value
val lineHeight = originalFont.lineHeight * ratio
Text(
modifier = Modifier
.clearAndSetSemantics {
contentDescription?.let {
this.contentDescription = it
}
}
.align(Alignment.Center),
text = avatarData.initial,
style = originalFont.copy(fontSize = fontSize, lineHeight = lineHeight, letterSpacing = 0.sp),
color = avatarColors.foreground,
)
}
TextAvatar(
text = avatarData.initialLetter,
size = forcedAvatarSize ?: avatarData.size.dp,
colors = avatarColors,
contentDescription = contentDescription,
modifier = modifier
)
}
@Preview(group = PreviewGroup.Avatars)

View File

@@ -18,7 +18,7 @@ data class AvatarData(
val url: String? = null,
val size: AvatarSize,
) {
val initial by lazy {
val initialLetter by lazy {
// For roomIds, use "#" as initial
(name?.takeIf { it.isNotBlank() } ?: id.takeIf { !it.startsWith("!") } ?: "#")
.let { dn ->

View File

@@ -14,30 +14,30 @@ class AvatarDataTest {
@Test
fun `initial with text should get the first char, uppercased`() {
val data = AvatarData("id", "test", null, AvatarSize.InviteSender)
assertThat(data.initial).isEqualTo("T")
assertThat(data.initialLetter).isEqualTo("T")
}
@Test
fun `initial with leading whitespace should get the first non-whitespace char, uppercased`() {
val data = AvatarData("id", " test", null, AvatarSize.InviteSender)
assertThat(data.initial).isEqualTo("T")
assertThat(data.initialLetter).isEqualTo("T")
}
@Test
fun `initial with long emoji should get the full emoji`() {
val data = AvatarData("id", "\uD83C\uDFF3\uFE0F\u200D\uD83C\uDF08 Test", null, AvatarSize.InviteSender)
assertThat(data.initial).isEqualTo("\uD83C\uDFF3\uFE0F\u200D\uD83C\uDF08")
assertThat(data.initialLetter).isEqualTo("\uD83C\uDFF3\uFE0F\u200D\uD83C\uDF08")
}
@Test
fun `initial with short emoji should get the emoji`() {
val data = AvatarData("id", "✂ Test", null, AvatarSize.InviteSender)
assertThat(data.initial).isEqualTo("")
assertThat(data.initialLetter).isEqualTo("")
}
@Test
fun `initial with a single letter should take that letter`() {
val data = AvatarData("id", "T", null, AvatarSize.InviteSender)
assertThat(data.initial).isEqualTo("T")
assertThat(data.initialLetter).isEqualTo("T")
}
}