Merge branch 'develop' of ssh://github.com/vector-im/element-x-android-poc into develop
This commit is contained in:
commit
41c1e9fcaf
17 changed files with 212 additions and 39 deletions
|
|
@ -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()
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue