refactor: split messages history actions

This commit is contained in:
Codex
2026-05-14 21:05:20 +03:00
parent 0ffd92b875
commit 6b7f8f2397
5 changed files with 487 additions and 426 deletions
@@ -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
)
}
} }
@@ -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
)
}
}
@@ -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
)
}
}
}
@@ -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) }
)
}
}
}
}
@@ -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(