Introduce AvatarType to be able to render space avatars

This commit is contained in:
Benoit Marty
2025-06-23 17:08:14 +02:00
parent d524c23b8d
commit 8df920a3ac
22 changed files with 337 additions and 78 deletions

View File

@@ -40,6 +40,7 @@ import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtomSize
import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.modifiers.clearFocusOnTap
@@ -219,6 +220,7 @@ private fun RoomNameWithAvatar(
) {
UnsavedAvatar(
avatarUri = avatarUri,
avatarType = AvatarType.Room(),
modifier = Modifier.clickable(onClick = onAvatarClick),
)

View File

@@ -53,6 +53,7 @@ import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.button.SuperButton
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
@@ -92,7 +93,11 @@ fun JoinRoomView(
vertical = 32.dp
),
topBar = {
JoinRoomTopBar(contentState = state.contentState, onBackClick = onBackClick)
JoinRoomTopBar(
contentState = state.contentState,
hideAvatarImage = state.hideAvatarsImages,
onBackClick = onBackClick,
)
},
content = {
JoinRoomContent(
@@ -490,7 +495,11 @@ private fun DefaultLoadedContent(
RoomPreviewOrganism(
modifier = modifier,
avatar = {
Avatar(contentState.avatarData(AvatarSize.RoomHeader), hideImage = hideAvatarImage)
Avatar(
contentState.avatarData(AvatarSize.RoomHeader),
hideImage = hideAvatarImage,
avatarType = AvatarType.Room(),
)
},
title = {
if (contentState.name != null) {
@@ -545,6 +554,7 @@ private fun DefaultLoadedContent(
@Composable
private fun JoinRoomTopBar(
contentState: ContentState,
hideAvatarImage: Boolean,
onBackClick: () -> Unit,
) {
TopAppBar(
@@ -561,7 +571,11 @@ private fun JoinRoomTopBar(
modifier = titleModifier,
verticalAlignment = Alignment.CenterVertically
) {
Avatar(avatarData = contentState.avatarData(AvatarSize.TimelineRoom))
Avatar(
avatarData = contentState.avatarData(AvatarSize.TimelineRoom),
hideImage = hideAvatarImage,
avatarType = AvatarType.Room(),
)
Text(
modifier = Modifier.padding(horizontal = 8.dp),
text = contentState.name,

View File

@@ -83,8 +83,9 @@ import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorVi
import io.element.android.features.roomcall.api.RoomCallState
import io.element.android.libraries.androidutils.ui.hideKeyboard
import io.element.android.libraries.designsystem.atomic.molecules.ComposerAlertMolecule
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.RoomAvatar
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.preview.ElementPreview
@@ -563,10 +564,12 @@ private fun RoomAvatarAndNameRow(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically
) {
RoomAvatar(
Avatar(
avatarData = roomAvatar,
heroes = heroes,
isTombstoned = isTombstoned,
avatarType = AvatarType.Room(
heroes = heroes,
isTombstoned = isTombstoned,
),
)
Text(
modifier = Modifier.padding(horizontal = 8.dp),

View File

@@ -27,6 +27,7 @@ import io.element.android.features.messages.impl.R
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.components.avatar.anAvatarData
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@@ -98,6 +99,11 @@ private fun SuggestionItemView(
is ResolvedSuggestion.Member -> suggestion.roomMember.getAvatarData(avatarSize)
is ResolvedSuggestion.Alias -> suggestion.getAvatarData(avatarSize)
}
val avatarType = when (suggestion) {
is ResolvedSuggestion.Alias -> AvatarType.Room()
ResolvedSuggestion.AtRoom,
is ResolvedSuggestion.Member -> AvatarType.User
}
val title = when (suggestion) {
is ResolvedSuggestion.AtRoom -> stringResource(R.string.screen_room_mentions_at_room_title)
is ResolvedSuggestion.Member -> suggestion.roomMember.displayName
@@ -109,7 +115,12 @@ private fun SuggestionItemView(
is ResolvedSuggestion.Alias -> suggestion.roomAlias.value
}
Avatar(avatarData = avatarData, modifier = Modifier.padding(start = 16.dp, top = 12.dp, bottom = 12.dp))
Avatar(
avatarData = avatarData,
avatarType = avatarType,
modifier = Modifier.padding(start = 16.dp, top = 12.dp, bottom = 12.dp),
)
Column(
modifier = Modifier

View File

@@ -16,7 +16,8 @@ import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.preferences.impl.R
import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.avatar.RoomAvatar
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
@@ -97,9 +98,11 @@ fun EditDefaultNotificationSettingView(
Text(text = subtitle)
},
leadingContent = ListItemContent.Custom {
RoomAvatar(
Avatar(
avatarData = summary.avatarData,
heroes = summary.heroesAvatar,
avatarType = AvatarType.Room(
heroes = summary.heroesAvatar,
),
)
},
onClick = {

View File

@@ -32,6 +32,7 @@ import io.element.android.features.preferences.impl.R
import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.modifiers.clearFocusOnTap
import io.element.android.libraries.designsystem.preview.ElementPreview
@@ -101,6 +102,7 @@ fun EditUserProfileView(
displayName = state.displayName,
avatarUrl = state.userAvatarUrl,
avatarSize = AvatarSize.EditProfileDetails,
avatarType = AvatarType.User,
onAvatarClick = { onAvatarClick() },
modifier = Modifier.align(Alignment.CenterHorizontally),
)

View File

@@ -48,10 +48,11 @@ import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
import io.element.android.libraries.designsystem.atomic.atoms.MatrixBadgeAtom
import io.element.android.libraries.designsystem.atomic.molecules.MatrixBadgeRowMolecule
import io.element.android.libraries.designsystem.components.ClickableLinkText
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.components.avatar.DmAvatars
import io.element.android.libraries.designsystem.components.avatar.RoomAvatar
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.button.MainActionButton
import io.element.android.libraries.designsystem.components.list.ListItemContent
@@ -399,12 +400,14 @@ private fun RoomHeaderSection(
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
RoomAvatar(
Avatar(
avatarData = AvatarData(roomId.value, roomName, avatarUrl, AvatarSize.RoomHeader),
heroes = heroes.map { user ->
user.getAvatarData(size = AvatarSize.RoomHeader)
}.toPersistentList(),
isTombstoned = isTombstoned,
avatarType = AvatarType.Room(
heroes = heroes.map { user ->
user.getAvatarData(size = AvatarSize.RoomHeader)
}.toPersistentList(),
isTombstoned = isTombstoned,
),
modifier = Modifier
.clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) }
.testTag(TestTags.roomDetailAvatar)

View File

@@ -34,6 +34,7 @@ import io.element.android.features.roomdetails.impl.R
import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.modifiers.clearFocusOnTap
import io.element.android.libraries.designsystem.preview.ElementPreview
@@ -103,6 +104,7 @@ fun RoomDetailsEditView(
displayName = state.roomRawName,
avatarUrl = state.roomAvatarUrl,
avatarSize = AvatarSize.EditRoomDetails,
avatarType = AvatarType.Room(),
onAvatarClick = ::onAvatarClick,
modifier = Modifier.fillMaxWidth(),
)

View File

@@ -40,6 +40,7 @@ import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.features.roomdirectory.impl.R
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@@ -248,7 +249,8 @@ private fun RoomDirectoryRoomRow(
) {
Avatar(
avatarData = roomDescription.avatarData(AvatarSize.RoomDirectoryItem),
modifier = Modifier.align(Alignment.CenterVertically)
avatarType = AvatarType.Room(),
modifier = Modifier.align(Alignment.CenterVertically),
)
Column(
modifier = Modifier

View File

@@ -44,7 +44,8 @@ import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryProvid
import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType
import io.element.android.libraries.core.extensions.orEmpty
import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom
import io.element.android.libraries.designsystem.components.avatar.RoomAvatar
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
@@ -184,11 +185,13 @@ private fun RoomSummaryScaffoldRow(
.padding(horizontal = 16.dp, vertical = 11.dp)
.height(IntrinsicSize.Min),
) {
RoomAvatar(
Avatar(
avatarData = room.avatarData,
heroes = room.heroes,
isTombstoned = room.isTombstoned,
hideAvatarImage = hideAvatarImage,
avatarType = AvatarType.Room(
heroes = room.heroes,
isTombstoned = room.isTombstoned,
),
hideImage = hideAvatarImage,
)
Spacer(modifier = Modifier.width(16.dp))
Column(

View File

@@ -10,7 +10,6 @@ package io.element.android.libraries.designsystem.components.avatar
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.collectAsState
@@ -37,15 +36,50 @@ import timber.log.Timber
fun Avatar(
avatarData: AvatarData,
modifier: Modifier = Modifier,
avatarType: AvatarType = AvatarType.User,
contentDescription: String? = null,
// If not null, will be used instead of the size from avatarData
forcedAvatarSize: Dp? = null,
// If true, will show initials even if avatarData.url is not null
hideImage: Boolean = false,
) {
when (avatarType) {
is AvatarType.Room -> RoomAvatar(
avatarData = avatarData,
avatarType = avatarType,
modifier = modifier,
hideAvatarImage = hideImage,
contentDescription = contentDescription,
)
AvatarType.User -> UserAvatar(
avatarData = avatarData,
modifier = modifier,
contentDescription = contentDescription,
forcedAvatarSize = forcedAvatarSize,
hideImage = hideImage,
)
is AvatarType.Space -> SpaceAvatar(
avatarData = avatarData,
avatarType = avatarType,
modifier = modifier,
hideAvatarImage = hideImage,
contentDescription = contentDescription,
)
}
}
@Composable
private fun UserAvatar(
avatarData: AvatarData,
modifier: Modifier = Modifier,
contentDescription: String? = null,
forcedAvatarSize: Dp? = null,
hideImage: Boolean = false,
) {
if (avatarData.url.isNullOrBlank() || hideImage) {
InitialLetterAvatar(
avatarData = avatarData,
avatarType = AvatarType.User,
forcedAvatarSize = forcedAvatarSize,
modifier = modifier,
contentDescription = contentDescription,
@@ -53,6 +87,7 @@ fun Avatar(
} else {
ImageAvatar(
avatarData = avatarData,
avatarType = AvatarType.User,
forcedAvatarSize = forcedAvatarSize,
modifier = modifier,
contentDescription = contentDescription,
@@ -61,8 +96,9 @@ fun Avatar(
}
@Composable
private fun ImageAvatar(
internal fun ImageAvatar(
avatarData: AvatarData,
avatarType: AvatarType,
forcedAvatarSize: Dp?,
modifier: Modifier = Modifier,
contentDescription: String? = null,
@@ -74,7 +110,7 @@ private fun ImageAvatar(
contentScale = ContentScale.Crop,
modifier = modifier
.size(size)
.clip(CircleShape)
.clip(avatarShape(avatarType))
) {
val collectedState by painter.state.collectAsState()
when (val state = collectedState) {
@@ -85,12 +121,14 @@ private fun ImageAvatar(
}
InitialLetterAvatar(
avatarData = avatarData,
avatarType = avatarType,
forcedAvatarSize = forcedAvatarSize,
contentDescription = contentDescription,
)
}
else -> InitialLetterAvatar(
avatarData = avatarData,
avatarType = avatarType,
forcedAvatarSize = forcedAvatarSize,
contentDescription = contentDescription,
)
@@ -99,8 +137,9 @@ private fun ImageAvatar(
}
@Composable
private fun InitialLetterAvatar(
internal fun InitialLetterAvatar(
avatarData: AvatarData,
avatarType: AvatarType,
forcedAvatarSize: Dp?,
contentDescription: String?,
modifier: Modifier = Modifier,
@@ -109,6 +148,7 @@ private fun InitialLetterAvatar(
TextAvatar(
text = avatarData.initialLetter,
size = forcedAvatarSize ?: avatarData.size.dp,
avatarType = avatarType,
colors = avatarColors,
contentDescription = contentDescription,
modifier = modifier

View File

@@ -31,9 +31,10 @@ import kotlin.math.sin
private const val MAX_AVATAR_COUNT = 4
@Composable
fun AvatarCluster(
internal fun AvatarCluster(
avatars: ImmutableList<AvatarData>,
modifier: Modifier = Modifier,
avatarType: AvatarType = AvatarType.User,
hideAvatarImages: Boolean = false,
contentDescription: String? = null,
) {
@@ -50,6 +51,7 @@ fun AvatarCluster(
1 -> {
Avatar(
avatarData = limitedAvatars[0],
avatarType = avatarType,
modifier = modifier,
contentDescription = contentDescription,
hideImage = hideAvatarImages
@@ -78,10 +80,10 @@ fun AvatarCluster(
}
Box(
modifier = modifier
.size(size.dp)
.semantics {
this.contentDescription = contentDescription.orEmpty()
},
.size(size.dp)
.semantics {
this.contentDescription = contentDescription.orEmpty()
},
contentAlignment = Alignment.Center,
) {
limitedAvatars.forEachIndexed { index, heroAvatar ->
@@ -89,15 +91,16 @@ fun AvatarCluster(
val yOffset = (offsetRadius * sin(angle * index.toDouble() + angleOffset)).dp
Box(
modifier = Modifier
.size(heroAvatarSize)
.offset(
x = xOffset,
y = yOffset,
)
.size(heroAvatarSize)
.offset(
x = xOffset,
y = yOffset,
)
) {
Avatar(
avatarData = heroAvatar,
forcedAvatarSize = heroAvatarSize,
avatarType = avatarType,
hideImage = hideAvatarImages,
)
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2025 New Vector 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.designsystem.components.avatar
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Shape
@Composable
fun avatarShape(
avatarType: AvatarType,
): Shape {
return when (avatarType) {
is AvatarType.Space -> RoundedCornerShape(avatarType.cornerSize)
is AvatarType.Room,
AvatarType.User -> CircleShape
}
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2023, 2024 New Vector 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.designsystem.components.avatar
import androidx.compose.ui.unit.Dp
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
sealed interface AvatarType {
data object User : AvatarType
data class Room(
val isTombstoned: Boolean = false,
val heroes: ImmutableList<AvatarData> = persistentListOf(),
) : AvatarType
data class Space(
val cornerSize: Dp,
) : AvatarType
}

View File

@@ -9,36 +9,46 @@ package io.element.android.libraries.designsystem.components.avatar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import kotlinx.collections.immutable.ImmutableList
@Composable
fun RoomAvatar(
internal fun RoomAvatar(
avatarData: AvatarData,
heroes: ImmutableList<AvatarData>,
avatarType: AvatarType.Room,
modifier: Modifier = Modifier,
isTombstoned: Boolean = false,
hideAvatarImage: Boolean = false,
contentDescription: String? = null,
) {
when {
isTombstoned -> {
avatarType.isTombstoned -> {
TombstonedRoomAvatar(
size = avatarData.size,
modifier = modifier,
avatarType = avatarType,
contentDescription = contentDescription
)
}
avatarData.url != null || heroes.isEmpty() -> {
Avatar(
avatarData = avatarData,
modifier = modifier,
contentDescription = contentDescription,
hideImage = hideAvatarImage
)
avatarData.url != null || avatarType.heroes.isEmpty() -> {
if (avatarData.url.isNullOrBlank() || hideAvatarImage) {
InitialLetterAvatar(
avatarData = avatarData,
avatarType = avatarType,
modifier = modifier,
contentDescription = contentDescription,
forcedAvatarSize = null,
)
} else {
ImageAvatar(
avatarData = avatarData,
avatarType = avatarType,
forcedAvatarSize = null,
modifier = modifier,
contentDescription = contentDescription,
)
}
}
else -> {
AvatarCluster(
avatars = heroes,
avatars = avatarType.heroes,
modifier = modifier,
hideAvatarImages = hideAvatarImage,
contentDescription = contentDescription

View File

@@ -0,0 +1,74 @@
/*
* Copyright 2025 New Vector 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.designsystem.components.avatar
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.utils.CommonDrawables
@Composable
fun SpaceAvatar(
avatarData: AvatarData,
avatarType: AvatarType.Space,
modifier: Modifier = Modifier,
isTombstoned: Boolean = false,
hideAvatarImage: Boolean = false,
contentDescription: String? = null,
) {
when {
isTombstoned -> TombstonedRoomAvatar(
size = avatarData.size,
avatarType = avatarType,
modifier = modifier,
contentDescription = contentDescription,
)
avatarData.url.isNullOrBlank() || hideAvatarImage -> InitialLetterAvatar(
avatarData = avatarData,
avatarType = avatarType,
modifier = modifier,
contentDescription = contentDescription,
forcedAvatarSize = null,
)
else -> ImageAvatar(
avatarData = avatarData,
avatarType = avatarType,
forcedAvatarSize = null,
modifier = modifier,
contentDescription = contentDescription,
)
}
}
@Preview(group = PreviewGroup.Avatars)
@Composable
internal fun SpaceAvatarPreview() =
ElementThemedPreview(
drawableFallbackForImages = CommonDrawables.sample_avatar,
) {
Row(
modifier = Modifier.padding(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
SpaceAvatar(
avatarData = anAvatarData(),
avatarType = AvatarType.Space(cornerSize = 16.dp),
)
SpaceAvatar(
avatarData = anAvatarData(),
avatarType = AvatarType.Space(cornerSize = 16.dp),
isTombstoned = true,
)
}
}

View File

@@ -8,9 +8,11 @@
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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -34,12 +36,13 @@ internal fun TextAvatar(
size: Dp,
colors: AvatarColors,
contentDescription: String?,
avatarType: AvatarType,
modifier: Modifier = Modifier,
) {
Box(
modifier
.size(size)
.clip(CircleShape)
.clip(avatarShape(avatarType))
.background(color = colors.background)
) {
val fontSize = size.toSp() / 2
@@ -64,13 +67,25 @@ internal fun TextAvatar(
@Preview(group = PreviewGroup.Avatars)
@Composable
internal fun TextAvatarPreview() = ElementPreview {
TextAvatar(
text = "AB",
size = 40.dp,
colors = AvatarColors(
background = ElementTheme.colors.bgSubtlePrimary,
foreground = ElementTheme.colors.iconPrimary,
),
contentDescription = null,
)
Row(
modifier = Modifier.padding(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
listOf(
AvatarType.User,
AvatarType.Room(),
AvatarType.Space(8.dp),
).forEach { avatarType ->
TextAvatar(
text = "AB",
size = 40.dp,
colors = AvatarColors(
background = ElementTheme.colors.bgSubtlePrimary,
foreground = ElementTheme.colors.iconPrimary,
),
avatarType = avatarType,
contentDescription = null,
)
}
}
}

View File

@@ -16,9 +16,10 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
@Composable
fun TombstonedRoomAvatar(
internal fun TombstonedRoomAvatar(
size: AvatarSize,
modifier: Modifier = Modifier,
avatarType: AvatarType,
contentDescription: String? = null,
) {
TextAvatar(
@@ -29,7 +30,8 @@ fun TombstonedRoomAvatar(
foreground = ElementTheme.colors.iconTertiary
),
modifier = modifier,
contentDescription = contentDescription
avatarType = avatarType,
contentDescription = contentDescription,
)
}
@@ -38,6 +40,7 @@ fun TombstonedRoomAvatar(
internal fun TombstonedRoomAvatarPreview() = ElementPreview {
TombstonedRoomAvatar(
size = AvatarSize.RoomListItem,
avatarType = AvatarType.Room(),
contentDescription = null,
)
}

View File

@@ -35,6 +35,7 @@ import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon
@@ -49,6 +50,7 @@ fun EditableAvatarView(
displayName: String?,
avatarUrl: Uri?,
avatarSize: AvatarSize,
avatarType: AvatarType,
onAvatarClick: () -> Unit,
modifier: Modifier = Modifier,
) {
@@ -74,12 +76,14 @@ fun EditableAvatarView(
null, "mxc" -> {
Avatar(
avatarData = AvatarData(matrixId, displayName, avatarUrl?.toString(), size = avatarSize),
avatarType = avatarType,
modifier = Modifier.fillMaxSize(),
)
}
else -> {
UnsavedAvatar(
avatarUri = avatarUrl,
avatarType = avatarType,
modifier = Modifier.fillMaxSize(),
)
}
@@ -116,6 +120,7 @@ internal fun EditableAvatarViewPreview(
displayName = "A room",
avatarUrl = uri,
avatarSize = AvatarSize.EditRoomDetails,
avatarType = AvatarType.User,
onAvatarClick = {},
)
}

View File

@@ -28,8 +28,9 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.RoomAvatar
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon
@@ -53,10 +54,12 @@ fun SelectedRoom(
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
RoomAvatar(
Avatar(
avatarData = roomInfo.getAvatarData(AvatarSize.SelectedRoom),
heroes = roomInfo.heroes.map { it.getAvatarData(AvatarSize.SelectedRoom) }.toImmutableList(),
isTombstoned = roomInfo.isTombstoned,
avatarType = AvatarType.Room(
heroes = roomInfo.heroes.map { it.getAvatarData(AvatarSize.SelectedRoom) }.toImmutableList(),
isTombstoned = roomInfo.isTombstoned,
),
)
Text(
// If name is null, we do not have space to render "No room name", so just use `#` here.

View File

@@ -9,10 +9,11 @@ package io.element.android.libraries.matrix.ui.components
import android.net.Uri
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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AddAPhoto
import androidx.compose.material3.MaterialTheme
@@ -27,6 +28,8 @@ import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.components.avatar.avatarShape
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon
@@ -40,11 +43,12 @@ import io.element.android.libraries.designsystem.theme.temporaryColorBgSpecial
@Composable
fun UnsavedAvatar(
avatarUri: Uri?,
avatarType: AvatarType,
modifier: Modifier = Modifier,
) {
val commonModifier = modifier
.size(70.dp)
.clip(CircleShape)
.clip(avatarShape(avatarType))
if (avatarUri != null) {
val context = LocalContext.current
@@ -75,8 +79,13 @@ fun UnsavedAvatar(
@PreviewsDayNight
@Composable
internal fun UnsavedAvatarPreview() = ElementPreview {
Row {
UnsavedAvatar(null)
UnsavedAvatar(Uri.EMPTY)
Row(
modifier = Modifier.padding(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
UnsavedAvatar(null, AvatarType.User)
UnsavedAvatar(Uri.EMPTY, AvatarType.User)
UnsavedAvatar(null, AvatarType.Space(8.dp))
UnsavedAvatar(Uri.EMPTY, AvatarType.Space(8.dp))
}
}

View File

@@ -32,8 +32,9 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.RoomAvatar
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@@ -214,12 +215,14 @@ private fun RoomSummaryView(
.heightIn(56.dp),
verticalAlignment = Alignment.CenterVertically
) {
RoomAvatar(
Avatar(
avatarData = roomInfo.getAvatarData(size = AvatarSize.RoomSelectRoomListItem),
heroes = roomInfo.heroes.map { user ->
user.getAvatarData(size = AvatarSize.RoomSelectRoomListItem)
}.toPersistentList(),
isTombstoned = roomInfo.isTombstoned,
avatarType = AvatarType.Room(
heroes = roomInfo.heroes.map { user ->
user.getAvatarData(size = AvatarSize.RoomSelectRoomListItem)
}.toPersistentList(),
isTombstoned = roomInfo.isTombstoned,
),
)
Column(
modifier = Modifier