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.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<RoomListRoomSummary>,
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 = {}
)

View File

@@ -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() {

View File

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

View File

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

View File

@@ -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<MatrixUser> = Uninitialized,
val rooms: Async<List<RoomListRoomSummary>> = Uninitialized,
val canLoadMore: Boolean = false,
val logoutAction: Async<Unit> = Uninitialized,

View File

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

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