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
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 ?: "",
)
}
}

View File

@@ -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)
}

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 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")

View File

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