Merge branch 'develop' of ssh://github.com/vector-im/element-x-android-poc into develop

This commit is contained in:
ganfra 2022-12-01 20:43:00 +01:00
commit 41c1e9fcaf
17 changed files with 212 additions and 39 deletions

View file

@ -20,20 +20,27 @@ import org.matrix.rustcomponents.sdk.MessageFormat
import org.matrix.rustcomponents.sdk.MessageType
import org.matrix.rustcomponents.sdk.TimelineKey
class MessageTimelineItemStateMapper(
class MessageTimelineItemStateFactory(
private val client: MatrixClient,
private val room: MatrixRoom,
private val dispatcher: CoroutineDispatcher,
) {
suspend fun map(timelineItems: List<MatrixTimelineItem>): List<MessagesTimelineItemState> =
suspend fun create(
timelineItems: List<MatrixTimelineItem>,
highlightedEventId: String? = null,
): List<MessagesTimelineItemState> =
withContext(dispatcher) {
val messagesTimelineItemState = ArrayList<MessagesTimelineItemState>()
for (index in timelineItems.indices.reversed()) {
val currentTimelineItem = timelineItems[index]
val timelineItemState = when (currentTimelineItem) {
is MatrixTimelineItem.Event -> {
buildMessageEvent(currentTimelineItem, index, timelineItems)
buildMessageEvent(
currentTimelineItem,
index,
timelineItems,
highlightedEventId
)
}
is MatrixTimelineItem.Virtual -> MessagesTimelineItemState.Virtual(
"virtual_item_$index"
@ -48,7 +55,8 @@ class MessageTimelineItemStateMapper(
private suspend fun buildMessageEvent(
currentTimelineItem: MatrixTimelineItem.Event,
index: Int,
timelineItems: List<MatrixTimelineItem>
timelineItems: List<MatrixTimelineItem>,
highlightedEventId: String?,
): MessagesTimelineItemState.MessageEvent {
val currentSender = currentTimelineItem.event.sender()
val groupPosition =
@ -68,6 +76,7 @@ class MessageTimelineItemStateMapper(
senderAvatar = senderAvatarData,
content = currentTimelineItem.computeContent(),
isMine = currentTimelineItem.event.isOwn(),
isHighlighted = currentTimelineItem.event.eventId().orEmpty() == highlightedEventId,
groupPosition = groupPosition,
reactionsState = currentTimelineItem.computeReactionsState()
)

View file

@ -41,6 +41,7 @@ import io.element.android.x.features.messages.model.*
import io.element.android.x.features.messages.model.content.*
import io.element.android.x.features.messages.textcomposer.MessageComposerViewModel
import io.element.android.x.features.messages.textcomposer.MessageComposerViewState
import io.element.android.x.textcomposer.MessageComposerMode
import io.element.android.x.textcomposer.TextComposer
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
@ -72,6 +73,7 @@ fun MessagesScreen(
val timelineItems by viewModel.collectAsState(MessagesViewState::timelineItems)
val hasMoreToLoad by viewModel.collectAsState(MessagesViewState::hasMoreToLoad)
val snackBarContent by viewModel.collectAsState(MessagesViewState::snackbarContent)
val composerMode by viewModel.collectAsState(MessagesViewState::composerMode)
val composerFullScreen by composerViewModel.collectAsState(MessageComposerViewState::isFullScreen)
val composerCanSendMessage by composerViewModel.collectAsState(MessageComposerViewState::isSendButtonVisible)
val composerText by composerViewModel.collectAsState(MessageComposerViewState::text)
@ -87,6 +89,8 @@ fun MessagesScreen(
composerFullScreen = composerFullScreen,
onComposerFullScreenChange = composerViewModel::onComposerFullScreenChange,
onComposerTextChange = composerViewModel::updateText,
composerMode = composerMode,
onCloseSpecialMode = viewModel::setNormalMode,
composerCanSendMessage = composerCanSendMessage,
composerText = composerText,
onClick = {
@ -107,6 +111,16 @@ fun MessagesScreen(
onActionClicked = {
viewModel.handleItemAction(it)
coroutineScope.launch {
val targetEvent = viewModel.getTargetEvent()
when (it) {
is MessagesItemAction.Edit -> {
// Entering Edit mode, update the text in the composer.
val newComposerText =
(targetEvent?.content as? MessagesTimelineItemTextBasedContent)?.body.orEmpty()
composerViewModel.updateText(newComposerText)
}
else -> Unit
}
actionsSheetState.hide()
}
}
@ -133,6 +147,8 @@ fun MessagesScreenContent(
composerFullScreen: Boolean,
onComposerFullScreenChange: () -> Unit,
onComposerTextChange: (CharSequence) -> Unit,
composerMode: MessageComposerMode,
onCloseSpecialMode: () -> Unit,
composerCanSendMessage: Boolean,
composerText: StableCharSequence?,
snackbarHostState: SnackbarHostState,
@ -155,6 +171,8 @@ fun MessagesScreenContent(
onSendMessage = onSendMessage,
onClick = onClick,
onLongClick = onLongClick,
composerMode = composerMode,
onCloseSpecialMode = onCloseSpecialMode,
composerFullScreen = composerFullScreen,
onComposerFullScreenChange = onComposerFullScreenChange,
onComposerTextChange = onComposerTextChange,
@ -174,6 +192,8 @@ fun MessagesContent(
onSendMessage: (String) -> Unit,
onClick: (MessagesTimelineItemState.MessageEvent) -> Unit,
onLongClick: (MessagesTimelineItemState.MessageEvent) -> Unit,
composerMode: MessageComposerMode,
onCloseSpecialMode: () -> Unit,
composerFullScreen: Boolean,
onComposerFullScreenChange: () -> Unit,
onComposerTextChange: (CharSequence) -> Unit,
@ -202,6 +222,8 @@ fun MessagesContent(
onSendMessage = onSendMessage,
fullscreen = composerFullScreen,
onFullscreenToggle = onComposerFullScreenChange,
composerMode = composerMode,
onCloseSpecialMode = onCloseSpecialMode,
onComposerTextChange = onComposerTextChange,
composerCanSendMessage = composerCanSendMessage,
composerText = composerText?.charSequence?.toString(),
@ -363,6 +385,7 @@ fun MessageEventRow(
groupPosition = messageEvent.groupPosition,
isMine = messageEvent.isMine,
interactionSource = interactionSource,
isHighlighted = messageEvent.isHighlighted,
onClick = onClick,
onLongClick = onLongClick,
modifier = Modifier

View file

@ -10,15 +10,15 @@ import io.element.android.x.features.messages.model.MessagesItemActionsSheetStat
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.content.MessagesTimelineItemRedactedContent
import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextBasedContent
import io.element.android.x.matrix.MatrixClient
import io.element.android.x.matrix.MatrixInstance
import io.element.android.x.matrix.media.MediaResolver
import io.element.android.x.matrix.room.MatrixRoom
import io.element.android.x.matrix.timeline.MatrixTimeline
import io.element.android.x.textcomposer.MessageComposerMode
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
@ -28,7 +28,7 @@ class MessagesViewModel(
private val client: MatrixClient,
private val room: MatrixRoom,
private val timeline: MatrixTimeline,
private val messageTimelineItemStateMapper: MessageTimelineItemStateMapper,
private val messageTimelineItemStateFactory: MessageTimelineItemStateFactory,
private val initialState: MessagesViewState
) :
MavericksViewModel<MessagesViewState>(initialState) {
@ -42,13 +42,13 @@ class MessagesViewModel(
val matrix = MatrixInstance.getInstance()
val client = matrix.activeClient()
val room = client.getRoom(state.roomId) ?: return null
val messageTimelineItemStateMapper =
MessageTimelineItemStateMapper(client, room, Dispatchers.Default)
val messageTimelineItemStateFactory =
MessageTimelineItemStateFactory(client, room, Dispatchers.Default)
return MessagesViewModel(
client,
room,
room.timeline(),
messageTimelineItemStateMapper,
messageTimelineItemStateFactory,
state
)
}
@ -66,25 +66,72 @@ class MessagesViewModel(
}
fun sendMessage(text: String) {
viewModelScope.launch {
timeline.sendMessage(text)
withState { state ->
viewModelScope.launch {
when (state.composerMode) {
is MessageComposerMode.Normal -> timeline.sendMessage(text)
is MessageComposerMode.Edit -> timeline.editMessage(
state.composerMode.eventId,
text
)
is MessageComposerMode.Quote -> TODO()
is MessageComposerMode.Reply -> timeline.replyMessage(
state.composerMode.eventId,
text
)
}
// Reset composer
setNormalMode()
}
}
}
suspend fun getTargetEvent(): MessagesTimelineItemState.MessageEvent? {
val currentState = awaitState()
return currentState.itemActionsSheetState.invoke()?.targetItem
}
fun handleItemAction(action: MessagesItemAction) {
viewModelScope.launch(Dispatchers.Default) {
val currentState = awaitState()
Timber.v("Handle $action for ${currentState.itemActionsSheetState}")
val targetEvent =
currentState.itemActionsSheetState.invoke()?.targetItem ?: return@launch
val targetEvent = getTargetEvent()
?: return@launch
when (action) {
MessagesItemAction.Copy -> notImplementedYet()
MessagesItemAction.Forward -> notImplementedYet()
MessagesItemAction.Redact -> handleActionRedact(targetEvent)
MessagesItemAction.Edit -> handleActionEdit(targetEvent)
MessagesItemAction.Reply -> handleActionReply(targetEvent)
}
}
}
fun setNormalMode() {
setComposerMode(MessageComposerMode.Normal(""))
}
private fun handleActionEdit(targetEvent: MessagesTimelineItemState.MessageEvent) {
setComposerMode(
MessageComposerMode.Edit(
targetEvent.id,
(targetEvent.content as? MessagesTimelineItemTextBasedContent)?.body.orEmpty()
)
)
}
private fun handleActionReply(targetEvent: MessagesTimelineItemState.MessageEvent) {
setComposerMode(MessageComposerMode.Reply(targetEvent.safeSenderName, targetEvent.id, ""))
}
private fun setComposerMode(mode: MessageComposerMode) {
setState {
copy(
composerMode = mode
)
}
}
private fun notImplementedYet() {
setSnackbarContent("Not implemented yet!")
}
@ -110,10 +157,12 @@ class MessagesViewModel(
emptyList()
} else {
mutableListOf(
MessagesItemAction.Reply,
MessagesItemAction.Forward,
MessagesItemAction.Copy,
).also {
if (messagesTimelineItemState.isMine) {
it.add(MessagesItemAction.Edit)
it.add(MessagesItemAction.Redact)
}
}
@ -140,8 +189,20 @@ class MessagesViewModel(
}
}.launchIn(viewModelScope)
timeline.timelineItems()
.map(messageTimelineItemStateMapper::map)
combine(
timeline.timelineItems(),
stateFlow.map {
when (it.composerMode) {
is MessageComposerMode.Normal -> null
is MessageComposerMode.Edit -> it.composerMode.eventId
is MessageComposerMode.Quote -> null
is MessageComposerMode.Reply -> it.composerMode.eventId
}
}
.distinctUntilChanged()
) { timelineItems, highlightedEventId ->
messageTimelineItemStateFactory.create(timelineItems, highlightedEventId)
}
.execute {
copy(timelineItems = it)
}

View file

@ -9,7 +9,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Shape
@ -25,6 +24,7 @@ fun MessageEventBubble(
groupPosition: MessagesItemGroupPosition,
isMine: Boolean,
interactionSource: MutableInteractionSource,
isHighlighted: Boolean,
modifier: Modifier = Modifier,
onClick: () -> Unit,
onLongClick: () -> Unit,
@ -65,17 +65,25 @@ fun MessageEventBubble(
}
}
val backgroundBubbleColor = if (isMine) {
val backgroundBubbleColor = if (isHighlighted) {
if (LocalIsDarkTheme.current) {
SystemGrey5Dark
MessageHighlightDark
} else {
SystemGrey5Light
MessageHighlightLight
}
} else {
if (LocalIsDarkTheme.current) {
SystemGrey6Dark
if (isMine) {
if (LocalIsDarkTheme.current) {
SystemGrey5Dark
} else {
SystemGrey5Light
}
} else {
SystemGrey6Light
if (LocalIsDarkTheme.current) {
SystemGrey6Dark
} else {
SystemGrey6Light
}
}
}
val bubbleShape = bubbleShape()

View file

@ -13,4 +13,6 @@ sealed class MessagesItemAction(
object Forward : MessagesItemAction("Forward", VectorIcons.ArrowForward)
object Copy : MessagesItemAction("Copy", VectorIcons.Copy)
object Redact : MessagesItemAction("Redact", VectorIcons.Delete, destructive = true)
object Reply : MessagesItemAction("Reply", VectorIcons.Reply)
object Edit : MessagesItemAction("Edit", VectorIcons.Edit)
}

View file

@ -16,6 +16,7 @@ sealed interface MessagesTimelineItemState {
val content: MessagesTimelineItemContent,
val sentTime: String = "",
val isMine: Boolean = false,
val isHighlighted: Boolean = false,
val groupPosition: MessagesItemGroupPosition = MessagesItemGroupPosition.None,
val reactionsState: MessagesItemReactionState
) : MessagesTimelineItemState {

View file

@ -4,6 +4,7 @@ import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.Uninitialized
import io.element.android.x.designsystem.components.avatar.AvatarData
import io.element.android.x.textcomposer.MessageComposerMode
data class MessagesViewState(
val roomId: String,
@ -13,6 +14,8 @@ data class MessagesViewState(
val hasMoreToLoad: Boolean = true,
val itemActionsSheetState: Async<MessagesItemActionsSheetState> = Uninitialized,
val snackbarContent: String? = null,
// TODO Highlight item in reply / edit in the timeline
val composerMode: MessageComposerMode = MessageComposerMode.Normal(""),
) : MavericksState {
@Suppress("unused")