RoomList: avoid recomposition on avatar and add placeholder

This commit is contained in:
ganfra 2022-11-03 10:58:53 +01:00
parent 66dc61cc3e
commit c66b7a699c
9 changed files with 145 additions and 54 deletions

View file

@ -28,6 +28,7 @@ import com.airbnb.mvrx.Success
import com.airbnb.mvrx.compose.collectAsState import com.airbnb.mvrx.compose.collectAsState
import com.airbnb.mvrx.compose.mavericksViewModel import com.airbnb.mvrx.compose.mavericksViewModel
import io.element.android.x.core.data.LogCompositions 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.MatrixUser
import io.element.android.x.features.roomlist.model.RoomListRoomSummary import io.element.android.x.features.roomlist.model.RoomListRoomSummary
import io.element.android.x.features.roomlist.model.RoomListViewState import io.element.android.x.features.roomlist.model.RoomListViewState
@ -49,7 +50,7 @@ fun RoomListScreen(
val matrixUser by viewModel.collectAsState(RoomListViewState::user) val matrixUser by viewModel.collectAsState(RoomListViewState::user)
RoomListContent( RoomListContent(
roomSummaries = roomSummaries().orEmpty(), roomSummaries = roomSummaries().orEmpty(),
matrixUser = matrixUser, matrixUser = matrixUser(),
onRoomClicked = onRoomClicked, onRoomClicked = onRoomClicked,
onLogoutClicked = viewModel::logout onLogoutClicked = viewModel::logout
) )
@ -58,7 +59,7 @@ fun RoomListScreen(
@Composable @Composable
fun RoomListContent( fun RoomListContent(
roomSummaries: List<RoomListRoomSummary>, roomSummaries: List<RoomListRoomSummary>,
matrixUser: MatrixUser, matrixUser: MatrixUser?,
onRoomClicked: (RoomId) -> Unit, onRoomClicked: (RoomId) -> Unit,
onLogoutClicked: () -> Unit, onLogoutClicked: () -> Unit,
) { ) {
@ -84,15 +85,16 @@ fun RoomListContent(
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun RoomListTopBar(matrixUser: MatrixUser, onLogoutClicked: () -> Unit) { fun RoomListTopBar(matrixUser: MatrixUser?, onLogoutClicked: () -> Unit) {
LogCompositions(tag = "RoomListScreen", msg = "TopBar") LogCompositions(tag = "RoomListScreen", msg = "TopBar")
if (matrixUser == null) return
TopAppBar( TopAppBar(
title = { title = {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Avatar(data = matrixUser.avatarData, size = 32.dp) Avatar(matrixUser.avatarData)
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text("${matrixUser.username}") Text("${matrixUser.username}")
} }
@ -131,7 +133,7 @@ private fun RoomItem(
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Avatar(data = room.avatarData) Avatar(room.avatarData)
Column( Column(
modifier = Modifier modifier = Modifier
.padding(12.dp) .padding(12.dp)
@ -182,13 +184,13 @@ private fun PreviewableRoomListContent() {
hasUnread = true, hasUnread = true,
timestamp = "14:18", timestamp = "14:18",
lastMessage = "A message", lastMessage = "A message",
avatarData = null, avatarData = AvatarData("R"),
id = "roomId" id = "roomId"
) )
) )
RoomListContent( RoomListContent(
roomSummaries = roomSummaries, roomSummaries = roomSummaries,
matrixUser = MatrixUser("User#1"), matrixUser = MatrixUser("User#1", avatarData = AvatarData("U")),
onRoomClicked = {}, onRoomClicked = {},
onLogoutClicked = {} onLogoutClicked = {}
) )

View file

@ -5,6 +5,7 @@ import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MavericksViewModel import com.airbnb.mvrx.MavericksViewModel
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import io.element.android.x.core.data.parallelMap 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.MatrixUser
import io.element.android.x.features.roomlist.model.RoomListRoomSummary import io.element.android.x.features.roomlist.model.RoomListRoomSummary
import io.element.android.x.features.roomlist.model.RoomListViewState import io.element.android.x.features.roomlist.model.RoomListViewState
@ -41,17 +42,18 @@ class RoomListViewModel(initialState: RoomListViewState) :
viewModelScope.launch { viewModelScope.launch {
val client = getClient() val client = getClient()
client.startSync() client.startSync()
val userAvatarUrl = client.loadUserAvatarURLString().getOrNull() suspend {
val userDisplayName = client.loadUserDisplayName().getOrNull() val userAvatarUrl = client.loadUserAvatarURLString().getOrNull()
val avatarData = loadAvatarData(client, userAvatarUrl) val userDisplayName = client.loadUserDisplayName().getOrNull()
setState { val avatarData =
copy( loadAvatarData(client, userDisplayName ?: client.userId().value, userAvatarUrl)
user = MatrixUser( MatrixUser(
username = userDisplayName, username = userDisplayName ?: client.userId().value,
avatarUrl = userAvatarUrl, avatarUrl = userAvatarUrl,
avatarData = avatarData, avatarData = avatarData,
)
) )
}.execute {
copy(user = it)
} }
client.roomSummaryDataSource().roomSummaries() client.roomSummaryDataSource().roomSummaries()
.map { roomSummaries -> .map { roomSummaries ->
@ -75,7 +77,11 @@ class RoomListViewModel(initialState: RoomListViewState) :
isPlaceholder = true isPlaceholder = true
) )
is RoomSummary.Filled -> { is RoomSummary.Filled -> {
val avatarData = loadAvatarData(client, roomSummary.details.avatarURLString) val avatarData = loadAvatarData(
client,
roomSummary.details.name,
roomSummary.details.avatarURLString
)
RoomListRoomSummary( RoomListRoomSummary(
id = roomSummary.identifier(), id = roomSummary.identifier(),
name = roomSummary.details.name, 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 mediaContent = url?.let {
val mediaSource = mediaSourceFromUrl(it) val mediaSource = mediaSourceFromUrl(it)
client.loadMediaThumbnailForSource(mediaSource, size, size) client.loadMediaThumbnailForSource(mediaSource, size, size)
@ -97,7 +108,9 @@ class RoomListViewModel(initialState: RoomListViewState) :
return mediaContent?.fold( return mediaContent?.fold(
{ it }, { it },
{ null } { null }
) ).let { model ->
AvatarData(name.first().toString(), model, size.toInt())
}
} }
private fun handleLogout() { private fun handleLogout() {

View file

@ -1,10 +1,11 @@
package io.element.android.x.features.roomlist.model package io.element.android.x.features.roomlist.model
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import io.element.android.x.designsystem.components.avatar.AvatarData
@Stable @Stable
data class MatrixUser( data class MatrixUser(
val username: String? = null, val username: String? = null,
val avatarUrl: String? = null, val avatarUrl: String? = null,
val avatarData: ByteArray? = null, val avatarData: AvatarData = AvatarData(),
) )

View file

@ -1,5 +1,6 @@
package io.element.android.x.features.roomlist.model 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 import io.element.android.x.matrix.core.RoomId
data class RoomListRoomSummary( data class RoomListRoomSummary(
@ -9,6 +10,6 @@ data class RoomListRoomSummary(
val hasUnread: Boolean = false, val hasUnread: Boolean = false,
val timestamp: String? = null, val timestamp: String? = null,
val lastMessage: CharSequence? = null, val lastMessage: CharSequence? = null,
val avatarData: ByteArray? = null, val avatarData: AvatarData = AvatarData(),
val isPlaceholder: Boolean = false, val isPlaceholder: Boolean = false,
) )

View file

@ -7,7 +7,7 @@ import io.element.android.x.features.roomlist.model.MatrixUser
import io.element.android.x.matrix.room.RoomSummary import io.element.android.x.matrix.room.RoomSummary
data class RoomListViewState( data class RoomListViewState(
val user: MatrixUser = MatrixUser(), val user: Async<MatrixUser> = Uninitialized,
val rooms: Async<List<RoomListRoomSummary>> = Uninitialized, val rooms: Async<List<RoomListRoomSummary>> = Uninitialized,
val canLoadMore: Boolean = false, val canLoadMore: Boolean = false,
val logoutAction: Async<Unit> = Uninitialized, val logoutAction: Async<Unit> = Uninitialized,

View file

@ -4,3 +4,6 @@ import androidx.compose.ui.graphics.Color
val LightGrey = Color(0x993C3C43) val LightGrey = Color(0x993C3C43)
val DarkGrey = Color(0x99EBEBF5) val DarkGrey = Color(0x99EBEBF5)
val AvatarGradientStart = Color(0xFF4CA1AF)
val AvatarGradientEnd = Color(0xFFC4E0E5)

View file

@ -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)
)
}

View file

@ -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,
)
}
}

View file

@ -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
}
}