Start implementing an ugly timeline

This commit is contained in:
ganfra 2022-11-07 22:24:43 +01:00
parent 7907fce9e5
commit ae051f0885
6 changed files with 174 additions and 38 deletions

View file

@ -3,6 +3,7 @@
package io.element.android.x.features.messages package io.element.android.x.features.messages
import Avatar import Avatar
import androidx.compose.foundation.BorderStroke
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.*
@ -10,6 +11,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ripple.rememberRipple import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -18,14 +20,16 @@ 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.graphics.Color
import androidx.compose.ui.layout.LastBaseline
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
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.designsystem.components.avatar.AvatarData
import io.element.android.x.features.messages.model.MessagesItemGroupPosition
import io.element.android.x.features.messages.model.MessagesTimelineItemState
import io.element.android.x.features.messages.model.MessagesViewState import io.element.android.x.features.messages.model.MessagesViewState
import io.element.android.x.matrix.timeline.MatrixTimelineItem
@Composable @Composable
fun MessagesScreen(roomId: String) { fun MessagesScreen(roomId: String) {
@ -48,7 +52,7 @@ fun MessagesScreen(roomId: String) {
fun MessagesContent( fun MessagesContent(
roomTitle: String?, roomTitle: String?,
roomAvatar: AvatarData?, roomAvatar: AvatarData?,
timelineItems: List<MatrixTimelineItem>, timelineItems: List<MessagesTimelineItemState>,
hasMoreToLoad: Boolean, hasMoreToLoad: Boolean,
onReachedLoadMore: () -> Unit, onReachedLoadMore: () -> Unit,
) { ) {
@ -83,7 +87,7 @@ fun MessagesContent(
fun TimelineItems( fun TimelineItems(
padding: PaddingValues, padding: PaddingValues,
lazyListState: LazyListState, lazyListState: LazyListState,
timelineItems: List<MatrixTimelineItem>, timelineItems: List<MessagesTimelineItemState>,
hasMoreToLoad: Boolean, hasMoreToLoad: Boolean,
onReachedLoadMore: () -> Unit, onReachedLoadMore: () -> Unit,
) { ) {
@ -110,36 +114,92 @@ fun TimelineItems(
@Composable @Composable
fun TimelineItemRow( fun TimelineItemRow(
timelineItem: MatrixTimelineItem timelineItem: MessagesTimelineItemState
) { ) {
when (timelineItem) { when (timelineItem) {
MatrixTimelineItem.Other -> return is MessagesTimelineItemState.Virtual -> return
MatrixTimelineItem.Virtual -> return is MessagesTimelineItemState.MessageEvent -> MessageEventRow(messageEvent = timelineItem)
is MatrixTimelineItem.Event -> { }
Column( }
modifier = Modifier
.fillMaxWidth() @Composable
.clickable( fun MessageEventRow(
onClick = { }, messageEvent: MessagesTimelineItemState.MessageEvent,
indication = rememberRipple(), modifier: Modifier = Modifier
interactionSource = remember { MutableInteractionSource() } ) {
), val contentAlignment = if (messageEvent.isMine) {
) { Alignment.CenterEnd
Row( } else {
modifier = Modifier Alignment.CenterStart
.fillMaxWidth() }
.padding(16.dp) Box(
.height(IntrinsicSize.Min), modifier = Modifier
verticalAlignment = Alignment.CenterVertically .fillMaxWidth()
) { .wrapContentHeight(),
Text( contentAlignment = contentAlignment
fontSize = 14.sp, ) {
text = timelineItem.event.raw() ?: "", Row(modifier = modifier
) .widthIn(max = 300.dp)
.clickable(
onClick = { },
indication = rememberRipple(),
interactionSource = remember { MutableInteractionSource() }
)) {
if (!messageEvent.isMine) {
Spacer(modifier = Modifier.width(16.dp))
}
Column {
if (messageEvent.groupPosition.showSenderInformation() && !messageEvent.isMine) {
MessageSenderInformation(messageEvent.sender, messageEvent.senderAvatar)
} }
MessageEventBubble(messageEvent)
}
if (messageEvent.isMine) {
Spacer(modifier = Modifier.width(16.dp))
} }
} }
}
if (messageEvent.groupPosition is MessagesItemGroupPosition.First) {
Spacer(modifier = Modifier.height(8.dp))
} else {
Spacer(modifier = Modifier.height(4.dp))
}
}
@Composable
private fun MessageSenderInformation(sender: String, senderAvatar: AvatarData?) {
Row {
if (senderAvatar != null) {
Avatar(senderAvatar)
Spacer(modifier = Modifier.width(8.dp))
}
Text(
text = sender,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier
.alignBy(LastBaseline)
.paddingFrom(LastBaseline, after = 8.dp)
)
}
}
@Composable
fun MessageEventBubble(
messageEvent: MessagesTimelineItemState.MessageEvent,
) {
val backgroundBubbleColor = if (messageEvent.isMine) {
MaterialTheme.colorScheme.surfaceVariant
} else {
MaterialTheme.colorScheme.primary
}
Surface(
color = backgroundBubbleColor,
shape = RoundedCornerShape(20.dp, 20.dp, 20.dp, 20.dp),
) {
Text(
modifier = Modifier.padding(16.dp),
text = messageEvent.content ?: "",
)
} }
} }

View file

@ -5,17 +5,23 @@ import com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import io.element.android.x.designsystem.components.avatar.AvatarData import io.element.android.x.designsystem.components.avatar.AvatarData
import io.element.android.x.designsystem.components.avatar.AvatarSize import io.element.android.x.designsystem.components.avatar.AvatarSize
import io.element.android.x.features.messages.model.MessagesItemGroupPosition
import io.element.android.x.features.messages.model.MessagesTimelineItemState
import io.element.android.x.features.messages.model.MessagesViewState import io.element.android.x.features.messages.model.MessagesViewState
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.MatrixRoom import io.element.android.x.matrix.room.MatrixRoom
import io.element.android.x.matrix.timeline.MatrixTimeline import io.element.android.x.matrix.timeline.MatrixTimeline
import io.element.android.x.matrix.timeline.MatrixTimelineItem
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.rustcomponents.sdk.MessageType
import org.matrix.rustcomponents.sdk.mediaSourceFromUrl import org.matrix.rustcomponents.sdk.mediaSourceFromUrl
private const val PAGINATION_COUNT = 50 private const val PAGINATION_COUNT = 50
class MessagesViewModel( class MessagesViewModel(
private val client: MatrixClient, private val client: MatrixClient,
private val room: MatrixRoom, private val room: MatrixRoom,
@ -35,15 +41,13 @@ class MessagesViewModel(
val room = client.getRoom(state.roomId) ?: return null val room = client.getRoom(state.roomId) ?: return null
return MessagesViewModel(client, room, room.timeline(), state) return MessagesViewModel(client, room, room.timeline(), state)
} }
} }
init { init {
handleInit() handleInit()
} }
fun loadMore(){ fun loadMore() {
viewModelScope.launch { viewModelScope.launch {
timeline.paginateBackwards(PAGINATION_COUNT) timeline.paginateBackwards(PAGINATION_COUNT)
setState { copy(hasMoreToLoad = timeline.hasMoreToLoad) } setState { copy(hasMoreToLoad = timeline.hasMoreToLoad) }
@ -51,10 +55,6 @@ class MessagesViewModel(
} }
private fun handleInit() { private fun handleInit() {
setState {
copy(hasMoreToLoad = timeline.hasMoreToLoad)
}
room.syncUpdateFlow() room.syncUpdateFlow()
.onEach { .onEach {
val avatarData = val avatarData =
@ -67,6 +67,42 @@ class MessagesViewModel(
}.launchIn(viewModelScope) }.launchIn(viewModelScope)
timeline.timelineItems() timeline.timelineItems()
.map { timelineItems ->
val messagesTimelineItemState = ArrayList<MessagesTimelineItemState>()
for (index in timelineItems.indices.reversed()) {
val currentTimelineItem = timelineItems[index]
val timelineItemState = when (currentTimelineItem) {
is MatrixTimelineItem.Event -> {
val prevTimelineItem = timelineItems.getOrNull(index - 1)
val nextTimelineItem = timelineItems.getOrNull(index + 1)
val messageType =
currentTimelineItem.event.content().asMessage()?.msgtype()
val contentStr = when (messageType) {
is MessageType.Emote -> messageType.content.body
is MessageType.Image -> messageType.content.body
is MessageType.Notice -> messageType.content.body
is MessageType.Text -> messageType.content.body
null -> null
}
MessagesTimelineItemState.MessageEvent(
id = currentTimelineItem.event.eventId() ?: "",
sender = currentTimelineItem.event.sender(),
content = contentStr,
isMine = currentTimelineItem.event.sender() == client.userId().value,
groupPosition = MessagesItemGroupPosition.None
)
}
is MatrixTimelineItem.Virtual -> MessagesTimelineItemState.Virtual(
"virtual_item_$index"
)
MatrixTimelineItem.Other -> continue
}
messagesTimelineItemState.add(timelineItemState)
}
messagesTimelineItemState
}
.execute { .execute {
copy(timelineItems = it) copy(timelineItems = it)
} }

View file

@ -0,0 +1,17 @@
package io.element.android.x.features.messages.model
sealed interface MessagesItemGroupPosition {
object First : MessagesItemGroupPosition
object Middle : MessagesItemGroupPosition
object Last : MessagesItemGroupPosition
object None : MessagesItemGroupPosition
fun showSenderInformation(): Boolean {
return when (this) {
First, None -> true
else -> false
}
}
}

View file

@ -0,0 +1,23 @@
package io.element.android.x.features.messages.model
import io.element.android.x.designsystem.components.avatar.AvatarData
sealed interface MessagesTimelineItemState {
data class Virtual(
val id: String
) : MessagesTimelineItemState
data class MessageEvent(
val id: String = "",
val sender: String = "",
val senderAvatar: AvatarData? = null,
val content: String? = null,
val sentTime: String = "",
val isMine: Boolean = false,
val groupPosition: MessagesItemGroupPosition = MessagesItemGroupPosition.None
) : MessagesTimelineItemState
}

View file

@ -10,8 +10,8 @@ data class MessagesViewState(
val roomId: String, val roomId: String,
val roomName: String? = null, val roomName: String? = null,
val roomAvatar: AvatarData? = null, val roomAvatar: AvatarData? = null,
val timelineItems: Async<List<MatrixTimelineItem>> = Uninitialized, val timelineItems: Async<List<MessagesTimelineItemState>> = Uninitialized,
val hasMoreToLoad: Boolean = false, val hasMoreToLoad: Boolean = true,
) : MavericksState { ) : MavericksState {
@Suppress("unused") @Suppress("unused")

View file

@ -31,7 +31,7 @@ class MatrixTimeline(
fun timelineItems(): Flow<List<MatrixTimelineItem>> { fun timelineItems(): Flow<List<MatrixTimelineItem>> {
return diffFlow().combine(timelineItems) { _, _ -> return diffFlow().combine(timelineItems) { _, _ ->
timelineItems.value.reversed() timelineItems.value
} }
} }