Composer: Edit and reply.
TODO: call renderComposerMode and highlight selected item.
This commit is contained in:
committed by
Benoit Marty
parent
d2ab0872ec
commit
232cabcb27
@@ -40,6 +40,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
|
||||
@@ -71,6 +72,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)
|
||||
@@ -86,6 +88,8 @@ fun MessagesScreen(
|
||||
composerFullScreen = composerFullScreen,
|
||||
onComposerFullScreenChange = composerViewModel::onComposerFullScreenChange,
|
||||
onComposerTextChange = composerViewModel::updateText,
|
||||
composerMode = composerMode,
|
||||
onCloseSpecialMode = viewModel::setNormalMode,
|
||||
composerCanSendMessage = composerCanSendMessage,
|
||||
composerText = composerText,
|
||||
onClick = {
|
||||
@@ -106,6 +110,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()
|
||||
}
|
||||
}
|
||||
@@ -132,6 +146,8 @@ fun MessagesScreenContent(
|
||||
composerFullScreen: Boolean,
|
||||
onComposerFullScreenChange: () -> Unit,
|
||||
onComposerTextChange: (CharSequence) -> Unit,
|
||||
composerMode: MessageComposerMode,
|
||||
onCloseSpecialMode: () -> Unit,
|
||||
composerCanSendMessage: Boolean,
|
||||
composerText: StableCharSequence?,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
@@ -154,6 +170,8 @@ fun MessagesScreenContent(
|
||||
onSendMessage = onSendMessage,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
composerMode = composerMode,
|
||||
onCloseSpecialMode = onCloseSpecialMode,
|
||||
composerFullScreen = composerFullScreen,
|
||||
onComposerFullScreenChange = onComposerFullScreenChange,
|
||||
onComposerTextChange = onComposerTextChange,
|
||||
@@ -173,6 +191,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,
|
||||
@@ -201,6 +221,8 @@ fun MessagesContent(
|
||||
onSendMessage = onSendMessage,
|
||||
fullscreen = composerFullScreen,
|
||||
onFullscreenToggle = onComposerFullScreenChange,
|
||||
composerMode = composerMode,
|
||||
onCloseSpecialMode = onCloseSpecialMode,
|
||||
onComposerTextChange = onComposerTextChange,
|
||||
composerCanSendMessage = composerCanSendMessage,
|
||||
composerText = composerText?.charSequence?.toString(),
|
||||
|
||||
@@ -10,11 +10,13 @@ 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
|
||||
@@ -66,25 +68,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 +159,12 @@ class MessagesViewModel(
|
||||
emptyList()
|
||||
} else {
|
||||
mutableListOf(
|
||||
MessagesItemAction.Reply,
|
||||
MessagesItemAction.Forward,
|
||||
MessagesItemAction.Copy,
|
||||
).also {
|
||||
if (messagesTimelineItemState.isMine) {
|
||||
it.add(MessagesItemAction.Edit)
|
||||
it.add(MessagesItemAction.Redact)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -6,4 +6,6 @@ object VectorIcons {
|
||||
val Copy = R.drawable.ic_content_copy
|
||||
val ArrowForward = R.drawable.ic_content_arrow_forward
|
||||
val Delete = R.drawable.ic_baseline_delete_outline_24
|
||||
val Reply = R.drawable.ic_baseline_reply_24
|
||||
val Edit = R.drawable.ic_baseline_edit_24
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:tint="#000000" android:viewportHeight="24"
|
||||
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M10,9V5l-7,7 7,7v-4.1c5,0 8.5,1.6 11,5.1 -1,-5 -4,-10 -11,-11z"/>
|
||||
</vector>
|
||||
@@ -82,6 +82,22 @@ class MatrixRoom(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun editMessage(originalEventId: String, message: String): Result<Unit> = withContext(coroutineDispatchers.io) {
|
||||
val transactionId = genTransactionId()
|
||||
val content = messageEventContentFromMarkdown(message)
|
||||
runCatching {
|
||||
room.edit(/* TODO use content */ message, originalEventId, transactionId)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun replyMessage(eventId: String, message: String): Result<Unit> = withContext(coroutineDispatchers.io) {
|
||||
val transactionId = genTransactionId()
|
||||
val content = messageEventContentFromMarkdown(message)
|
||||
runCatching {
|
||||
room.sendReply(/* TODO use content */ message, eventId, transactionId)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun redactEvent(eventId: String, reason: String? = null, ) = withContext(coroutineDispatchers.io) {
|
||||
val transactionId = genTransactionId()
|
||||
runCatching {
|
||||
|
||||
@@ -124,6 +124,14 @@ class MatrixTimeline(
|
||||
return matrixRoom.sendMessage(message)
|
||||
}
|
||||
|
||||
suspend fun editMessage(originalEventId: String, message: String): Result<Unit> {
|
||||
return matrixRoom.editMessage(originalEventId, message = message)
|
||||
}
|
||||
|
||||
suspend fun replyMessage(inReplyToEventId: String, message: String): Result<Unit> {
|
||||
return matrixRoom.replyMessage(inReplyToEventId, message)
|
||||
}
|
||||
|
||||
override fun onUpdate(update: TimelineDiff) {
|
||||
coroutineScope.launch {
|
||||
updateTimelineItems {
|
||||
|
||||
@@ -19,8 +19,18 @@ package io.element.android.x.textcomposer
|
||||
sealed interface MessageComposerMode {
|
||||
data class Normal(val content: CharSequence?) : MessageComposerMode
|
||||
|
||||
sealed class Special(open val event: Any /* TODO set correct type here */, open val defaultContent: CharSequence) : MessageComposerMode
|
||||
data class Edit(override val event: Any, override val defaultContent: CharSequence) : Special(event, defaultContent)
|
||||
class Quote(override val event: Any, override val defaultContent: CharSequence) : Special(event, defaultContent)
|
||||
class Reply(override val event: Any, override val defaultContent: CharSequence) : Special(event, defaultContent)
|
||||
sealed class Special(open val eventId: String, open val defaultContent: CharSequence) :
|
||||
MessageComposerMode
|
||||
|
||||
data class Edit(override val eventId: String, override val defaultContent: CharSequence) :
|
||||
Special(eventId, defaultContent)
|
||||
|
||||
class Quote(override val eventId: String, override val defaultContent: CharSequence) :
|
||||
Special(eventId, defaultContent)
|
||||
|
||||
class Reply(
|
||||
val senderName: String,
|
||||
override val eventId: String,
|
||||
override val defaultContent: CharSequence
|
||||
) : Special(eventId, defaultContent)
|
||||
}
|
||||
|
||||
@@ -506,10 +506,7 @@ class RichTextComposerLayout @JvmOverloads constructor(
|
||||
views.composerModeIconView.setImageResource(R.drawable.ic_quote)
|
||||
}
|
||||
is MessageComposerMode.Reply -> {
|
||||
// TODO We need sender info
|
||||
// val senderInfo = mode.event.senderInfo
|
||||
val userName =
|
||||
"TODO Sender name" // senderInfo.displayName ?: senderInfo.disambiguatedDisplayName
|
||||
val userName = mode.senderName
|
||||
views.composerModeTitleView.text =
|
||||
resources.getString(R.string.replying_to, userName)
|
||||
views.composerModeIconView.setImageResource(R.drawable.ic_reply)
|
||||
|
||||
@@ -26,6 +26,8 @@ fun TextComposer(
|
||||
modifier: Modifier = Modifier,
|
||||
fullscreen: Boolean,
|
||||
onFullscreenToggle: () -> Unit,
|
||||
composerMode: MessageComposerMode,
|
||||
onCloseSpecialMode: () -> Unit,
|
||||
onComposerTextChange: (CharSequence) -> Unit,
|
||||
composerCanSendMessage: Boolean,
|
||||
composerText: String?,
|
||||
@@ -50,6 +52,7 @@ fun TextComposer(
|
||||
}
|
||||
|
||||
override fun onCloseRelatedMessage() {
|
||||
onCloseSpecialMode()
|
||||
}
|
||||
|
||||
override fun onSendMessage(text: CharSequence) {
|
||||
@@ -70,7 +73,7 @@ fun TextComposer(
|
||||
}
|
||||
setFullScreen(fullscreen, true)
|
||||
(this as MessageComposerView).apply {
|
||||
setup(isInDarkMode)
|
||||
setup(isInDarkMode, composerMode)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -83,6 +86,7 @@ fun TextComposer(
|
||||
// Example of Compose -> View communication
|
||||
val messageComposerView = (view as MessageComposerView)
|
||||
view.setFullScreen(fullscreen, false)
|
||||
// TODO messageComposerView.renderComposerMode(composerMode)
|
||||
messageComposerView.sendButton.isInvisible = !composerCanSendMessage
|
||||
messageComposerView.setTextIfDifferent(composerText ?: "")
|
||||
}
|
||||
@@ -107,7 +111,7 @@ private fun FakeComposer(modifier: Modifier) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun MessageComposerView.setup(isDarkMode: Boolean) {
|
||||
private fun MessageComposerView.setup(isDarkMode: Boolean, composerMode: MessageComposerMode) {
|
||||
val editTextColor = if (isDarkMode) {
|
||||
Color.WHITE
|
||||
} else {
|
||||
@@ -118,6 +122,7 @@ private fun MessageComposerView.setup(isDarkMode: Boolean) {
|
||||
editText.setHint(ElementR.string.room_message_placeholder)
|
||||
emojiButton?.isVisible = true
|
||||
sendButton.isVisible = true
|
||||
// TODO renderComposerMode(composerMode)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@@ -128,6 +133,8 @@ fun TextComposerPreview() {
|
||||
fullscreen = false,
|
||||
onFullscreenToggle = { },
|
||||
onComposerTextChange = {},
|
||||
composerMode = MessageComposerMode.Normal(""),
|
||||
onCloseSpecialMode = {},
|
||||
composerCanSendMessage = true,
|
||||
composerText = "Message",
|
||||
)
|
||||
|
||||
@@ -8,6 +8,16 @@
|
||||
android:orientation="vertical"
|
||||
android:background="@drawable/bg_composer_rich_bottom_sheet">
|
||||
|
||||
<!--
|
||||
There are issues here:
|
||||
|
||||
View class androidx.appcompat.widget.AppCompatImageView is an AppCompat widget that can only be used with a Theme.AppCompat theme (or descendant).
|
||||
View class io.element.android.wysiwyg.EditorEditText is an AppCompat widget that can only be used with a Theme.AppCompat theme (or descendant).
|
||||
layout_constraintHeight_default="wrap" is deprecated. Use layout_height="WRAP_CONTENT" and layout_constrainedHeight="true" instead.
|
||||
View class com.google.android.material.textfield.TextInputEditText is an AppCompat widget that can only be used with a Theme.AppCompat theme (or descendant).
|
||||
layout_constraintHeight_default="wrap" is deprecated. Use layout_height="WRAP_CONTENT" and layout_constrainedHeight="true" instead.
|
||||
-->
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/bottomSheetHandle"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
Reference in New Issue
Block a user