RoomList: avoid recomposition on avatar and add placeholder
This commit is contained in:
parent
66dc61cc3e
commit
c66b7a699c
9 changed files with 145 additions and 54 deletions
|
|
@ -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 = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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…
Add table
Add a link
Reference in a new issue