From ae051f0885529948a737d3eced7e55ed8bbe6a5e Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 7 Nov 2022 22:24:43 +0100 Subject: [PATCH] Start implementing an ugly timeline --- .../x/features/messages/MessagesScreen.kt | 116 +++++++++++++----- .../x/features/messages/MessagesViewModel.kt | 50 ++++++-- .../model/MessagesItemGroupPosition.kt | 17 +++ .../model/MessagesTimelineItemState.kt | 23 ++++ .../messages/model/MessagesViewState.kt | 4 +- .../x/matrix/timeline/MatrixTimeline.kt | 2 +- 6 files changed, 174 insertions(+), 38 deletions(-) create mode 100644 features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesItemGroupPosition.kt create mode 100644 features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesTimelineItemState.kt diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesScreen.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesScreen.kt index f08529a2bd..54282c8e49 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesScreen.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesScreen.kt @@ -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, + timelineItems: List, hasMoreToLoad: Boolean, onReachedLoadMore: () -> Unit, ) { @@ -83,7 +87,7 @@ fun MessagesContent( fun TimelineItems( padding: PaddingValues, lazyListState: LazyListState, - timelineItems: List, + timelineItems: List, 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 ?: "", + ) } } diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesViewModel.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesViewModel.kt index be37e5326f..7fdf2739c9 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesViewModel.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesViewModel.kt @@ -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() + 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) } diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesItemGroupPosition.kt b/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesItemGroupPosition.kt new file mode 100644 index 0000000000..c9c884b196 --- /dev/null +++ b/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesItemGroupPosition.kt @@ -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 + } + } + + +} diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesTimelineItemState.kt b/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesTimelineItemState.kt new file mode 100644 index 0000000000..50ca2d487e --- /dev/null +++ b/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesTimelineItemState.kt @@ -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 + +} + + + diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesViewState.kt b/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesViewState.kt index b739011eec..fe0368a0b3 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesViewState.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesViewState.kt @@ -10,8 +10,8 @@ data class MessagesViewState( val roomId: String, val roomName: String? = null, val roomAvatar: AvatarData? = null, - val timelineItems: Async> = Uninitialized, - val hasMoreToLoad: Boolean = false, + val timelineItems: Async> = Uninitialized, + val hasMoreToLoad: Boolean = true, ) : MavericksState { @Suppress("unused") diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/MatrixTimeline.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/MatrixTimeline.kt index e7c06c4af3..10c372dd35 100644 --- a/libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/MatrixTimeline.kt +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/MatrixTimeline.kt @@ -31,7 +31,7 @@ class MatrixTimeline( fun timelineItems(): Flow> { return diffFlow().combine(timelineItems) { _, _ -> - timelineItems.value.reversed() + timelineItems.value } }