Continue refinement of RoomList (and remove avatar library)

This commit is contained in:
ganfra 2022-11-03 18:02:53 +01:00
parent 5984ffc960
commit f55bb16bfa
22 changed files with 305 additions and 322 deletions

View file

@ -13,6 +13,7 @@ dependencies {
implementation(libs.mavericks.compose)
implementation(libs.timber)
implementation(libs.datetime)
implementation(libs.accompanist.placeholder)
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.3")
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")

View file

@ -2,39 +2,30 @@
package io.element.android.x.features.roomlist
import Avatar
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExitToApp
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.*
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
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.ElementXTheme
import io.element.android.x.designsystem.components.avatar.AvatarData
import io.element.android.x.features.roomlist.components.RoomItem
import io.element.android.x.features.roomlist.components.RoomListTopBar
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
import io.element.android.x.features.roomlist.model.stubbedRoomSummaries
import io.element.android.x.matrix.core.RoomId
@Composable
@ -87,125 +78,29 @@ fun RoomListContent(
}
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun RoomListTopBar(
matrixUser: MatrixUser?,
onLogoutClicked: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior
) {
LogCompositions(tag = "RoomListScreen", msg = "TopBar")
if (matrixUser == null) return
MediumTopAppBar(
modifier = Modifier
.nestedScroll(scrollBehavior.nestedScrollConnection),
title = {
Text(
fontWeight = FontWeight.Bold,
text = "All Chats"
)
},
navigationIcon = {
IconButton(onClick = {}) {
Avatar(matrixUser.avatarData)
}
},
actions = {
IconButton(
onClick = onLogoutClicked
) {
Icon(Icons.Default.ExitToApp, contentDescription = "logout")
}
},
scrollBehavior = scrollBehavior,
)
}
@Composable
private fun RoomItem(
modifier: Modifier = Modifier,
room: RoomListRoomSummary,
onClick: (RoomId) -> Unit
) {
if (room.isPlaceholder) {
return
}
Column(
modifier = modifier
.fillMaxWidth()
.height(72.dp)
.clickable(
onClick = { onClick(room.roomId) },
indication = rememberRipple(),
interactionSource = remember { MutableInteractionSource() }
),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Avatar(room.avatarData)
Column(
modifier = Modifier
.padding(12.dp)
.weight(1f)
) {
Text(
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
text = room.name,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = room.lastMessage?.toString().orEmpty(),
color = MaterialTheme.colorScheme.secondary,
fontSize = 15.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Column(
) {
Text(
fontSize = 12.sp,
text = room.timestamp ?: "",
color = MaterialTheme.colorScheme.secondary,
)
Spacer(modifier.size(4.dp))
val unreadIndicatorColor =
if (room.hasUnread) MaterialTheme.colorScheme.primary else Color.Transparent
Box(
modifier = Modifier
.size(12.dp)
.clip(CircleShape)
.background(unreadIndicatorColor)
.align(Alignment.End),
)
}
}
private fun PreviewableRoomListContent() {
ElementXTheme(darkTheme = false) {
RoomListContent(
roomSummaries = stubbedRoomSummaries(),
matrixUser = MatrixUser("User#1", avatarData = AvatarData("U")),
onRoomClicked = {},
onLogoutClicked = {}
)
}
}
@Preview
@Composable
private fun PreviewableRoomListContent() {
val roomSummaries = listOf(
RoomListRoomSummary(
name = "Room",
hasUnread = true,
timestamp = "14:18",
lastMessage = "A message",
avatarData = AvatarData("R"),
id = "roomId"
private fun PreviewableDarkRoomListContent() {
ElementXTheme(darkTheme = true) {
RoomListContent(
roomSummaries = stubbedRoomSummaries(),
matrixUser = MatrixUser("User#1", avatarData = AvatarData("U")),
onRoomClicked = {},
onLogoutClicked = {}
)
)
RoomListContent(
roomSummaries = roomSummaries,
matrixUser = MatrixUser("User#1", avatarData = AvatarData("U")),
onRoomClicked = {},
onLogoutClicked = {}
)
}
}
}

View file

@ -1,12 +1,12 @@
package io.element.android.x.features.roomlist
import androidx.compose.ui.unit.dp
import com.airbnb.mvrx.Fail
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.designsystem.components.avatar.AvatarSize
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
@ -47,7 +47,7 @@ class RoomListViewModel(initialState: RoomListViewState) :
val userAvatarUrl = client.loadUserAvatarURLString().getOrNull()
val userDisplayName = client.loadUserDisplayName().getOrNull()
val avatarData =
loadAvatarData(client, userDisplayName ?: client.userId().value, userAvatarUrl, 32)
loadAvatarData(client, userDisplayName ?: client.userId().value, userAvatarUrl, AvatarSize.SMALL)
MatrixUser(
username = userDisplayName ?: client.userId().value,
avatarUrl = userAvatarUrl,
@ -73,10 +73,7 @@ class RoomListViewModel(initialState: RoomListViewState) :
): List<RoomListRoomSummary> {
return roomSummaries.parallelMap { roomSummary ->
when (roomSummary) {
is RoomSummary.Empty -> RoomListRoomSummary(
id = roomSummary.identifier,
isPlaceholder = true
)
is RoomSummary.Empty -> RoomListRoomSummary.placeholder(roomSummary.identifier)
is RoomSummary.Filled -> {
val avatarData = loadAvatarData(
client,
@ -100,17 +97,17 @@ class RoomListViewModel(initialState: RoomListViewState) :
client: MatrixClient,
name: String,
url: String?,
size: Long = 48
size: AvatarSize = AvatarSize.MEDIUM
): AvatarData {
val mediaContent = url?.let {
val mediaSource = mediaSourceFromUrl(it)
client.loadMediaThumbnailForSource(mediaSource, size, size)
client.loadMediaThumbnailForSource(mediaSource, size.value.toLong(), size.value.toLong())
}
return mediaContent?.fold(
{ it },
{ null }
).let { model ->
AvatarData(name.first().toString(), model, size.toInt())
AvatarData(name.first().uppercase(), model, size)
}
}

View file

@ -0,0 +1,157 @@
package io.element.android.x.features.roomlist.components
import Avatar
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.accompanist.placeholder.material.placeholder
import io.element.android.x.features.roomlist.model.RoomListRoomSummary
import io.element.android.x.matrix.core.RoomId
@Composable
internal fun RoomItem(
modifier: Modifier = Modifier,
room: RoomListRoomSummary,
onClick: (RoomId) -> Unit
) {
if (room.isPlaceholder) {
return PlaceholderRoomItem(modifier = modifier, room = room)
}
Column(
modifier = modifier
.fillMaxWidth()
.clickable(
onClick = { onClick(room.roomId) },
indication = rememberRipple(),
interactionSource = remember { MutableInteractionSource() }
),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.height(IntrinsicSize.Min),
verticalAlignment = CenterVertically
) {
Avatar(room.avatarData)
Column(
modifier = Modifier
.padding(start = 12.dp, end = 4.dp, top = 12.dp, bottom = 12.dp)
.alignByBaseline()
.weight(1f)
) {
// Name
Text(
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
text = room.name,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
// Last Message
Text(
text = room.lastMessage?.toString().orEmpty(),
color = MaterialTheme.colorScheme.secondary,
lineHeight = 20.sp,
fontSize = 15.sp,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
// Timestamp and Unread
Column(
modifier = Modifier
.alignByBaseline(),
) {
Text(
fontSize = 12.sp,
text = room.timestamp ?: "",
color = MaterialTheme.colorScheme.secondary,
)
Spacer(modifier.size(4.dp))
val unreadIndicatorColor =
if (room.hasUnread) MaterialTheme.colorScheme.primary else Color.Transparent
Box(
modifier = Modifier
.size(12.dp)
.clip(CircleShape)
.background(unreadIndicatorColor)
.align(Alignment.End),
)
}
}
}
}
@Composable
internal fun PlaceholderRoomItem(
modifier: Modifier = Modifier,
room: RoomListRoomSummary,
) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
verticalAlignment = CenterVertically,
) {
Text(
modifier = Modifier
.size(room.avatarData.size.dp)
.clip(CircleShape)
.placeholder(true),
text = ""
)
Column(
modifier = Modifier
.padding(start = 12.dp, end = 4.dp, top = 12.dp, bottom = 12.dp)
.weight(1f)
) {
Text(
modifier = Modifier
.size(width = 80.dp, height = 12.dp)
.placeholder(visible = true),
text = "",
)
Spacer(modifier = Modifier.size(4.dp))
Text(
modifier = Modifier
.size(width = 160.dp, height = 12.dp)
.placeholder(visible = true),
text = "",
)
}
Column {
Text(
modifier = Modifier
.size(width = 24.dp, height = 12.dp)
.placeholder(visible = true),
text = "",
color = MaterialTheme.colorScheme.secondary,
)
Spacer(Modifier.size(4.dp))
Box(
modifier = Modifier
.size(12.dp)
.clip(CircleShape)
.background(Color.Transparent)
.align(Alignment.End),
)
}
}
}

View file

@ -0,0 +1,48 @@
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.x.features.roomlist.components
import Avatar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExitToApp
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.text.font.FontWeight
import io.element.android.x.core.data.LogCompositions
import io.element.android.x.features.roomlist.model.MatrixUser
@Composable
fun RoomListTopBar(
matrixUser: MatrixUser?,
onLogoutClicked: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior
) {
LogCompositions(tag = "RoomListScreen", msg = "TopBar")
MediumTopAppBar(
modifier = Modifier
.nestedScroll(scrollBehavior.nestedScrollConnection),
title = {
Text(
fontWeight = FontWeight.Bold,
text = "All Chats"
)
},
navigationIcon = {
if (matrixUser != null) {
IconButton(onClick = {}) {
Avatar(matrixUser.avatarData)
}
}
},
actions = {
IconButton(
onClick = onLogoutClicked
) {
Icon(Icons.Default.ExitToApp, contentDescription = "logout")
}
},
scrollBehavior = scrollBehavior,
)
}

View file

@ -12,4 +12,18 @@ data class RoomListRoomSummary(
val lastMessage: CharSequence? = null,
val avatarData: AvatarData = AvatarData(),
val isPlaceholder: Boolean = false,
)
) {
companion object {
fun placeholder(id: String): RoomListRoomSummary {
return RoomListRoomSummary(
id = id,
isPlaceholder = true,
name = "Short name",
timestamp = "hh:mm",
lastMessage = "Last message for placeholder",
avatarData = AvatarData("S")
)
}
}
}

View file

@ -0,0 +1,25 @@
package io.element.android.x.features.roomlist.model
import io.element.android.x.designsystem.components.avatar.AvatarData
internal fun stubbedRoomSummaries(): List<RoomListRoomSummary> {
return listOf(
RoomListRoomSummary(
name = "Room",
hasUnread = true,
timestamp = "14:18",
lastMessage = "A very very very very long message which suites on two lines",
avatarData = AvatarData("R"),
id = "roomId"
),
RoomListRoomSummary(
name = "Room#2",
hasUnread = false,
timestamp = "14:16",
lastMessage = "A short message",
avatarData = AvatarData("Z"),
id = "roomId2"
),
RoomListRoomSummary.placeholder("roomId2")
)
}