feat: compose-ify the message field

Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
Infi 2025-03-07 00:09:18 +01:00
parent 35c28bfde3
commit e117bf4b74
4 changed files with 269 additions and 293 deletions

View File

@ -60,7 +60,7 @@ import chat.revolt.api.routes.microservices.autumn.uploadToAutumn
import chat.revolt.api.schemas.ChannelType
import chat.revolt.api.settings.LoadedSettings
import chat.revolt.api.settings.SyncedSettings
import chat.revolt.components.chat.NativeMessageField
import chat.revolt.components.chat.MessageField
import chat.revolt.components.emoji.EmojiPicker
import chat.revolt.components.screens.chat.AttachmentManager
import chat.revolt.components.screens.chat.drawer.ChannelItem
@ -382,8 +382,8 @@ fun ShareTargetScreen(
)
}
NativeMessageField(
value = viewModel.messageContent,
MessageField(
initialValue = "",
onValueChange = { viewModel.messageContent = it },
canAttach = false,
forceSendButton = viewModel.attachments.isNotEmpty(),
@ -404,7 +404,7 @@ fun ShareTargetScreen(
context.getString(R.string.share_target_select_channel),
Toast.LENGTH_SHORT
).show()
return@NativeMessageField
return@MessageField
} else {
viewModel.send(selectedChannel!!) {
onFinished()

View File

@ -1,17 +1,6 @@
package chat.revolt.components.chat
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.RectShape
import android.net.Uri
import android.os.Build
import android.util.DisplayMetrics
import android.util.Log
import android.view.KeyEvent
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import android.view.inputmethod.InputMethodManager
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandIn
import androidx.compose.animation.fadeIn
@ -22,23 +11,36 @@ import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.content.ReceiveContentListener
import androidx.compose.foundation.content.consume
import androidx.compose.foundation.content.contentReceiver
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SuggestionChip
import androidx.compose.material3.SuggestionChipDefaults
@ -54,22 +56,31 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.isAltPressed
import androidx.compose.ui.input.key.isCtrlPressed
import androidx.compose.ui.input.key.isMetaPressed
import androidx.compose.ui.input.key.isShiftPressed
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.ViewCompat
import androidx.core.view.inputmethod.EditorInfoCompat
import androidx.core.view.inputmethod.InputConnectionCompat
import androidx.core.view.setPadding
import androidx.core.widget.addTextChangedListener
import chat.revolt.R
import chat.revolt.activities.RevoltTweenFloat
import chat.revolt.activities.RevoltTweenInt
@ -81,48 +92,23 @@ import chat.revolt.components.generic.UserAvatar
import chat.revolt.components.screens.chat.ChannelIcon
import chat.revolt.internals.Autocomplete
import kotlinx.coroutines.launch
import logcat.logcat
private fun convertDpToPixel(dp: Float, context: Context): Float {
return dp * (context.resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)
fun Pair<Int, Int>.asTextRange(): TextRange {
return TextRange(this.first, this.second)
}
fun String.applyAutocompleteSuggestion(
suggestion: AutocompleteSuggestion,
cursorPosition: Int
): String {
return when (suggestion) {
is AutocompleteSuggestion.User -> {
this.replaceRange(
cursorPosition - suggestion.query.length - 1,
cursorPosition,
"@${suggestion.user.username}#${suggestion.user.discriminator} "
)
}
private fun CharSequence.isEmptyOrOnlyNewlines(): Boolean {
return this.lines().all { it.isEmpty() || it.all { c -> c == '\n' } }
}
is AutocompleteSuggestion.Channel -> {
if (suggestion.channel.name?.contains(" ", ignoreCase = true) == true) {
this.replaceRange(
cursorPosition - suggestion.query.length - 1,
cursorPosition,
"<#${suggestion.channel.id}> "
)
} else {
this.replaceRange(
cursorPosition - suggestion.query.length - 1,
cursorPosition,
"#${suggestion.channel.name} "
)
}
}
private fun TextFieldState.lastWord(): String? {
return this.text.substring(0, this.selection.min)
.split(" ").lastOrNull()
}
is AutocompleteSuggestion.Emoji -> {
this.replaceRange(
cursorPosition - suggestion.query.length - 1,
cursorPosition,
suggestion.shortcode + " "
)
}
}
private fun CharSequence.lastWordStartsAt(): Int {
return this.lastIndexOf(" ")
}
sealed class AutocompleteSuggestion {
@ -147,8 +133,8 @@ sealed class AutocompleteSuggestion {
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun NativeMessageField(
value: String,
fun MessageField(
initialValue: String,
onValueChange: (String) -> Unit,
onAddAttachment: () -> Unit,
onCommitAttachment: (Uri) -> Unit,
@ -163,10 +149,11 @@ fun NativeMessageField(
failedValidation: Boolean = false,
serverId: String? = null,
channelId: String? = null,
valueIsBlank: Boolean = false,
editMode: Boolean = false,
initialValueDirtyMarker: Any = Unit,
cancelEdit: () -> Unit = {},
onFocusChange: (Boolean) -> Unit = {},
onSelectionChange: (Pair<Int, Int>) -> Unit = {}
) {
val placeholderResource = when (channelType) {
ChannelType.DirectMessage -> R.string.message_field_placeholder_dm
@ -176,31 +163,95 @@ fun NativeMessageField(
ChannelType.SavedMessages -> R.string.message_field_placeholder_notes
}
var requestFocus by remember { mutableStateOf({}) }
var clearFocus by remember { mutableStateOf({}) }
val sendButtonVisible =
(value.isNotBlank() || forceSendButton) && !disabled && !failedValidation
(!valueIsBlank || forceSendButton) && !disabled && !failedValidation
val density = LocalDensity.current
val scope = rememberCoroutineScope()
val selectionColour = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f).toArgb()
val cursorColour = MaterialTheme.colorScheme.primary.toArgb()
val failedValidationColour = MaterialTheme.colorScheme.error.toArgb()
val contentColour = LocalContentColor.current.toArgb()
val placeholderColour = LocalContentColor.current.copy(alpha = 0.5f).toArgb()
val focusManager = LocalFocusManager.current
val focusRequester = remember { FocusRequester() }
var selection by remember { mutableStateOf(0 to 0) }
val autocompleteSuggestions = remember { mutableStateListOf<AutocompleteSuggestion>() }
val autocompleteSuggestionState = rememberLazyListState()
val receiveContentListener = remember {
ReceiveContentListener { transferableContent ->
transferableContent.consume { item ->
val uri = item.uri
if (uri != null) {
onCommitAttachment(uri)
}
uri != null
}
}
}
var textFieldState = rememberTextFieldState(
initialText = initialValue,
initialSelection = selection.asTextRange()
)
LaunchedEffect(initialValue, initialValueDirtyMarker) {
logcat { "New initial value: $initialValue" }
logcat { "Old state: $textFieldState" }
textFieldState.setTextAndPlaceCursorAtEnd(initialValue)
logcat { "New state: $textFieldState" }
}
val scope = rememberCoroutineScope()
LaunchedEffect(textFieldState.text) {
logcat { "New text is ${textFieldState.text}" }
onValueChange(textFieldState.text.toString())
scope.launch {
autocompleteSuggestionState.animateScrollToItem(0)
}
autocompleteSuggestions.clear()
if (textFieldState.text.isNotBlank() &&
(textFieldState.selection.min == textFieldState.selection.max)
) {
val lastWord = textFieldState.lastWord()
if (lastWord != null) {
when {
lastWord.startsWith(':') && !lastWord.endsWith(':') -> {
autocompleteSuggestions.addAll(
Autocomplete.emoji(lastWord.substring(1))
)
}
lastWord.startsWith('@') -> {
if (channelId != null && serverId != null) {
autocompleteSuggestions.addAll(
Autocomplete.user(
channelId,
serverId,
lastWord.substring(1)
)
)
}
}
lastWord.startsWith('#') -> {
if (serverId != null) {
autocompleteSuggestions.addAll(
Autocomplete.channel(
serverId,
lastWord.substring(1)
)
)
}
}
}
}
}
}
LaunchedEffect(editMode) {
if (editMode) {
requestFocus()
focusRequester.requestFocus()
} else {
clearFocus()
focusManager.clearFocus()
}
}
@ -208,7 +259,7 @@ fun NativeMessageField(
modifier = modifier.background(MaterialTheme.colorScheme.surfaceContainer)
) {
AnimatedVisibility(
visible = autocompleteSuggestions.size > 0,
visible = autocompleteSuggestions.isNotEmpty(),
enter = expandIn(initialSize = { full ->
IntSize(
full.width,
@ -241,12 +292,17 @@ fun NativeMessageField(
is AutocompleteSuggestion.User -> {
SuggestionChip(
onClick = {
onValueChange(
value.applyAutocompleteSuggestion(
item,
selection.first
textFieldState.edit {
val lastWordStartsAt =
textFieldState.text
.substring(0, textFieldState.selection.max)
.lastWordStartsAt()
replace(
if (lastWordStartsAt == -1) 0 else (lastWordStartsAt + 1),
textFieldState.selection.max,
"@${item.user.username}#${item.user.discriminator} "
)
)
}
},
label = { Text("@${item.user.username}#${item.user.discriminator}") },
icon = {
@ -269,12 +325,29 @@ fun NativeMessageField(
is AutocompleteSuggestion.Channel -> {
SuggestionChip(
onClick = {
onValueChange(
value.applyAutocompleteSuggestion(
item,
selection.first
textFieldState.edit {
val lastWordStartsAt =
textFieldState.text
.substring(0, textFieldState.selection.max)
.lastWordStartsAt()
val replacement =
if (item.channel.name?.contains(
" ",
ignoreCase = true
) == true
) {
"<#${item.channel.id}> "
} else {
"#${item.channel.name} "
}
replace(
if (lastWordStartsAt == -1) 0 else (lastWordStartsAt + 1),
textFieldState.selection.max,
replacement
)
)
}
},
label = { Text("#${item.channel.name}") },
icon = {
@ -293,12 +366,17 @@ fun NativeMessageField(
is AutocompleteSuggestion.Emoji -> {
SuggestionChip(
onClick = {
onValueChange(
value.applyAutocompleteSuggestion(
item,
selection.first
textFieldState.edit {
val lastWordStartsAt =
textFieldState.text
.substring(0, textFieldState.selection.max)
.lastWordStartsAt()
replace(
if (lastWordStartsAt == -1) 0 else (lastWordStartsAt + 1),
textFieldState.selection.max,
item.shortcode + " "
)
)
}
},
label = {
if (item.custom != null) {
@ -353,7 +431,7 @@ fun NativeMessageField(
.clickable {
if (!editMode) {
// hide keyboard because it's annoying
clearFocus()
focusManager.clearFocus()
onAddAttachment()
}
}
@ -362,191 +440,68 @@ fun NativeMessageField(
)
}
AndroidView(
factory = { context ->
object : androidx.appcompat.widget.AppCompatEditText(context) {
var serverId: String? = null
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
var ic = super.onCreateInputConnection(outAttrs)
EditorInfoCompat.setContentMimeTypes(
outAttrs,
arrayOf("image/*")
)
val mimeTypes = ViewCompat.getOnReceiveContentMimeTypes(this)
if (mimeTypes != null) {
EditorInfoCompat.setContentMimeTypes(outAttrs, mimeTypes)
ic = ic?.let {
InputConnectionCompat.createWrapper(
this,
it,
outAttrs
)
}
}
return ic
}
override fun onSelectionChanged(selStart: Int, selEnd: Int) {
super.onSelectionChanged(selStart, selEnd)
onSelectionChange(selStart to selEnd)
selection = selStart to selEnd
scope.launch {
autocompleteSuggestionState.animateScrollToItem(0)
}
autocompleteSuggestions.clear()
if (text?.isNotBlank() == false) return
if (selStart != selEnd) return
val lastWord =
text?.substring(0, selStart)?.split(" ")?.lastOrNull() ?: return
when {
lastWord.startsWith(':') && !lastWord.endsWith(':') -> {
autocompleteSuggestions.addAll(
Autocomplete.emoji(lastWord.substring(1))
)
}
lastWord.startsWith('@') -> {
if (channelId == null) return
autocompleteSuggestions.addAll(
Autocomplete.user(
channelId,
this.serverId,
lastWord.substring(1)
)
)
}
lastWord.startsWith('#') -> {
if (this.serverId == null) return
autocompleteSuggestions.addAll(
Autocomplete.channel(
this.serverId!!,
lastWord.substring(1)
)
)
}
}
}
override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
return when (keyCode) {
KeyEvent.KEYCODE_ENTER -> {
if (event.isCtrlPressed && sendButtonVisible) {
onSendMessage()
true
} else super.onKeyUp(keyCode, event)
}
else -> super.onKeyUp(keyCode, event)
}
}
}.apply {
background = null
textSize = 16f
setPadding((density.density * 16.dp.value).toInt())
// Propagate text changes to parent
addTextChangedListener {
onValueChange(it.toString())
}
// Hide/show keyboard on focus change and propagate to parent
onFocusChangeListener =
android.view.View.OnFocusChangeListener { _, hasFocus ->
val keyboard =
context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
if (hasFocus) {
keyboard.showSoftInput(
this,
0
)
} else {
keyboard.hideSoftInputFromWindow(this.windowToken, 0)
}
onFocusChange(hasFocus)
}
ViewCompat.setOnReceiveContentListener(
this,
arrayOf("image/*")
) { _, payload ->
// Check mimetype
if (payload.clip.description.hasMimeType("image/*")) {
// Get image
val item = payload.clip.getItemAt(0)
val uri = item.uri
if (uri == null) {
Log.e("MessageField", "Received payload with null uri")
return@setOnReceiveContentListener payload
}
onCommitAttachment(uri)
return@setOnReceiveContentListener null
}
payload
}
isFocusable = true
isFocusableInTouchMode = true
typeface = ResourcesCompat.getFont(context, R.font.inter)
// Set colours
highlightColor = selectionColour
setTextColor(contentColour)
setHintTextColor(ColorStateList.valueOf(placeholderColour))
// Caret colour and size
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val shapeDrawable = ShapeDrawable(RectShape())
shapeDrawable.paint.color = cursorColour
val sizeInDp = 1
val sizeInPixels = (sizeInDp * resources.displayMetrics.density).toInt()
shapeDrawable.intrinsicWidth = sizeInPixels
shapeDrawable.intrinsicHeight = sizeInPixels
setTextCursorDrawable(shapeDrawable)
}
clearFocus = {
this.clearFocus()
}
requestFocus = {
this.requestFocus()
}
}
},
update = {
if (value != it.text.toString()) {
it.setText(value)
it.setSelection(value.length)
it.invalidate()
}
it.hint = it.context.getString(placeholderResource, channelName)
it.serverId = serverId
if (failedValidation) {
it.setTextColor(failedValidationColour)
} else {
it.setTextColor(contentColour)
}
if (it.text?.isEmpty() == true) {
it.maxLines = 1
} else {
it.maxLines = 5
}
},
BasicTextField(
state = textFieldState,
textStyle = LocalTextStyle.current.copy(
color = if (failedValidation) {
MaterialTheme.colorScheme.error
} else LocalContentColor.current
),
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
keyboardOptions = KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences,
keyboardType = KeyboardType.Text,
imeAction = ImeAction.None,
showKeyboardOnFocus = false
),
modifier = Modifier
.weight(1f)
.testTag("message_field")
.heightIn(max = 128.dp)
.verticalScroll(rememberScrollState())
.onFocusChanged {
onFocusChange(it.isFocused)
}
.focusRequester(focusRequester)
.contentReceiver(receiveContentListener)
.onKeyEvent {
if (it.type == KeyEventType.KeyUp) {
when {
it.key == Key.Enter &&
!it.isShiftPressed &&
!it.isAltPressed &&
it.isCtrlPressed &&
!it.isMetaPressed -> {
onSendMessage()
return@onKeyEvent true
}
it.key == Key.Escape &&
!it.isShiftPressed &&
!it.isAltPressed &&
!it.isCtrlPressed &&
!it.isMetaPressed -> {
cancelEdit()
return@onKeyEvent true
}
}
}
return@onKeyEvent false
},
decorator = { innerTextField ->
Box(Modifier.padding(horizontal = 16.dp, vertical = 18.dp)) {
if (textFieldState.text.isEmptyOrOnlyNewlines()) {
Text(
stringResource(placeholderResource, channelName),
style = LocalTextStyle.current.copy(
color = LocalContentColor.current.copy(alpha = 0.5f)
),
modifier = Modifier.align(Alignment.CenterStart)
)
}
innerTextField()
}
}
)
Icon(
@ -557,7 +512,7 @@ fun NativeMessageField(
.clip(CircleShape)
.size(32.dp)
.clickable {
clearFocus()
focusManager.clearFocus()
onPickEmoji()
}
.padding(4.dp)
@ -610,8 +565,8 @@ fun NativeMessageField(
@Preview
@Composable
fun NativeMessageFieldPreview() {
NativeMessageField(
value = "Hello world!",
MessageField(
initialValue = "Hello world!",
onValueChange = {},
onAddAttachment = {},
onCommitAttachment = {},
@ -626,6 +581,5 @@ fun NativeMessageFieldPreview() {
editMode = false,
cancelEdit = {},
onFocusChange = {},
onSelectionChange = {}
)
}

View File

@ -122,7 +122,7 @@ import chat.revolt.callbacks.Action
import chat.revolt.callbacks.ActionChannel
import chat.revolt.components.chat.DateDivider
import chat.revolt.components.chat.Message
import chat.revolt.components.chat.NativeMessageField
import chat.revolt.components.chat.MessageField
import chat.revolt.components.chat.SystemMessage
import chat.revolt.components.emoji.EmojiPicker
import chat.revolt.components.generic.GroupIcon
@ -969,7 +969,7 @@ fun ChannelScreen(
AssistChip(
onClick = {
viewModel.editingMessage = null
viewModel.putDraftContent("")
viewModel.putDraftContent("", true)
},
label = {
Text(stringResource(R.string.message_field_editing_message))
@ -992,8 +992,9 @@ fun ChannelScreen(
}
}
NativeMessageField(
value = viewModel.draftContent,
MessageField(
initialValue = viewModel.initialTextFieldValue,
initialValueDirtyMarker = viewModel.initialTextFieldValueDirtyMarker,
onValueChange = viewModel::putDraftContent,
onAddAttachment = {
if (viewModel.activePane == ChannelScreenActivePane.AttachmentPicker) {
@ -1032,6 +1033,11 @@ fun ChannelScreen(
serverId = viewModel.channel?.server,
channelId = channelId,
failedValidation = viewModel.draftContent.length > 2000,
valueIsBlank = viewModel.draftContent.isBlank(),
cancelEdit = {
viewModel.editingMessage = null
viewModel.putDraftContent("", true)
}
)
DropdownMenu(

View File

@ -78,6 +78,14 @@ class ChannelScreenViewModel @Inject constructor(
var activePane by mutableStateOf<ChannelScreenActivePane>(ChannelScreenActivePane.None)
var keyboardHeight by mutableIntStateOf(0)
// Setting [initialTextFieldValue] causes the text field to switch value.
// However [initialTextFieldValue] gets de-synced with the actual text field value when the user types.
// For a field that keeps track of the actual text field value, see [draftContent].
// Setting [draftContent] does not cause the text field to switch value.
// Note that if [initialTextFieldValue] is set to the same value it has now, the text field will not switch value.
// Set [initialTextFieldValueDirtyMarker] to a new value to force the text field to switch value.
var initialTextFieldValue by mutableStateOf("")
var initialTextFieldValueDirtyMarker by mutableStateOf("")
var draftContent by mutableStateOf("")
var draftAttachments = mutableStateListOf<FileArgs>()
var draftReplyTo = mutableStateListOf<SendMessageReply>()
@ -198,7 +206,7 @@ class ChannelScreenViewModel @Inject constructor(
}
fun putAtCursorPosition(text: String) {
putDraftContent(draftContent + text)
putDraftContent(draftContent + text, true)
}
private var lastSentBeginTyping: Instant? = null
@ -241,7 +249,11 @@ class ChannelScreenViewModel @Inject constructor(
}
}
fun putDraftContent(content: String) {
/**
* Puts the draft content in the KV storage, the in-memory state of the message content,
* and, if [setInitial] is true, updates the text field to say the new [content].
*/
fun putDraftContent(content: String, setInitial: Boolean = false) {
viewModelScope.launch {
kvStorage.set("draftContent/${channel?.id}", content)
}
@ -257,6 +269,10 @@ class ChannelScreenViewModel @Inject constructor(
}
draftContent = content
if (setInitial) {
initialTextFieldValue = content
initialTextFieldValueDirtyMarker = ULID.makeNext()
}
}
suspend fun addReplyTo(messageId: String) {
@ -295,7 +311,7 @@ class ChannelScreenViewModel @Inject constructor(
messageId = editingMessage ?: return,
newContent = draftContent,
)
putDraftContent("")
putDraftContent("", true)
} catch (e: Exception) {
Log.e("ChannelScreenViewModel", "Failed to edit message", e)
}
@ -368,7 +384,7 @@ class ChannelScreenViewModel @Inject constructor(
updateItems(listOf(ChannelScreenItem.ProspectiveMessage(prospectiveMessage)) + items)
kvStorage.remove("draftContent/${channel?.id}")
putDraftContent("")
putDraftContent("", true)
draftReplyTo.clear()
attachmentUploadProgress = 0f
@ -723,7 +739,7 @@ class ChannelScreenViewModel @Inject constructor(
m is ChannelScreenItem.RegularMessage && m.message.id == it.messageId
} as? ChannelScreenItem.RegularMessage ?: return@onEach
putDraftContent(message.message.content ?: "")
putDraftContent(message.message.content ?: "", true)
this@ChannelScreenViewModel.draftAttachments.clear()
draftReplyTo.clear()
}
@ -740,7 +756,7 @@ class ChannelScreenViewModel @Inject constructor(
shouldMention
)
)
putDraftContent(it.content)
putDraftContent(it.content, true)
}
}
}.catch {