diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListScreen.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListScreen.kt index 07fb90d3ce..2169a266ad 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListScreen.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListScreen.kt @@ -28,6 +28,7 @@ import com.airbnb.mvrx.Success import com.airbnb.mvrx.compose.collectAsState import com.airbnb.mvrx.compose.mavericksViewModel import io.element.android.x.core.data.LogCompositions +import io.element.android.x.designsystem.components.avatar.AvatarData import io.element.android.x.features.roomlist.model.MatrixUser import io.element.android.x.features.roomlist.model.RoomListRoomSummary import io.element.android.x.features.roomlist.model.RoomListViewState @@ -49,7 +50,7 @@ fun RoomListScreen( val matrixUser by viewModel.collectAsState(RoomListViewState::user) RoomListContent( roomSummaries = roomSummaries().orEmpty(), - matrixUser = matrixUser, + matrixUser = matrixUser(), onRoomClicked = onRoomClicked, onLogoutClicked = viewModel::logout ) @@ -58,7 +59,7 @@ fun RoomListScreen( @Composable fun RoomListContent( roomSummaries: List, - matrixUser: MatrixUser, + matrixUser: MatrixUser?, onRoomClicked: (RoomId) -> Unit, onLogoutClicked: () -> Unit, ) { @@ -84,15 +85,16 @@ fun RoomListContent( @OptIn(ExperimentalMaterial3Api::class) @Composable -fun RoomListTopBar(matrixUser: MatrixUser, onLogoutClicked: () -> Unit) { +fun RoomListTopBar(matrixUser: MatrixUser?, onLogoutClicked: () -> Unit) { LogCompositions(tag = "RoomListScreen", msg = "TopBar") + if (matrixUser == null) return TopAppBar( title = { Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { - Avatar(data = matrixUser.avatarData, size = 32.dp) + Avatar(matrixUser.avatarData) Spacer(modifier = Modifier.width(8.dp)) Text("${matrixUser.username}") } @@ -131,7 +133,7 @@ private fun RoomItem( .padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically, ) { - Avatar(data = room.avatarData) + Avatar(room.avatarData) Column( modifier = Modifier .padding(12.dp) @@ -182,13 +184,13 @@ private fun PreviewableRoomListContent() { hasUnread = true, timestamp = "14:18", lastMessage = "A message", - avatarData = null, + avatarData = AvatarData("R"), id = "roomId" ) ) RoomListContent( roomSummaries = roomSummaries, - matrixUser = MatrixUser("User#1"), + matrixUser = MatrixUser("User#1", avatarData = AvatarData("U")), onRoomClicked = {}, onLogoutClicked = {} ) diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListViewModel.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListViewModel.kt index 72ace28464..7c8a31a1c2 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListViewModel.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListViewModel.kt @@ -5,6 +5,7 @@ import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MavericksViewModel import com.airbnb.mvrx.Success import io.element.android.x.core.data.parallelMap +import io.element.android.x.designsystem.components.avatar.AvatarData import io.element.android.x.features.roomlist.model.MatrixUser import io.element.android.x.features.roomlist.model.RoomListRoomSummary import io.element.android.x.features.roomlist.model.RoomListViewState @@ -41,17 +42,18 @@ class RoomListViewModel(initialState: RoomListViewState) : viewModelScope.launch { val client = getClient() client.startSync() - val userAvatarUrl = client.loadUserAvatarURLString().getOrNull() - val userDisplayName = client.loadUserDisplayName().getOrNull() - val avatarData = loadAvatarData(client, userAvatarUrl) - setState { - copy( - user = MatrixUser( - username = userDisplayName, - avatarUrl = userAvatarUrl, - avatarData = avatarData, - ) + suspend { + val userAvatarUrl = client.loadUserAvatarURLString().getOrNull() + val userDisplayName = client.loadUserDisplayName().getOrNull() + val avatarData = + loadAvatarData(client, userDisplayName ?: client.userId().value, userAvatarUrl) + MatrixUser( + username = userDisplayName ?: client.userId().value, + avatarUrl = userAvatarUrl, + avatarData = avatarData, ) + }.execute { + copy(user = it) } client.roomSummaryDataSource().roomSummaries() .map { roomSummaries -> @@ -75,7 +77,11 @@ class RoomListViewModel(initialState: RoomListViewState) : isPlaceholder = true ) is RoomSummary.Filled -> { - val avatarData = loadAvatarData(client, roomSummary.details.avatarURLString) + val avatarData = loadAvatarData( + client, + roomSummary.details.name, + roomSummary.details.avatarURLString + ) RoomListRoomSummary( id = roomSummary.identifier(), name = roomSummary.details.name, @@ -89,7 +95,12 @@ class RoomListViewModel(initialState: RoomListViewState) : } } - private suspend fun loadAvatarData(client: MatrixClient, url: String?, size: Long = 48): ByteArray? { + private suspend fun loadAvatarData( + client: MatrixClient, + name: String, + url: String?, + size: Long = 48 + ): AvatarData { val mediaContent = url?.let { val mediaSource = mediaSourceFromUrl(it) client.loadMediaThumbnailForSource(mediaSource, size, size) @@ -97,7 +108,9 @@ class RoomListViewModel(initialState: RoomListViewState) : return mediaContent?.fold( { it }, { null } - ) + ).let { model -> + AvatarData(name.first().toString(), model, size.toInt()) + } } private fun handleLogout() { diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/MatrixUser.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/MatrixUser.kt index dd0f8ae083..f002fc17b5 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/MatrixUser.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/MatrixUser.kt @@ -1,10 +1,11 @@ package io.element.android.x.features.roomlist.model import androidx.compose.runtime.Stable +import io.element.android.x.designsystem.components.avatar.AvatarData @Stable data class MatrixUser( val username: String? = null, val avatarUrl: String? = null, - val avatarData: ByteArray? = null, + val avatarData: AvatarData = AvatarData(), ) diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListRoomSummary.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListRoomSummary.kt index ed333e2db4..ab54f5b1be 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListRoomSummary.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListRoomSummary.kt @@ -1,5 +1,6 @@ package io.element.android.x.features.roomlist.model +import io.element.android.x.designsystem.components.avatar.AvatarData import io.element.android.x.matrix.core.RoomId data class RoomListRoomSummary( @@ -9,6 +10,6 @@ data class RoomListRoomSummary( val hasUnread: Boolean = false, val timestamp: String? = null, val lastMessage: CharSequence? = null, - val avatarData: ByteArray? = null, + val avatarData: AvatarData = AvatarData(), val isPlaceholder: Boolean = false, ) \ No newline at end of file diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListViewState.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListViewState.kt index d7439a16b2..52f8cfdfcf 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListViewState.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListViewState.kt @@ -7,7 +7,7 @@ import io.element.android.x.features.roomlist.model.MatrixUser import io.element.android.x.matrix.room.RoomSummary data class RoomListViewState( - val user: MatrixUser = MatrixUser(), + val user: Async = Uninitialized, val rooms: Async> = Uninitialized, val canLoadMore: Boolean = false, val logoutAction: Async = Uninitialized, diff --git a/libraries/designsystem/src/main/java/io/element/android/x/designsystem/Color.kt b/libraries/designsystem/src/main/java/io/element/android/x/designsystem/Color.kt index 1a8646cd51..6d1e92c5df 100644 --- a/libraries/designsystem/src/main/java/io/element/android/x/designsystem/Color.kt +++ b/libraries/designsystem/src/main/java/io/element/android/x/designsystem/Color.kt @@ -4,3 +4,6 @@ import androidx.compose.ui.graphics.Color val LightGrey = Color(0x993C3C43) val DarkGrey = Color(0x99EBEBF5) + +val AvatarGradientStart = Color(0xFF4CA1AF) +val AvatarGradientEnd = Color(0xFFC4E0E5) \ No newline at end of file diff --git a/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/Avatar.kt b/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/Avatar.kt deleted file mode 100644 index 2fac353f48..0000000000 --- a/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/Avatar.kt +++ /dev/null @@ -1,31 +0,0 @@ -import android.util.Log -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage - -/** - * TODO fallback Avatar - */ -@Composable -fun Avatar( - data: ByteArray?, - size: Dp = 48.dp, -) { - AsyncImage( - model = data, - onError = { - Log.e("TAG", "Error $it\n${it.result}", it.result.throwable) - }, - contentDescription = null, - modifier = Modifier - .size(size) - .clip(CircleShape) - ) -} - diff --git a/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/avatar/Avatar.kt b/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/avatar/Avatar.kt new file mode 100644 index 0000000000..33c93c1e8e --- /dev/null +++ b/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/avatar/Avatar.kt @@ -0,0 +1,68 @@ +import android.util.Log +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil.compose.AsyncImage +import io.element.android.x.designsystem.AvatarGradientEnd +import io.element.android.x.designsystem.AvatarGradientStart +import io.element.android.x.designsystem.components.avatar.AvatarData + +@Composable +fun Avatar(avatarData: AvatarData) { + if (avatarData.model == null) { + InitialsAvatar( + modifier = Modifier + .size(avatarData.size.dp) + .clip(CircleShape), + initials = avatarData.initials + ) + } else { + AsyncImage( + model = avatarData.model, + onError = { + Log.e("TAG", "Error $it\n${it.result}", it.result.throwable) + }, + contentDescription = null, + modifier = Modifier + .size(avatarData.size.dp) + .clip(CircleShape) + ) + } +} + +@Composable +private fun InitialsAvatar( + initials: String, + modifier: Modifier = Modifier, +) { + val initialsGradient = Brush.linearGradient( + listOf( + AvatarGradientStart, + AvatarGradientEnd, + ) + ) + Box( + modifier + .background(brush = initialsGradient) + ) { + Text( + modifier = Modifier + .align(Alignment.Center), + text = initials, + fontSize = 24.sp, + color = Color.White, + ) + } +} + + diff --git a/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/avatar/AvatarData.kt b/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/avatar/AvatarData.kt new file mode 100644 index 0000000000..2d4e6f34ef --- /dev/null +++ b/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/avatar/AvatarData.kt @@ -0,0 +1,34 @@ +package io.element.android.x.designsystem.components.avatar + +import androidx.compose.runtime.Stable + +@Stable +data class AvatarData( + val initials: String = "", + val model: ByteArray? = null, + val size: Int = 0 +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AvatarData + + if (initials != other.initials) return false + if (model != null) { + if (other.model == null) return false + if (!model.contentEquals(other.model)) return false + } else if (other.model != null) return false + if (size != other.size) return false + + return true + } + + override fun hashCode(): Int { + var result = initials.hashCode() + result = 31 * result + (model?.contentHashCode() ?: 0) + result = 31 * result + size + return result + } + +} \ No newline at end of file