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

39
features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListViewModel.kt

@ -5,6 +5,7 @@ import com.airbnb.mvrx.Loading @@ -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) : @@ -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) : @@ -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) : @@ -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) : @@ -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() {

3
features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/MatrixUser.kt

@ -1,10 +1,11 @@ @@ -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(),
)

3
features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListRoomSummary.kt

@ -1,5 +1,6 @@ @@ -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( @@ -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,
)

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

3
libraries/designsystem/src/main/java/io/element/android/x/designsystem/Color.kt

@ -4,3 +4,6 @@ import androidx.compose.ui.graphics.Color @@ -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)

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

@ -1,31 +0,0 @@ @@ -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 @@ @@ -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 @@ @@ -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