Start implementing an ugly timeline
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
package io.element.android.x.features.messages
|
||||
|
||||
import Avatar
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
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.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -18,14 +20,16 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
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.sp
|
||||
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.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.matrix.timeline.MatrixTimelineItem
|
||||
|
||||
@Composable
|
||||
fun MessagesScreen(roomId: String) {
|
||||
@@ -48,7 +52,7 @@ fun MessagesScreen(roomId: String) {
|
||||
fun MessagesContent(
|
||||
roomTitle: String?,
|
||||
roomAvatar: AvatarData?,
|
||||
timelineItems: List<MatrixTimelineItem>,
|
||||
timelineItems: List<MessagesTimelineItemState>,
|
||||
hasMoreToLoad: Boolean,
|
||||
onReachedLoadMore: () -> Unit,
|
||||
) {
|
||||
@@ -83,7 +87,7 @@ fun MessagesContent(
|
||||
fun TimelineItems(
|
||||
padding: PaddingValues,
|
||||
lazyListState: LazyListState,
|
||||
timelineItems: List<MatrixTimelineItem>,
|
||||
timelineItems: List<MessagesTimelineItemState>,
|
||||
hasMoreToLoad: Boolean,
|
||||
onReachedLoadMore: () -> Unit,
|
||||
) {
|
||||
@@ -110,36 +114,92 @@ fun TimelineItems(
|
||||
|
||||
@Composable
|
||||
fun TimelineItemRow(
|
||||
timelineItem: MatrixTimelineItem
|
||||
timelineItem: MessagesTimelineItemState
|
||||
) {
|
||||
when (timelineItem) {
|
||||
MatrixTimelineItem.Other -> return
|
||||
MatrixTimelineItem.Virtual -> return
|
||||
is MatrixTimelineItem.Event -> {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(
|
||||
onClick = { },
|
||||
indication = rememberRipple(),
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
.height(IntrinsicSize.Min),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
fontSize = 14.sp,
|
||||
text = timelineItem.event.raw() ?: "",
|
||||
)
|
||||
is MessagesTimelineItemState.Virtual -> return
|
||||
is MessagesTimelineItemState.MessageEvent -> MessageEventRow(messageEvent = timelineItem)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MessageEventRow(
|
||||
messageEvent: MessagesTimelineItemState.MessageEvent,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val contentAlignment = if (messageEvent.isMine) {
|
||||
Alignment.CenterEnd
|
||||
} else {
|
||||
Alignment.CenterStart
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
contentAlignment = contentAlignment
|
||||
) {
|
||||
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 ?: "",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,17 +5,23 @@ import com.airbnb.mvrx.MavericksViewModelFactory
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import io.element.android.x.designsystem.components.avatar.AvatarData
|
||||
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.matrix.MatrixClient
|
||||
import io.element.android.x.matrix.MatrixInstance
|
||||
import io.element.android.x.matrix.room.MatrixRoom
|
||||
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.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.rustcomponents.sdk.MessageType
|
||||
import org.matrix.rustcomponents.sdk.mediaSourceFromUrl
|
||||
|
||||
private const val PAGINATION_COUNT = 50
|
||||
|
||||
class MessagesViewModel(
|
||||
private val client: MatrixClient,
|
||||
private val room: MatrixRoom,
|
||||
@@ -35,15 +41,13 @@ class MessagesViewModel(
|
||||
val room = client.getRoom(state.roomId) ?: return null
|
||||
return MessagesViewModel(client, room, room.timeline(), state)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
init {
|
||||
handleInit()
|
||||
}
|
||||
|
||||
fun loadMore(){
|
||||
fun loadMore() {
|
||||
viewModelScope.launch {
|
||||
timeline.paginateBackwards(PAGINATION_COUNT)
|
||||
setState { copy(hasMoreToLoad = timeline.hasMoreToLoad) }
|
||||
@@ -51,10 +55,6 @@ class MessagesViewModel(
|
||||
}
|
||||
|
||||
private fun handleInit() {
|
||||
setState {
|
||||
copy(hasMoreToLoad = timeline.hasMoreToLoad)
|
||||
}
|
||||
|
||||
room.syncUpdateFlow()
|
||||
.onEach {
|
||||
val avatarData =
|
||||
@@ -67,6 +67,42 @@ class MessagesViewModel(
|
||||
}.launchIn(viewModelScope)
|
||||
|
||||
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 {
|
||||
copy(timelineItems = it)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ data class MessagesViewState(
|
||||
val roomId: String,
|
||||
val roomName: String? = null,
|
||||
val roomAvatar: AvatarData? = null,
|
||||
val timelineItems: Async<List<MatrixTimelineItem>> = Uninitialized,
|
||||
val hasMoreToLoad: Boolean = false,
|
||||
val timelineItems: Async<List<MessagesTimelineItemState>> = Uninitialized,
|
||||
val hasMoreToLoad: Boolean = true,
|
||||
) : MavericksState {
|
||||
|
||||
@Suppress("unused")
|
||||
|
||||
@@ -31,7 +31,7 @@ class MatrixTimeline(
|
||||
|
||||
fun timelineItems(): Flow<List<MatrixTimelineItem>> {
|
||||
return diffFlow().combine(timelineItems) { _, _ ->
|
||||
timelineItems.value.reversed()
|
||||
timelineItems.value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user