Browse Source

RoomList: avoid recomposition on avatar and add placeholder

feature/bma/flipper
ganfra 2 years ago
parent
commit
eb2fd13518
  1. 16
      features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListScreen.kt
  2. 39
      features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListViewModel.kt
  3. 3
      features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/MatrixUser.kt
  4. 3
      features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListRoomSummary.kt
  5. 2
      features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListViewState.kt
  6. 3
      libraries/designsystem/src/main/java/io/element/android/x/designsystem/Color.kt
  7. 31
      libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/Avatar.kt
  8. 68
      libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/avatar/Avatar.kt
  9. 34
      libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/avatar/AvatarData.kt

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

39
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.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() {

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

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

2
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 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,

3
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 LightGrey = Color(0x993C3C43)
val DarkGrey = Color(0x99EBEBF5) val DarkGrey = Color(0x99EBEBF5)
val AvatarGradientStart = Color(0xFF4CA1AF)
val AvatarGradientEnd = Color(0xFFC4E0E5)

31
libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/Avatar.kt

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

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

34
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
}
}
Loading…
Cancel
Save