RoomList: add avatar for rooms and date formatting
This commit is contained in:
parent
1801bcfcd3
commit
9037b9b66b
16 changed files with 244 additions and 61 deletions
|
|
@ -69,6 +69,7 @@ dependencies {
|
||||||
implementation project(":features:login")
|
implementation project(":features:login")
|
||||||
implementation project(":features:roomlist")
|
implementation project(":features:roomlist")
|
||||||
|
|
||||||
|
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.2.0"
|
||||||
implementation 'io.github.raamcosta.compose-destinations:core:1.7.23-beta'
|
implementation 'io.github.raamcosta.compose-destinations:core:1.7.23-beta'
|
||||||
ksp 'io.github.raamcosta.compose-destinations:ksp:1.7.23-beta'
|
ksp 'io.github.raamcosta.compose-destinations:ksp:1.7.23-beta'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ dependencies {
|
||||||
implementation(project(":libraries:designsystem"))
|
implementation(project(":libraries:designsystem"))
|
||||||
implementation(libs.mavericks.compose)
|
implementation(libs.mavericks.compose)
|
||||||
implementation(libs.timber)
|
implementation(libs.timber)
|
||||||
|
implementation(libs.datetime)
|
||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation("junit:junit:4.13.2")
|
||||||
androidTestImplementation("androidx.test.ext:junit:1.1.3")
|
androidTestImplementation("androidx.test.ext:junit:1.1.3")
|
||||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
package io.element.android.x.features.roomlist
|
||||||
|
|
||||||
|
import android.text.format.DateFormat
|
||||||
|
import android.text.format.DateUtils
|
||||||
|
import kotlinx.datetime.*
|
||||||
|
import kotlinx.datetime.TimeZone
|
||||||
|
import java.time.Period
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
|
class LastMessageFormatter(
|
||||||
|
private val clock: Clock = Clock.System,
|
||||||
|
private val locale: Locale = Locale.getDefault()
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val onlyTimeFormatter: DateTimeFormatter by lazy {
|
||||||
|
val pattern = DateFormat.getBestDateTimePattern(locale, "HH:mm")
|
||||||
|
DateTimeFormatter.ofPattern(pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val dateWithMonthFormatter: DateTimeFormatter by lazy {
|
||||||
|
val pattern = DateFormat.getBestDateTimePattern(locale, "d MMM")
|
||||||
|
DateTimeFormatter.ofPattern(pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val dateWithYearFormatter: DateTimeFormatter by lazy {
|
||||||
|
val pattern = DateFormat.getBestDateTimePattern(locale, "dd.MM.yyyy")
|
||||||
|
DateTimeFormatter.ofPattern(pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun format(timestamp: Long?): String {
|
||||||
|
if (timestamp == null) return ""
|
||||||
|
val now: Instant = clock.now()
|
||||||
|
val tsInstant = Instant.fromEpochSeconds(timestamp)
|
||||||
|
val nowDateTime = now.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||||
|
val tsDateTime = tsInstant.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||||
|
val isSameDay = nowDateTime.date == tsDateTime.date
|
||||||
|
return when {
|
||||||
|
isSameDay -> {
|
||||||
|
onlyTimeFormatter.format(tsDateTime.toJavaLocalDateTime())
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
formatDate(tsDateTime, nowDateTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatDate(
|
||||||
|
date: LocalDateTime,
|
||||||
|
currentDate: LocalDateTime,
|
||||||
|
): String {
|
||||||
|
val period = Period.between(date.date.toJavaLocalDate(), currentDate.date.toJavaLocalDate())
|
||||||
|
return if (period.years.absoluteValue >= 1) {
|
||||||
|
formatDateWithYear(date)
|
||||||
|
} else if (period.days.absoluteValue < 2 && period.months.absoluteValue < 1) {
|
||||||
|
getRelativeDay(date.toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds())
|
||||||
|
} else {
|
||||||
|
formatDateWithMonth(date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatDateWithMonth(localDateTime: LocalDateTime): String {
|
||||||
|
return dateWithMonthFormatter.format(localDateTime.toJavaLocalDateTime())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatDateWithYear(localDateTime: LocalDateTime): String {
|
||||||
|
return dateWithYearFormatter.format(localDateTime.toJavaLocalDateTime())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getRelativeDay(ts: Long): String {
|
||||||
|
return DateUtils.getRelativeTimeSpanString(
|
||||||
|
ts,
|
||||||
|
clock.now().toEpochMilliseconds(),
|
||||||
|
DateUtils.DAY_IN_MILLIS,
|
||||||
|
DateUtils.FORMAT_SHOW_WEEKDAY
|
||||||
|
).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package io.element.android.x.features.roomlist
|
package io.element.android.x.features.roomlist
|
||||||
|
|
||||||
sealed interface RoomListActions {
|
sealed interface RoomListActions {
|
||||||
object LoadMore : RoomListActions
|
|
||||||
object Logout : RoomListActions
|
object Logout : RoomListActions
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
package io.element.android.x.features.roomlist
|
package io.element.android.x.features.roomlist
|
||||||
|
|
||||||
import android.widget.Space
|
import Avatar
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ExitToApp
|
import androidx.compose.material.icons.filled.ExitToApp
|
||||||
import androidx.compose.material.ripple.rememberRipple
|
import androidx.compose.material.ripple.rememberRipple
|
||||||
|
|
@ -15,9 +17,11 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
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.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.airbnb.mvrx.Success
|
import com.airbnb.mvrx.Success
|
||||||
|
|
@ -25,10 +29,10 @@ 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.LightGrey
|
import io.element.android.x.designsystem.LightGrey
|
||||||
import io.element.android.x.designsystem.components.Avatar
|
|
||||||
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.RoomListViewState
|
||||||
import io.element.android.x.matrix.core.RoomId
|
import io.element.android.x.matrix.core.RoomId
|
||||||
import io.element.android.x.matrix.room.RoomSummary
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomListScreen(
|
fun RoomListScreen(
|
||||||
|
|
@ -54,7 +58,7 @@ fun RoomListScreen(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomListContent(
|
fun RoomListContent(
|
||||||
roomSummaries: List<RoomSummary>,
|
roomSummaries: List<RoomListRoomSummary>,
|
||||||
matrixUser: MatrixUser,
|
matrixUser: MatrixUser,
|
||||||
onRoomClicked: (RoomId) -> Unit,
|
onRoomClicked: (RoomId) -> Unit,
|
||||||
onLogoutClicked: () -> Unit,
|
onLogoutClicked: () -> Unit,
|
||||||
|
|
@ -69,12 +73,11 @@ fun RoomListContent(
|
||||||
onLogoutClicked = onLogoutClicked
|
onLogoutClicked = onLogoutClicked
|
||||||
)
|
)
|
||||||
LazyColumn {
|
LazyColumn {
|
||||||
items(roomSummaries, key = { it.identifier() }) { room ->
|
items(roomSummaries, key = { it.id }) { room ->
|
||||||
RoomItem(room = room) {
|
RoomItem(room = room) {
|
||||||
onRoomClicked(it)
|
onRoomClicked(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -90,7 +93,7 @@ fun RoomListTopBar(matrixUser: MatrixUser, onLogoutClicked: () -> Unit) {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Avatar(data = matrixUser.avatarData)
|
Avatar(data = matrixUser.avatarData, size = 32.dp)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text("${matrixUser.username}")
|
Text("${matrixUser.username}")
|
||||||
}
|
}
|
||||||
|
|
@ -108,18 +111,17 @@ fun RoomListTopBar(matrixUser: MatrixUser, onLogoutClicked: () -> Unit) {
|
||||||
@Composable
|
@Composable
|
||||||
private fun RoomItem(
|
private fun RoomItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
room: RoomSummary,
|
room: RoomListRoomSummary,
|
||||||
onClick: (RoomId) -> Unit
|
onClick: (RoomId) -> Unit
|
||||||
) {
|
) {
|
||||||
if (room !is RoomSummary.Filled) {
|
if (room.isPlaceholder) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val details = room.details
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(
|
.clickable(
|
||||||
onClick = { onClick(room.details.roomId) },
|
onClick = { onClick(room.roomId) },
|
||||||
indication = rememberRipple(),
|
indication = rememberRipple(),
|
||||||
interactionSource = remember { MutableInteractionSource() }
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
),
|
),
|
||||||
|
|
@ -128,12 +130,9 @@ private fun RoomItem(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Box(modifier = Modifier
|
Avatar(data = room.avatarData)
|
||||||
.align(Alignment.CenterVertically)
|
|
||||||
) {
|
|
||||||
Avatar(data = null)
|
|
||||||
}
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(12.dp)
|
.padding(12.dp)
|
||||||
|
|
@ -143,31 +142,56 @@ private fun RoomItem(
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = Color.Black,
|
color = Color.Black,
|
||||||
text = details.name,
|
text = room.name,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = details.lastMessage?.toString().orEmpty(),
|
text = room.lastMessage?.toString().orEmpty(),
|
||||||
color = LightGrey,
|
color = LightGrey,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier.padding(horizontal = 8.dp)
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
.align(Alignment.CenterVertically)
|
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
text = "14:18",
|
text = room.timestamp ?: "",
|
||||||
color = LightGrey
|
color = LightGrey,
|
||||||
|
)
|
||||||
|
Spacer(modifier.size(4.dp))
|
||||||
|
val unreadIndicatorColor = if(room.hasUnread) Color.Black else Color.Transparent
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(12.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(unreadIndicatorColor)
|
||||||
|
.align(Alignment.End),
|
||||||
)
|
)
|
||||||
Spacer(Modifier.size(20.dp))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun PreviewableRoomListContent() {
|
||||||
|
val roomSummaries = listOf(
|
||||||
|
RoomListRoomSummary(
|
||||||
|
name = "Room",
|
||||||
|
hasUnread = true,
|
||||||
|
timestamp = "14:18",
|
||||||
|
lastMessage = "A message",
|
||||||
|
avatarData = null,
|
||||||
|
id = "roomId"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
RoomListContent(
|
||||||
|
roomSummaries = roomSummaries,
|
||||||
|
matrixUser = MatrixUser("User#1"),
|
||||||
|
onRoomClicked = {},
|
||||||
|
onLogoutClicked = {}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -4,9 +4,16 @@ import com.airbnb.mvrx.Fail
|
||||||
import com.airbnb.mvrx.Loading
|
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.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.RoomListViewState
|
||||||
import io.element.android.x.matrix.MatrixClient
|
import io.element.android.x.matrix.MatrixClient
|
||||||
import io.element.android.x.matrix.MatrixInstance
|
import io.element.android.x.matrix.MatrixInstance
|
||||||
|
import io.element.android.x.matrix.room.RoomSummary
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.rustcomponents.sdk.mediaSourceFromUrl
|
import org.matrix.rustcomponents.sdk.mediaSourceFromUrl
|
||||||
|
|
||||||
|
|
@ -14,6 +21,7 @@ class RoomListViewModel(initialState: RoomListViewState) :
|
||||||
MavericksViewModel<RoomListViewState>(initialState) {
|
MavericksViewModel<RoomListViewState>(initialState) {
|
||||||
|
|
||||||
private val matrix = MatrixInstance.getInstance()
|
private val matrix = MatrixInstance.getInstance()
|
||||||
|
private val lastMessageFormatter = LastMessageFormatter()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
handleInit()
|
handleInit()
|
||||||
|
|
@ -21,12 +29,11 @@ class RoomListViewModel(initialState: RoomListViewState) :
|
||||||
|
|
||||||
fun handle(action: RoomListActions) {
|
fun handle(action: RoomListActions) {
|
||||||
when (action) {
|
when (action) {
|
||||||
RoomListActions.LoadMore -> TODO()
|
|
||||||
RoomListActions.Logout -> handleLogout()
|
RoomListActions.Logout -> handleLogout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun logout(){
|
fun logout() {
|
||||||
handleLogout()
|
handleLogout()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,26 +43,63 @@ class RoomListViewModel(initialState: RoomListViewState) :
|
||||||
client.startSync()
|
client.startSync()
|
||||||
val userAvatarUrl = client.loadUserAvatarURLString().getOrNull()
|
val userAvatarUrl = client.loadUserAvatarURLString().getOrNull()
|
||||||
val userDisplayName = client.loadUserDisplayName().getOrNull()
|
val userDisplayName = client.loadUserDisplayName().getOrNull()
|
||||||
val avatarData = userAvatarUrl?.let {
|
val avatarData = loadAvatarData(client, userAvatarUrl)
|
||||||
mediaSourceFromUrl(it)
|
|
||||||
}?.let {
|
|
||||||
client.loadMediaContentForSource(it)
|
|
||||||
}
|
|
||||||
setState {
|
setState {
|
||||||
copy(
|
copy(
|
||||||
user = MatrixUser(
|
user = MatrixUser(
|
||||||
username = userDisplayName,
|
username = userDisplayName,
|
||||||
avatarUrl = userAvatarUrl,
|
avatarUrl = userAvatarUrl,
|
||||||
avatarData = avatarData?.getOrNull()
|
avatarData = avatarData,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
client.roomSummaryDataSource().roomSummaries().execute {
|
client.roomSummaryDataSource().roomSummaries()
|
||||||
copy(rooms = it)
|
.map { roomSummaries ->
|
||||||
|
mapRoomSummaries(client, roomSummaries)
|
||||||
|
}
|
||||||
|
.flowOn(Dispatchers.Default)
|
||||||
|
.execute {
|
||||||
|
copy(rooms = it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun mapRoomSummaries(
|
||||||
|
client: MatrixClient,
|
||||||
|
roomSummaries: List<RoomSummary>
|
||||||
|
): List<RoomListRoomSummary> {
|
||||||
|
return roomSummaries.parallelMap { roomSummary ->
|
||||||
|
when (roomSummary) {
|
||||||
|
is RoomSummary.Empty -> RoomListRoomSummary(
|
||||||
|
id = roomSummary.identifier,
|
||||||
|
isPlaceholder = true
|
||||||
|
)
|
||||||
|
is RoomSummary.Filled -> {
|
||||||
|
val avatarData = loadAvatarData(client, roomSummary.details.avatarURLString)
|
||||||
|
RoomListRoomSummary(
|
||||||
|
id = roomSummary.identifier(),
|
||||||
|
name = roomSummary.details.name,
|
||||||
|
hasUnread = roomSummary.details.unreadNotificationCount > 0,
|
||||||
|
timestamp = lastMessageFormatter.format(roomSummary.details.lastMessageTimestamp),
|
||||||
|
lastMessage = roomSummary.details.lastMessage,
|
||||||
|
avatarData = avatarData,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun loadAvatarData(client: MatrixClient, url: String?, size: Long = 48): ByteArray? {
|
||||||
|
val mediaContent = url?.let {
|
||||||
|
val mediaSource = mediaSourceFromUrl(it)
|
||||||
|
client.loadMediaThumbnailForSource(mediaSource, size, size)
|
||||||
|
}
|
||||||
|
return mediaContent?.fold(
|
||||||
|
{ it },
|
||||||
|
{ null }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleLogout() {
|
private fun handleLogout() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
setState { copy(logoutAction = Loading()) }
|
setState { copy(logoutAction = Loading()) }
|
||||||
|
|
|
||||||
|
|
@ -6,5 +6,5 @@ import androidx.compose.runtime.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: List<UByte>? = null,
|
val avatarData: ByteArray? = null,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package io.element.android.x.features.roomlist.model
|
||||||
|
|
||||||
|
import io.element.android.x.matrix.core.RoomId
|
||||||
|
|
||||||
|
data class RoomListRoomSummary(
|
||||||
|
val id: String,
|
||||||
|
val roomId: RoomId = RoomId(id),
|
||||||
|
val name: String = "",
|
||||||
|
val hasUnread: Boolean = false,
|
||||||
|
val timestamp: String? = null,
|
||||||
|
val lastMessage: CharSequence? = null,
|
||||||
|
val avatarData: ByteArray? = null,
|
||||||
|
val isPlaceholder: Boolean = false,
|
||||||
|
)
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package io.element.android.x.features.roomlist
|
package io.element.android.x.features.roomlist.model
|
||||||
|
|
||||||
import com.airbnb.mvrx.Async
|
import com.airbnb.mvrx.Async
|
||||||
import com.airbnb.mvrx.MavericksState
|
import com.airbnb.mvrx.MavericksState
|
||||||
|
|
@ -8,7 +8,7 @@ import io.element.android.x.matrix.room.RoomSummary
|
||||||
|
|
||||||
data class RoomListViewState(
|
data class RoomListViewState(
|
||||||
val user: MatrixUser = MatrixUser(),
|
val user: MatrixUser = MatrixUser(),
|
||||||
val rooms: Async<List<RoomSummary>> = 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,
|
||||||
) : MavericksState
|
) : MavericksState
|
||||||
|
|
@ -33,6 +33,7 @@ test_orchestrator = "1.4.1"
|
||||||
mavericks = "3.0.1"
|
mavericks = "3.0.1"
|
||||||
timber = "5.0.1"
|
timber = "5.0.1"
|
||||||
coil = "2.2.1"
|
coil = "2.2.1"
|
||||||
|
datetime = "0.4.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
# Project
|
# Project
|
||||||
|
|
@ -70,6 +71,8 @@ test_orchestrator = { module = "androidx.test:orchestrator", version.ref = "test
|
||||||
|
|
||||||
mavericks_compose = { module = "com.airbnb.android:mavericks-compose", version.ref = "mavericks" }
|
mavericks_compose = { module = "com.airbnb.android:mavericks-compose", version.ref = "mavericks" }
|
||||||
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
|
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
|
||||||
coil_compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
|
coil_compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
|
||||||
|
datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" }
|
||||||
|
|
||||||
[bundles]
|
[bundles]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package io.element.android.x.core.data
|
||||||
|
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
|
||||||
|
suspend fun <A, B> Iterable<A>.parallelMap(f: suspend (A) -> B): List<B> = coroutineScope {
|
||||||
|
map { async { f(it) } }.awaitAll()
|
||||||
|
}
|
||||||
|
|
@ -1,37 +1,31 @@
|
||||||
package io.element.android.x.designsystem.components
|
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
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 androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil.compose.rememberAsyncImagePainter
|
import coil.compose.AsyncImage
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO fallback Avatar
|
* TODO fallback Avatar
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun Avatar(
|
fun Avatar(
|
||||||
data: List<UByte>?,
|
data: ByteArray?,
|
||||||
size: Dp = 48.dp,
|
size: Dp = 48.dp,
|
||||||
) {
|
) {
|
||||||
Image(
|
AsyncImage(
|
||||||
painter = rememberAsyncImagePainter(
|
model = data,
|
||||||
model = data?.toUByteArray()?.toByteArray(),
|
onError = {
|
||||||
onError = {
|
Log.e("TAG", "Error $it\n${it.result}", it.result.throwable)
|
||||||
Log.e("TAG", "Error $it\n${it.result}", it.result.throwable)
|
},
|
||||||
}),
|
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(size)
|
.size(size)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.border(1.5.dp, MaterialTheme.colorScheme.primary, CircleShape)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -96,10 +96,21 @@ class MatrixClient internal constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun loadMediaContentForSource(source: MediaSource): Result<List<UByte>> =
|
suspend fun loadMediaContentForSource(source: MediaSource): Result<ByteArray> =
|
||||||
withContext(dispatchers.io) {
|
withContext(dispatchers.io) {
|
||||||
runCatching {
|
runCatching {
|
||||||
client.getMediaContent(source)
|
client.getMediaContent(source).toUByteArray().toByteArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun loadMediaThumbnailForSource(
|
||||||
|
source: MediaSource,
|
||||||
|
width: Long,
|
||||||
|
height: Long
|
||||||
|
): Result<ByteArray> =
|
||||||
|
withContext(dispatchers.io) {
|
||||||
|
runCatching {
|
||||||
|
client.getMediaThumbnail(source, width.toULong(), height.toULong()).toUByteArray().toByteArray()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,5 +23,5 @@ data class RoomSummaryDetails(
|
||||||
val avatarURLString: String?,
|
val avatarURLString: String?,
|
||||||
val lastMessage: CharSequence?,
|
val lastMessage: CharSequence?,
|
||||||
val lastMessageTimestamp: Long?,
|
val lastMessageTimestamp: Long?,
|
||||||
val unreadNotificationCount: UInt,
|
val unreadNotificationCount: Int,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ internal class RustRoomSummaryDataSource(
|
||||||
name = room.name() ?: identifier,
|
name = room.name() ?: identifier,
|
||||||
isDirect = room.isDm() ?: false,
|
isDirect = room.isDm() ?: false,
|
||||||
avatarURLString = room.fullRoom()?.avatarUrl(),
|
avatarURLString = room.fullRoom()?.avatarUrl(),
|
||||||
unreadNotificationCount = room.unreadNotifications().notificationCount(),
|
unreadNotificationCount = room.unreadNotifications().notificationCount().toInt(),
|
||||||
lastMessage = latestRoomMessage?.body,
|
lastMessage = latestRoomMessage?.body,
|
||||||
lastMessageTimestamp = latestRoomMessage?.originServerTs
|
lastMessageTimestamp = latestRoomMessage?.originServerTs
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,16 @@ import composeVersion
|
||||||
import org.gradle.api.artifacts.VersionCatalog
|
import org.gradle.api.artifacts.VersionCatalog
|
||||||
|
|
||||||
fun CommonExtension<*, *, *, *>.androidConfig() {
|
fun CommonExtension<*, *, *, *>.androidConfig() {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
compileSdk = Versions.compileSdk
|
compileSdk = Versions.compileSdk
|
||||||
minSdk = Versions.minSdk
|
minSdk = Versions.minSdk
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
testOptions {
|
testOptions {
|
||||||
unitTests.isReturnDefaultValues = true
|
unitTests.isReturnDefaultValues = true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue