forked from melod1n/fast-messenger
refactor: split messages history actions
This commit is contained in:
+16
-394
@@ -1,401 +1,23 @@
|
|||||||
package dev.meloda.fast.messageshistory
|
package dev.meloda.fast.messageshistory
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
import androidx.compose.ui.text.SpanStyle
|
|
||||||
import androidx.compose.ui.text.TextRange
|
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
|
||||||
import dev.meloda.fast.common.extensions.listenValue
|
|
||||||
import dev.meloda.fast.common.extensions.launchDbRefresh
|
|
||||||
import dev.meloda.fast.common.extensions.removeIfCompat
|
|
||||||
import dev.meloda.fast.common.extensions.setValue
|
|
||||||
import dev.meloda.fast.common.provider.ResourceProvider
|
|
||||||
import dev.meloda.fast.data.UserConfig
|
|
||||||
import dev.meloda.fast.data.VkMemoryCache
|
|
||||||
import dev.meloda.fast.datastore.AppSettings
|
|
||||||
import dev.meloda.fast.domain.MessagesUseCase
|
|
||||||
import dev.meloda.fast.domain.util.extractReplySummary
|
|
||||||
import dev.meloda.fast.domain.util.extractReplyTitle
|
|
||||||
import dev.meloda.fast.domain.util.extractTitle
|
|
||||||
import dev.meloda.fast.messageshistory.model.ActionMode
|
|
||||||
import dev.meloda.fast.messageshistory.model.MessageDialog
|
|
||||||
import dev.meloda.fast.messageshistory.model.MessagesHistoryScreenState
|
|
||||||
import dev.meloda.fast.model.api.domain.FormatDataType
|
|
||||||
import dev.meloda.fast.model.api.domain.VkMessage
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.serialization.json.add
|
|
||||||
import kotlinx.serialization.json.buildJsonArray
|
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
|
||||||
import kotlinx.serialization.json.put
|
|
||||||
import kotlin.random.Random
|
|
||||||
|
|
||||||
internal class MessagesHistoryMessageActions(
|
internal class MessagesHistoryMessageActions(
|
||||||
private val viewModelScope: CoroutineScope,
|
private val sendEditActions: MessagesHistoryMessageSendEditActions
|
||||||
private val messagesUseCase: MessagesUseCase,
|
|
||||||
private val resourceProvider: ResourceProvider,
|
|
||||||
private val screenState: MutableStateFlow<MessagesHistoryScreenState>,
|
|
||||||
private val messages: MutableStateFlow<List<VkMessage>>,
|
|
||||||
private val showKeyboard: MutableStateFlow<Boolean>,
|
|
||||||
private val dialog: MutableStateFlow<MessageDialog?>,
|
|
||||||
private val syncUiMessages: () -> Unit,
|
|
||||||
private val onPinnedMessageChanged: (VkMessage?) -> Unit
|
|
||||||
) {
|
) {
|
||||||
private var lastMessageText: String? = null
|
fun replyToMessage(cmId: Long) = sendEditActions.replyToMessage(cmId)
|
||||||
private val sendingMessages: MutableList<VkMessage> = mutableListOf()
|
fun onMessageInputChanged(newText: TextFieldValue) = sendEditActions.onMessageInputChanged(newText)
|
||||||
private val failedMessages: MutableList<VkMessage> = mutableListOf()
|
fun onEmojiButtonLongClicked() = sendEditActions.onEmojiButtonLongClicked()
|
||||||
private var replyToCmId: Long? = null
|
fun editMessage(cmId: Long) = sendEditActions.editMessage(cmId)
|
||||||
private var editMessage: VkMessage? = null
|
fun stopEditMessage() = sendEditActions.stopEditMessage()
|
||||||
private var formatData = VkMessage.FormatData("1", emptyList())
|
fun onBoldClicked() = sendEditActions.onBoldClicked()
|
||||||
|
fun onItalicClicked() = sendEditActions.onItalicClicked()
|
||||||
fun replyToMessage(cmId: Long) {
|
fun onUnderlineClicked() = sendEditActions.onUnderlineClicked()
|
||||||
val messageToReply = messages.value.find { it.cmId == cmId } ?: return
|
fun onRegularClicked() = sendEditActions.onRegularClicked()
|
||||||
|
fun onActionButtonClicked() = sendEditActions.onActionButtonClicked()
|
||||||
showKeyboard.setValue { true }
|
fun onReplyCloseClicked() = sendEditActions.onReplyCloseClicked()
|
||||||
replyToCmId = cmId
|
fun onKeyboardShown() = sendEditActions.onKeyboardShown()
|
||||||
screenState.setValue { old ->
|
fun sendMessage() = sendEditActions.sendMessage()
|
||||||
old.copy(
|
fun confirmDeleteCurrentEditMessage() = sendEditActions.confirmDeleteCurrentEditMessage()
|
||||||
replyTitle = messageToReply.extractTitle(),
|
fun editCurrentEditMessage() = sendEditActions.editCurrentEditMessage()
|
||||||
replyText = messageToReply.extractReplySummary(resourceProvider.resources)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onMessageInputChanged(newText: TextFieldValue) {
|
|
||||||
screenState.setValue { old ->
|
|
||||||
old.copy(
|
|
||||||
message = newText,
|
|
||||||
actionMode =
|
|
||||||
when {
|
|
||||||
screenState.value.editCmId != null -> {
|
|
||||||
// TODO: 13/03/2026, Danil Nikolaev: also check if attachments is empty
|
|
||||||
if (newText.text.trim().isEmpty()) {
|
|
||||||
ActionMode.DELETE
|
|
||||||
} else {
|
|
||||||
ActionMode.EDIT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newText.text.trim().isEmpty() -> ActionMode.RECORD_AUDIO
|
|
||||||
else -> ActionMode.SEND
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
updateStyles()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onEmojiButtonLongClicked() {
|
|
||||||
AppSettings.Features.fastText.takeIf { it.isNotBlank() }?.let { text ->
|
|
||||||
val newText = "${screenState.value.message.text}$text"
|
|
||||||
onMessageInputChanged(
|
|
||||||
TextFieldValue(text = newText, selection = TextRange(newText.length))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun editMessage(cmId: Long) {
|
|
||||||
screenState.setValue { old -> old.copy(editCmId = cmId) }
|
|
||||||
|
|
||||||
val messageToEdit = messages.value.firstOrNull { it.cmId == cmId } ?: return
|
|
||||||
editMessage = messageToEdit
|
|
||||||
|
|
||||||
lastMessageText = screenState.value.message.text
|
|
||||||
|
|
||||||
var newState = screenState.value.copy(
|
|
||||||
message = TextFieldValue(
|
|
||||||
text = messageToEdit.text.orEmpty(),
|
|
||||||
selection = TextRange(messageToEdit.text.orEmpty().length)
|
|
||||||
),
|
|
||||||
actionMode = ActionMode.EDIT
|
|
||||||
)
|
|
||||||
|
|
||||||
messageToEdit.replyMessage?.let { reply ->
|
|
||||||
replyToCmId = reply.cmId
|
|
||||||
newState = newState.copy(
|
|
||||||
replyTitle = reply.extractReplyTitle(),
|
|
||||||
replyText = reply.extractReplySummary(resourceProvider.resources)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
showKeyboard.setValue { true }
|
|
||||||
screenState.setValue { newState }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stopEditMessage() {
|
|
||||||
val lastText = lastMessageText.orEmpty().trim()
|
|
||||||
|
|
||||||
screenState.setValue { old ->
|
|
||||||
old.copy(
|
|
||||||
editCmId = null,
|
|
||||||
message = TextFieldValue(
|
|
||||||
text = lastText,
|
|
||||||
selection = TextRange(lastText.length)
|
|
||||||
),
|
|
||||||
actionMode = if (lastText.isBlank()) ActionMode.RECORD_AUDIO else ActionMode.SEND,
|
|
||||||
replyTitle = null,
|
|
||||||
replyText = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onBoldClicked() = updateFormatting(FormatDataType.BOLD)
|
|
||||||
fun onItalicClicked() = updateFormatting(FormatDataType.ITALIC)
|
|
||||||
fun onUnderlineClicked() = updateFormatting(FormatDataType.UNDERLINE)
|
|
||||||
|
|
||||||
fun onRegularClicked() {
|
|
||||||
formatData = formatData.copy(items = emptyList())
|
|
||||||
updateStyles()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onActionButtonClicked() {
|
|
||||||
when (screenState.value.actionMode) {
|
|
||||||
ActionMode.DELETE -> confirmDeleteCurrentEditMessage()
|
|
||||||
ActionMode.EDIT -> editCurrentEditMessage()
|
|
||||||
ActionMode.RECORD_AUDIO -> {
|
|
||||||
screenState.setValue { it.copy(actionMode = ActionMode.RECORD_VIDEO) }
|
|
||||||
}
|
|
||||||
ActionMode.RECORD_VIDEO -> {
|
|
||||||
screenState.setValue { it.copy(actionMode = ActionMode.RECORD_AUDIO) }
|
|
||||||
}
|
|
||||||
ActionMode.SEND -> sendMessage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onReplyCloseClicked() {
|
|
||||||
replyToCmId = null
|
|
||||||
screenState.setValue { old ->
|
|
||||||
old.copy(
|
|
||||||
replyTitle = null,
|
|
||||||
replyText = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onKeyboardShown() {
|
|
||||||
showKeyboard.setValue { false }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendMessage() {
|
|
||||||
lastMessageText = screenState.value.message.text
|
|
||||||
|
|
||||||
val newMessage = VkMessage(
|
|
||||||
id = -1L - sendingMessages.size,
|
|
||||||
cmId = -1L - sendingMessages.size,
|
|
||||||
text = lastMessageText,
|
|
||||||
isOut = true,
|
|
||||||
peerId = screenState.value.convoId,
|
|
||||||
fromId = UserConfig.userId,
|
|
||||||
date = (System.currentTimeMillis() / 1000).toInt(),
|
|
||||||
randomId = Random.nextInt().toLong(),
|
|
||||||
action = null,
|
|
||||||
actionMemberId = null,
|
|
||||||
actionText = null,
|
|
||||||
actionCmId = null,
|
|
||||||
actionMessage = null,
|
|
||||||
updateTime = null,
|
|
||||||
isImportant = false,
|
|
||||||
forwards = null,
|
|
||||||
attachments = null,
|
|
||||||
replyMessage = when {
|
|
||||||
replyToCmId != null -> messages.value.find { it.cmId == replyToCmId }
|
|
||||||
else -> null
|
|
||||||
},
|
|
||||||
geoType = null,
|
|
||||||
user = VkMemoryCache.getUser(UserConfig.userId),
|
|
||||||
group = null,
|
|
||||||
actionUser = null,
|
|
||||||
actionGroup = null,
|
|
||||||
isPinned = false,
|
|
||||||
isSpam = false,
|
|
||||||
pinnedAt = null,
|
|
||||||
formatData = formatData,
|
|
||||||
)
|
|
||||||
formatData = formatData.copy(items = emptyList())
|
|
||||||
sendingMessages += newMessage
|
|
||||||
messages.setValue { old -> listOf(newMessage).plus(old) }
|
|
||||||
syncUiMessages()
|
|
||||||
|
|
||||||
screenState.setValue { old ->
|
|
||||||
old.copy(
|
|
||||||
message = TextFieldValue(),
|
|
||||||
actionMode = ActionMode.RECORD_AUDIO,
|
|
||||||
replyTitle = null,
|
|
||||||
replyText = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val replyCmId = replyToCmId
|
|
||||||
replyToCmId = null
|
|
||||||
|
|
||||||
val forward = when {
|
|
||||||
replyCmId != null -> {
|
|
||||||
buildJsonObject {
|
|
||||||
put("peer_id", screenState.value.convoId)
|
|
||||||
put("conversation_message_ids", buildJsonArray { add(replyCmId) })
|
|
||||||
put("is_reply", true)
|
|
||||||
}.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
messagesUseCase.sendMessage(
|
|
||||||
peerId = screenState.value.convoId,
|
|
||||||
randomId = newMessage.randomId,
|
|
||||||
message = newMessage.text,
|
|
||||||
forward = forward,
|
|
||||||
attachments = null,
|
|
||||||
formatData = newMessage.formatData,
|
|
||||||
).listenValue(viewModelScope) { state ->
|
|
||||||
state.processState(
|
|
||||||
any = { sendingMessages.remove(newMessage) },
|
|
||||||
error = { error ->
|
|
||||||
Log.d("MessagesHistoryViewModelImpl", "sendMessage: ERROR: $error")
|
|
||||||
|
|
||||||
val failedId = -500_000L - failedMessages.size
|
|
||||||
val newFailedMessage = newMessage.copy(id = failedId)
|
|
||||||
failedMessages += newFailedMessage
|
|
||||||
|
|
||||||
val newMessages = messages.value.toMutableList()
|
|
||||||
newMessages[newMessages.indexOf(newMessage)] = newFailedMessage
|
|
||||||
messages.setValue { newMessages }
|
|
||||||
syncUiMessages()
|
|
||||||
},
|
|
||||||
success = { response ->
|
|
||||||
viewModelScope.launch {
|
|
||||||
messagesUseCase.storeMessage(
|
|
||||||
newMessage.copy(
|
|
||||||
id = response.messageId,
|
|
||||||
cmId = response.cmId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
refreshMessagesFromDb()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun confirmDeleteCurrentEditMessage() {
|
|
||||||
val currentMessage = editMessage ?: return
|
|
||||||
dialog.setValue { MessageDialog.MessageDelete(currentMessage) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun editCurrentEditMessage() {
|
|
||||||
val newText = screenState.value.message.text
|
|
||||||
val lastText = lastMessageText.orEmpty().trim()
|
|
||||||
val currentReplyToCmId = replyToCmId
|
|
||||||
|
|
||||||
screenState.setValue { old ->
|
|
||||||
old.copy(
|
|
||||||
editCmId = null,
|
|
||||||
message = TextFieldValue(
|
|
||||||
text = lastText,
|
|
||||||
selection = TextRange(lastText.length)
|
|
||||||
),
|
|
||||||
actionMode = if (lastText.isBlank()) ActionMode.RECORD_AUDIO else ActionMode.SEND,
|
|
||||||
replyTitle = null,
|
|
||||||
replyText = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
syncUiMessages()
|
|
||||||
|
|
||||||
val newMessage = editMessage?.copy(
|
|
||||||
replyMessage = if (currentReplyToCmId == null) null else editMessage?.replyMessage,
|
|
||||||
text = newText
|
|
||||||
) ?: return
|
|
||||||
|
|
||||||
Log.d("MessagesHistoryViewModelImpl", "editMessage: $newMessage")
|
|
||||||
|
|
||||||
messagesUseCase.edit(
|
|
||||||
peerId = screenState.value.convoId,
|
|
||||||
cmId = newMessage.cmId,
|
|
||||||
message = newMessage.text,
|
|
||||||
attachments = null,
|
|
||||||
formatData = newMessage.formatData
|
|
||||||
).listenValue(viewModelScope) { state ->
|
|
||||||
state.processState(
|
|
||||||
error = { error ->
|
|
||||||
Log.d("MessagesHistoryViewModelImpl", "editMessage: ERROR: $error")
|
|
||||||
},
|
|
||||||
success = {
|
|
||||||
viewModelScope.launch {
|
|
||||||
messagesUseCase.storeMessage(newMessage)
|
|
||||||
refreshMessagesFromDb()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
replyToCmId = null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateFormatting(type: FormatDataType) {
|
|
||||||
val selectionRange = screenState.value.message.selection
|
|
||||||
val newItems = formatData.items.toMutableList()
|
|
||||||
val wasRemoved = newItems.removeIfCompat {
|
|
||||||
it.type == type &&
|
|
||||||
it.offset == selectionRange.start &&
|
|
||||||
it.offset + it.length == selectionRange.end
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!wasRemoved) {
|
|
||||||
newItems += VkMessage.FormatData.Item(
|
|
||||||
offset = selectionRange.start,
|
|
||||||
length = selectionRange.end - selectionRange.start,
|
|
||||||
type = type,
|
|
||||||
url = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
formatData = formatData.copy(items = newItems)
|
|
||||||
updateStyles()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateStyles() {
|
|
||||||
val annotations =
|
|
||||||
mutableListOf<AnnotatedString.Range<out AnnotatedString.Annotation>>()
|
|
||||||
|
|
||||||
formatData.items.forEach { item ->
|
|
||||||
val spanStyle = when (item.type) {
|
|
||||||
FormatDataType.BOLD -> SpanStyle(fontWeight = FontWeight.SemiBold)
|
|
||||||
FormatDataType.ITALIC -> SpanStyle(fontStyle = FontStyle.Italic)
|
|
||||||
FormatDataType.UNDERLINE -> SpanStyle(textDecoration = TextDecoration.Underline)
|
|
||||||
FormatDataType.URL -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
spanStyle?.let {
|
|
||||||
annotations += AnnotatedString.Range(
|
|
||||||
item = spanStyle,
|
|
||||||
start = item.offset,
|
|
||||||
end = item.offset + item.length
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val newText = AnnotatedString(
|
|
||||||
text = screenState.value.message.text,
|
|
||||||
annotations = annotations
|
|
||||||
)
|
|
||||||
|
|
||||||
screenState.setValue { old ->
|
|
||||||
old.copy(message = old.message.copy(annotatedString = newText))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun refreshMessagesFromDb() {
|
|
||||||
viewModelScope.launchDbRefresh(
|
|
||||||
load = {
|
|
||||||
val localMessages = messagesUseCase.getLocalMessages(screenState.value.convoId)
|
|
||||||
messages.setValue { localMessages }
|
|
||||||
},
|
|
||||||
after = ::syncUiMessages
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+400
@@ -0,0 +1,400 @@
|
|||||||
|
package dev.meloda.fast.messageshistory
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.TextRange
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import dev.meloda.fast.common.extensions.listenValue
|
||||||
|
import dev.meloda.fast.common.extensions.launchDbRefresh
|
||||||
|
import dev.meloda.fast.common.extensions.removeIfCompat
|
||||||
|
import dev.meloda.fast.common.extensions.setValue
|
||||||
|
import dev.meloda.fast.common.provider.ResourceProvider
|
||||||
|
import dev.meloda.fast.data.UserConfig
|
||||||
|
import dev.meloda.fast.data.VkMemoryCache
|
||||||
|
import dev.meloda.fast.datastore.AppSettings
|
||||||
|
import dev.meloda.fast.data.processState
|
||||||
|
import dev.meloda.fast.domain.MessagesUseCase
|
||||||
|
import dev.meloda.fast.domain.util.extractReplySummary
|
||||||
|
import dev.meloda.fast.domain.util.extractReplyTitle
|
||||||
|
import dev.meloda.fast.domain.util.extractTitle
|
||||||
|
import dev.meloda.fast.messageshistory.model.ActionMode
|
||||||
|
import dev.meloda.fast.messageshistory.model.MessageDialog
|
||||||
|
import dev.meloda.fast.messageshistory.model.MessagesHistoryScreenState
|
||||||
|
import dev.meloda.fast.model.api.domain.FormatDataType
|
||||||
|
import dev.meloda.fast.model.api.domain.VkMessage
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.serialization.json.add
|
||||||
|
import kotlinx.serialization.json.buildJsonArray
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
internal class MessagesHistoryMessageSendEditActions(
|
||||||
|
private val viewModelScope: CoroutineScope,
|
||||||
|
private val messagesUseCase: MessagesUseCase,
|
||||||
|
private val resourceProvider: ResourceProvider,
|
||||||
|
private val screenState: MutableStateFlow<MessagesHistoryScreenState>,
|
||||||
|
private val messages: MutableStateFlow<List<VkMessage>>,
|
||||||
|
private val showKeyboard: MutableStateFlow<Boolean>,
|
||||||
|
private val dialog: MutableStateFlow<MessageDialog?>,
|
||||||
|
private val syncUiMessages: () -> Unit
|
||||||
|
) {
|
||||||
|
private var lastMessageText: String? = null
|
||||||
|
private val sendingMessages: MutableList<VkMessage> = mutableListOf()
|
||||||
|
private val failedMessages: MutableList<VkMessage> = mutableListOf()
|
||||||
|
private var replyToCmId: Long? = null
|
||||||
|
private var editMessage: VkMessage? = null
|
||||||
|
private var formatData = VkMessage.FormatData("1", emptyList())
|
||||||
|
|
||||||
|
fun replyToMessage(cmId: Long) {
|
||||||
|
val messageToReply = messages.value.find { it.cmId == cmId } ?: return
|
||||||
|
|
||||||
|
showKeyboard.setValue { true }
|
||||||
|
replyToCmId = cmId
|
||||||
|
screenState.setValue { old ->
|
||||||
|
old.copy(
|
||||||
|
replyTitle = messageToReply.extractTitle(),
|
||||||
|
replyText = messageToReply.extractReplySummary(resourceProvider.resources)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onMessageInputChanged(newText: TextFieldValue) {
|
||||||
|
screenState.setValue { old ->
|
||||||
|
old.copy(
|
||||||
|
message = newText,
|
||||||
|
actionMode =
|
||||||
|
when {
|
||||||
|
screenState.value.editCmId != null -> {
|
||||||
|
// TODO: 13/03/2026, Danil Nikolaev: also check if attachments is empty
|
||||||
|
if (newText.text.trim().isEmpty()) {
|
||||||
|
ActionMode.DELETE
|
||||||
|
} else {
|
||||||
|
ActionMode.EDIT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newText.text.trim().isEmpty() -> ActionMode.RECORD_AUDIO
|
||||||
|
else -> ActionMode.SEND
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
updateStyles()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEmojiButtonLongClicked() {
|
||||||
|
AppSettings.Features.fastText.takeIf { it.isNotBlank() }?.let { text ->
|
||||||
|
val newText = "${screenState.value.message.text}$text"
|
||||||
|
onMessageInputChanged(
|
||||||
|
TextFieldValue(text = newText, selection = TextRange(newText.length))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun editMessage(cmId: Long) {
|
||||||
|
screenState.setValue { old -> old.copy(editCmId = cmId) }
|
||||||
|
|
||||||
|
val messageToEdit = messages.value.firstOrNull { it.cmId == cmId } ?: return
|
||||||
|
editMessage = messageToEdit
|
||||||
|
|
||||||
|
lastMessageText = screenState.value.message.text
|
||||||
|
|
||||||
|
var newState = screenState.value.copy(
|
||||||
|
message = TextFieldValue(
|
||||||
|
text = messageToEdit.text.orEmpty(),
|
||||||
|
selection = TextRange(messageToEdit.text.orEmpty().length)
|
||||||
|
),
|
||||||
|
actionMode = ActionMode.EDIT
|
||||||
|
)
|
||||||
|
|
||||||
|
messageToEdit.replyMessage?.let { reply ->
|
||||||
|
replyToCmId = reply.cmId
|
||||||
|
newState = newState.copy(
|
||||||
|
replyTitle = reply.extractReplyTitle(),
|
||||||
|
replyText = reply.extractReplySummary(resourceProvider.resources)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
showKeyboard.setValue { true }
|
||||||
|
screenState.setValue { newState }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopEditMessage() {
|
||||||
|
val lastText = lastMessageText.orEmpty().trim()
|
||||||
|
|
||||||
|
screenState.setValue { old ->
|
||||||
|
old.copy(
|
||||||
|
editCmId = null,
|
||||||
|
message = TextFieldValue(
|
||||||
|
text = lastText,
|
||||||
|
selection = TextRange(lastText.length)
|
||||||
|
),
|
||||||
|
actionMode = if (lastText.isBlank()) ActionMode.RECORD_AUDIO else ActionMode.SEND,
|
||||||
|
replyTitle = null,
|
||||||
|
replyText = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onBoldClicked() = updateFormatting(FormatDataType.BOLD)
|
||||||
|
fun onItalicClicked() = updateFormatting(FormatDataType.ITALIC)
|
||||||
|
fun onUnderlineClicked() = updateFormatting(FormatDataType.UNDERLINE)
|
||||||
|
|
||||||
|
fun onRegularClicked() {
|
||||||
|
formatData = formatData.copy(items = emptyList())
|
||||||
|
updateStyles()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onActionButtonClicked() {
|
||||||
|
when (screenState.value.actionMode) {
|
||||||
|
ActionMode.DELETE -> confirmDeleteCurrentEditMessage()
|
||||||
|
ActionMode.EDIT -> editCurrentEditMessage()
|
||||||
|
ActionMode.RECORD_AUDIO -> {
|
||||||
|
screenState.setValue { it.copy(actionMode = ActionMode.RECORD_VIDEO) }
|
||||||
|
}
|
||||||
|
ActionMode.RECORD_VIDEO -> {
|
||||||
|
screenState.setValue { it.copy(actionMode = ActionMode.RECORD_AUDIO) }
|
||||||
|
}
|
||||||
|
ActionMode.SEND -> sendMessage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onReplyCloseClicked() {
|
||||||
|
replyToCmId = null
|
||||||
|
screenState.setValue { old ->
|
||||||
|
old.copy(
|
||||||
|
replyTitle = null,
|
||||||
|
replyText = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onKeyboardShown() {
|
||||||
|
showKeyboard.setValue { false }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendMessage() {
|
||||||
|
lastMessageText = screenState.value.message.text
|
||||||
|
|
||||||
|
val newMessage = VkMessage(
|
||||||
|
id = -1L - sendingMessages.size,
|
||||||
|
cmId = -1L - sendingMessages.size,
|
||||||
|
text = lastMessageText,
|
||||||
|
isOut = true,
|
||||||
|
peerId = screenState.value.convoId,
|
||||||
|
fromId = UserConfig.userId,
|
||||||
|
date = (System.currentTimeMillis() / 1000).toInt(),
|
||||||
|
randomId = Random.nextInt().toLong(),
|
||||||
|
action = null,
|
||||||
|
actionMemberId = null,
|
||||||
|
actionText = null,
|
||||||
|
actionCmId = null,
|
||||||
|
actionMessage = null,
|
||||||
|
updateTime = null,
|
||||||
|
isImportant = false,
|
||||||
|
forwards = null,
|
||||||
|
attachments = null,
|
||||||
|
replyMessage = when {
|
||||||
|
replyToCmId != null -> messages.value.find { it.cmId == replyToCmId }
|
||||||
|
else -> null
|
||||||
|
},
|
||||||
|
geoType = null,
|
||||||
|
user = VkMemoryCache.getUser(UserConfig.userId),
|
||||||
|
group = null,
|
||||||
|
actionUser = null,
|
||||||
|
actionGroup = null,
|
||||||
|
isPinned = false,
|
||||||
|
isSpam = false,
|
||||||
|
pinnedAt = null,
|
||||||
|
formatData = formatData,
|
||||||
|
)
|
||||||
|
formatData = formatData.copy(items = emptyList())
|
||||||
|
sendingMessages += newMessage
|
||||||
|
messages.setValue { old -> listOf(newMessage).plus(old) }
|
||||||
|
syncUiMessages()
|
||||||
|
|
||||||
|
screenState.setValue { old ->
|
||||||
|
old.copy(
|
||||||
|
message = TextFieldValue(),
|
||||||
|
actionMode = ActionMode.RECORD_AUDIO,
|
||||||
|
replyTitle = null,
|
||||||
|
replyText = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val replyCmId = replyToCmId
|
||||||
|
replyToCmId = null
|
||||||
|
|
||||||
|
val forward = when {
|
||||||
|
replyCmId != null -> {
|
||||||
|
buildJsonObject {
|
||||||
|
put("peer_id", screenState.value.convoId)
|
||||||
|
put("conversation_message_ids", buildJsonArray { add(replyCmId) })
|
||||||
|
put("is_reply", true)
|
||||||
|
}.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
messagesUseCase.sendMessage(
|
||||||
|
peerId = screenState.value.convoId,
|
||||||
|
randomId = newMessage.randomId,
|
||||||
|
message = newMessage.text,
|
||||||
|
forward = forward,
|
||||||
|
attachments = null,
|
||||||
|
formatData = newMessage.formatData,
|
||||||
|
).listenValue(viewModelScope) { state ->
|
||||||
|
state.processState(
|
||||||
|
any = { sendingMessages.remove(newMessage) },
|
||||||
|
error = { error ->
|
||||||
|
Log.d("MessagesHistoryViewModelImpl", "sendMessage: ERROR: $error")
|
||||||
|
|
||||||
|
val failedId = -500_000L - failedMessages.size
|
||||||
|
val newFailedMessage = newMessage.copy(id = failedId)
|
||||||
|
failedMessages += newFailedMessage
|
||||||
|
|
||||||
|
val newMessages = messages.value.toMutableList()
|
||||||
|
newMessages[newMessages.indexOf(newMessage)] = newFailedMessage
|
||||||
|
messages.setValue { newMessages }
|
||||||
|
syncUiMessages()
|
||||||
|
},
|
||||||
|
success = { response ->
|
||||||
|
viewModelScope.launch {
|
||||||
|
messagesUseCase.storeMessage(
|
||||||
|
newMessage.copy(
|
||||||
|
id = response.messageId,
|
||||||
|
cmId = response.cmId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
refreshMessagesFromDb()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun confirmDeleteCurrentEditMessage() {
|
||||||
|
val currentMessage = editMessage ?: return
|
||||||
|
dialog.setValue { MessageDialog.MessageDelete(currentMessage) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun editCurrentEditMessage() {
|
||||||
|
val newText = screenState.value.message.text
|
||||||
|
val lastText = lastMessageText.orEmpty().trim()
|
||||||
|
val currentReplyToCmId = replyToCmId
|
||||||
|
|
||||||
|
screenState.setValue { old ->
|
||||||
|
old.copy(
|
||||||
|
editCmId = null,
|
||||||
|
message = TextFieldValue(
|
||||||
|
text = lastText,
|
||||||
|
selection = TextRange(lastText.length)
|
||||||
|
),
|
||||||
|
actionMode = if (lastText.isBlank()) ActionMode.RECORD_AUDIO else ActionMode.SEND,
|
||||||
|
replyTitle = null,
|
||||||
|
replyText = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
syncUiMessages()
|
||||||
|
|
||||||
|
val newMessage = editMessage?.copy(
|
||||||
|
replyMessage = if (currentReplyToCmId == null) null else editMessage?.replyMessage,
|
||||||
|
text = newText
|
||||||
|
) ?: return
|
||||||
|
|
||||||
|
Log.d("MessagesHistoryViewModelImpl", "editMessage: $newMessage")
|
||||||
|
|
||||||
|
messagesUseCase.edit(
|
||||||
|
peerId = screenState.value.convoId,
|
||||||
|
cmId = newMessage.cmId,
|
||||||
|
message = newMessage.text,
|
||||||
|
attachments = null,
|
||||||
|
formatData = newMessage.formatData
|
||||||
|
).listenValue(viewModelScope) { state ->
|
||||||
|
state.processState(
|
||||||
|
error = { error ->
|
||||||
|
Log.d("MessagesHistoryViewModelImpl", "editMessage: ERROR: $error")
|
||||||
|
},
|
||||||
|
success = {
|
||||||
|
viewModelScope.launch {
|
||||||
|
messagesUseCase.storeMessage(newMessage)
|
||||||
|
refreshMessagesFromDb()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
replyToCmId = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateFormatting(type: FormatDataType) {
|
||||||
|
val selectionRange = screenState.value.message.selection
|
||||||
|
val newItems = formatData.items.toMutableList()
|
||||||
|
val wasRemoved = newItems.removeIfCompat {
|
||||||
|
it.type == type &&
|
||||||
|
it.offset == selectionRange.start &&
|
||||||
|
it.offset + it.length == selectionRange.end
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wasRemoved) {
|
||||||
|
newItems += VkMessage.FormatData.Item(
|
||||||
|
offset = selectionRange.start,
|
||||||
|
length = selectionRange.end - selectionRange.start,
|
||||||
|
type = type,
|
||||||
|
url = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
formatData = formatData.copy(items = newItems)
|
||||||
|
updateStyles()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateStyles() {
|
||||||
|
val annotations =
|
||||||
|
mutableListOf<AnnotatedString.Range<out AnnotatedString.Annotation>>()
|
||||||
|
|
||||||
|
formatData.items.forEach { item ->
|
||||||
|
val spanStyle = when (item.type) {
|
||||||
|
FormatDataType.BOLD -> SpanStyle(fontWeight = FontWeight.SemiBold)
|
||||||
|
FormatDataType.ITALIC -> SpanStyle(fontStyle = FontStyle.Italic)
|
||||||
|
FormatDataType.UNDERLINE -> SpanStyle(textDecoration = TextDecoration.Underline)
|
||||||
|
FormatDataType.URL -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
spanStyle?.let {
|
||||||
|
annotations += AnnotatedString.Range(
|
||||||
|
item = spanStyle,
|
||||||
|
start = item.offset,
|
||||||
|
end = item.offset + item.length
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val newText = AnnotatedString(
|
||||||
|
text = screenState.value.message.text,
|
||||||
|
annotations = annotations
|
||||||
|
)
|
||||||
|
|
||||||
|
screenState.setValue { old ->
|
||||||
|
old.copy(message = old.message.copy(annotatedString = newText))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshMessagesFromDb() {
|
||||||
|
viewModelScope.launchDbRefresh(
|
||||||
|
load = {
|
||||||
|
val localMessages = messagesUseCase.getLocalMessages(screenState.value.convoId)
|
||||||
|
messages.setValue { localMessages }
|
||||||
|
},
|
||||||
|
after = ::syncUiMessages
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package dev.meloda.fast.messageshistory
|
||||||
|
|
||||||
|
import dev.meloda.fast.common.extensions.setValue
|
||||||
|
import dev.meloda.fast.messageshistory.model.MessageNavigation
|
||||||
|
import dev.meloda.fast.messageshistory.model.MessagesHistoryScreenState
|
||||||
|
import dev.meloda.fast.model.api.domain.VkMessage
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
||||||
|
internal class MessagesHistoryNavigationActions(
|
||||||
|
private val screenState: MutableStateFlow<MessagesHistoryScreenState>,
|
||||||
|
private val messages: MutableStateFlow<List<VkMessage>>,
|
||||||
|
private val navigation: MutableStateFlow<MessageNavigation?>
|
||||||
|
) {
|
||||||
|
fun onTopBarClicked() {
|
||||||
|
val cmId = messages.value.firstOrNull()?.cmId ?: return
|
||||||
|
|
||||||
|
navigation.setValue {
|
||||||
|
MessageNavigation.ChatMaterials(
|
||||||
|
peerId = screenState.value.convoId,
|
||||||
|
cmId = cmId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
package dev.meloda.fast.messageshistory
|
||||||
|
|
||||||
|
import dev.meloda.fast.common.extensions.listenValue
|
||||||
|
import dev.meloda.fast.data.processState
|
||||||
|
import dev.meloda.fast.domain.GetMessageReadPeersUseCase
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
|
internal class MessagesHistoryReadPeersLoader(
|
||||||
|
private val scope: CoroutineScope,
|
||||||
|
private val getMessageReadPeersUseCase: GetMessageReadPeersUseCase
|
||||||
|
) {
|
||||||
|
suspend fun loadMessageReadPeers(peerId: Long, cmId: Long): Int =
|
||||||
|
suspendCancellableCoroutine { continuation ->
|
||||||
|
scope.launch {
|
||||||
|
getMessageReadPeersUseCase
|
||||||
|
.invoke(peerId = peerId, cmId = cmId)
|
||||||
|
.listenValue(scope) { state ->
|
||||||
|
state.processState(
|
||||||
|
error = { continuation.resume(-1) },
|
||||||
|
success = { count -> continuation.resume(count) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
-32
@@ -6,10 +6,8 @@ import androidx.compose.ui.text.input.TextFieldValue
|
|||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dev.meloda.fast.common.extensions.listenValue
|
|
||||||
import dev.meloda.fast.common.extensions.setValue
|
import dev.meloda.fast.common.extensions.setValue
|
||||||
import dev.meloda.fast.common.provider.ResourceProvider
|
import dev.meloda.fast.common.provider.ResourceProvider
|
||||||
import dev.meloda.fast.data.processState
|
|
||||||
import dev.meloda.fast.domain.ConvoUseCase
|
import dev.meloda.fast.domain.ConvoUseCase
|
||||||
import dev.meloda.fast.domain.GetMessageReadPeersUseCase
|
import dev.meloda.fast.domain.GetMessageReadPeersUseCase
|
||||||
import dev.meloda.fast.domain.LongPollUpdatesReducer
|
import dev.meloda.fast.domain.LongPollUpdatesReducer
|
||||||
@@ -25,9 +23,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
|
|
||||||
class MessagesHistoryViewModelImpl(
|
class MessagesHistoryViewModelImpl(
|
||||||
private val applicationContext: Context,
|
private val applicationContext: Context,
|
||||||
@@ -62,7 +57,7 @@ class MessagesHistoryViewModelImpl(
|
|||||||
screenState = screenState
|
screenState = screenState
|
||||||
)
|
)
|
||||||
|
|
||||||
private val messageActions = MessagesHistoryMessageActions(
|
private val messageSendEditActions = MessagesHistoryMessageSendEditActions(
|
||||||
viewModelScope = viewModelScope,
|
viewModelScope = viewModelScope,
|
||||||
messagesUseCase = messagesUseCase,
|
messagesUseCase = messagesUseCase,
|
||||||
resourceProvider = resourceProvider,
|
resourceProvider = resourceProvider,
|
||||||
@@ -70,8 +65,11 @@ class MessagesHistoryViewModelImpl(
|
|||||||
messages = messages,
|
messages = messages,
|
||||||
showKeyboard = showKeyboard,
|
showKeyboard = showKeyboard,
|
||||||
dialog = dialog,
|
dialog = dialog,
|
||||||
syncUiMessages = ::syncUiMessages,
|
syncUiMessages = ::syncUiMessages
|
||||||
onPinnedMessageChanged = pinnedMessageHandler::update
|
)
|
||||||
|
|
||||||
|
private val messageActions = MessagesHistoryMessageActions(
|
||||||
|
sendEditActions = messageSendEditActions
|
||||||
)
|
)
|
||||||
|
|
||||||
private val messageTransportActions = MessagesHistoryMessageTransportActions(
|
private val messageTransportActions = MessagesHistoryMessageTransportActions(
|
||||||
@@ -110,6 +108,17 @@ class MessagesHistoryViewModelImpl(
|
|||||||
onPinnedMessageChanged = pinnedMessageHandler::update
|
onPinnedMessageChanged = pinnedMessageHandler::update
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val navigationActions = MessagesHistoryNavigationActions(
|
||||||
|
screenState = screenState,
|
||||||
|
messages = messages,
|
||||||
|
navigation = navigation
|
||||||
|
)
|
||||||
|
|
||||||
|
private val readPeersLoader = MessagesHistoryReadPeersLoader(
|
||||||
|
scope = viewModelScope,
|
||||||
|
getMessageReadPeersUseCase = getMessageReadPeersUseCase
|
||||||
|
)
|
||||||
|
|
||||||
private val interactionHandler = MessagesHistoryInteractionHandler(
|
private val interactionHandler = MessagesHistoryInteractionHandler(
|
||||||
screenState = screenState,
|
screenState = screenState,
|
||||||
messages = messages,
|
messages = messages,
|
||||||
@@ -146,14 +155,7 @@ class MessagesHistoryViewModelImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onTopBarClicked() {
|
override fun onTopBarClicked() {
|
||||||
val cmId = messages.value.firstOrNull()?.cmId ?: return
|
navigationActions.onTopBarClicked()
|
||||||
|
|
||||||
navigation.setValue {
|
|
||||||
MessageNavigation.ChatMaterials(
|
|
||||||
peerId = screenState.value.convoId,
|
|
||||||
cmId = cmId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDialogConfirmed(dialog: MessageDialog, bundle: Bundle) =
|
override fun onDialogConfirmed(dialog: MessageDialog, bundle: Bundle) =
|
||||||
@@ -238,22 +240,7 @@ class MessagesHistoryViewModelImpl(
|
|||||||
messageActions.onKeyboardShown()
|
messageActions.onKeyboardShown()
|
||||||
|
|
||||||
override suspend fun loadMessageReadPeers(peerId: Long, cmId: Long): Int =
|
override suspend fun loadMessageReadPeers(peerId: Long, cmId: Long): Int =
|
||||||
suspendCancellableCoroutine {
|
readPeersLoader.loadMessageReadPeers(peerId = peerId, cmId = cmId)
|
||||||
viewModelScope.launch {
|
|
||||||
getMessageReadPeersUseCase
|
|
||||||
.invoke(peerId = peerId, cmId = cmId)
|
|
||||||
.listenValue(viewModelScope) { state ->
|
|
||||||
state.processState(
|
|
||||||
error = { error ->
|
|
||||||
it.resume(-1)
|
|
||||||
},
|
|
||||||
success = { count ->
|
|
||||||
it.resume(count)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun syncUiMessages(): List<MessageUiItem> {
|
private fun syncUiMessages(): List<MessageUiItem> {
|
||||||
val newUiMessages = buildMessagesHistoryUiMessages(
|
val newUiMessages = buildMessagesHistoryUiMessages(
|
||||||
|
|||||||
Reference in New Issue
Block a user