diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryMessageActions.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryMessageActions.kt index 6eb62c19..6c4baaba 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryMessageActions.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryMessageActions.kt @@ -1,401 +1,23 @@ 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.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( - private val viewModelScope: CoroutineScope, - private val messagesUseCase: MessagesUseCase, - private val resourceProvider: ResourceProvider, - private val screenState: MutableStateFlow, - private val messages: MutableStateFlow>, - private val showKeyboard: MutableStateFlow, - private val dialog: MutableStateFlow, - private val syncUiMessages: () -> Unit, - private val onPinnedMessageChanged: (VkMessage?) -> Unit + private val sendEditActions: MessagesHistoryMessageSendEditActions ) { - private var lastMessageText: String? = null - private val sendingMessages: MutableList = mutableListOf() - private val failedMessages: MutableList = 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>() - - 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 - ) - } - + fun replyToMessage(cmId: Long) = sendEditActions.replyToMessage(cmId) + fun onMessageInputChanged(newText: TextFieldValue) = sendEditActions.onMessageInputChanged(newText) + fun onEmojiButtonLongClicked() = sendEditActions.onEmojiButtonLongClicked() + fun editMessage(cmId: Long) = sendEditActions.editMessage(cmId) + fun stopEditMessage() = sendEditActions.stopEditMessage() + fun onBoldClicked() = sendEditActions.onBoldClicked() + fun onItalicClicked() = sendEditActions.onItalicClicked() + fun onUnderlineClicked() = sendEditActions.onUnderlineClicked() + fun onRegularClicked() = sendEditActions.onRegularClicked() + fun onActionButtonClicked() = sendEditActions.onActionButtonClicked() + fun onReplyCloseClicked() = sendEditActions.onReplyCloseClicked() + fun onKeyboardShown() = sendEditActions.onKeyboardShown() + fun sendMessage() = sendEditActions.sendMessage() + fun confirmDeleteCurrentEditMessage() = sendEditActions.confirmDeleteCurrentEditMessage() + fun editCurrentEditMessage() = sendEditActions.editCurrentEditMessage() } diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryMessageSendEditActions.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryMessageSendEditActions.kt new file mode 100644 index 00000000..9fac1c23 --- /dev/null +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryMessageSendEditActions.kt @@ -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, + private val messages: MutableStateFlow>, + private val showKeyboard: MutableStateFlow, + private val dialog: MutableStateFlow, + private val syncUiMessages: () -> Unit +) { + private var lastMessageText: String? = null + private val sendingMessages: MutableList = mutableListOf() + private val failedMessages: MutableList = 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>() + + 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 + ) + } +} diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryNavigationActions.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryNavigationActions.kt new file mode 100644 index 00000000..08507f42 --- /dev/null +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryNavigationActions.kt @@ -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, + private val messages: MutableStateFlow>, + private val navigation: MutableStateFlow +) { + fun onTopBarClicked() { + val cmId = messages.value.firstOrNull()?.cmId ?: return + + navigation.setValue { + MessageNavigation.ChatMaterials( + peerId = screenState.value.convoId, + cmId = cmId + ) + } + } +} diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryReadPeersLoader.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryReadPeersLoader.kt new file mode 100644 index 00000000..d31dc21b --- /dev/null +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryReadPeersLoader.kt @@ -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) } + ) + } + } + } +} diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModelImpl.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModelImpl.kt index ed23db51..c848f89e 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModelImpl.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModelImpl.kt @@ -6,10 +6,8 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import dev.meloda.fast.common.extensions.listenValue import dev.meloda.fast.common.extensions.setValue import dev.meloda.fast.common.provider.ResourceProvider -import dev.meloda.fast.data.processState import dev.meloda.fast.domain.ConvoUseCase import dev.meloda.fast.domain.GetMessageReadPeersUseCase import dev.meloda.fast.domain.LongPollUpdatesReducer @@ -25,9 +23,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlin.coroutines.resume class MessagesHistoryViewModelImpl( private val applicationContext: Context, @@ -62,7 +57,7 @@ class MessagesHistoryViewModelImpl( screenState = screenState ) - private val messageActions = MessagesHistoryMessageActions( + private val messageSendEditActions = MessagesHistoryMessageSendEditActions( viewModelScope = viewModelScope, messagesUseCase = messagesUseCase, resourceProvider = resourceProvider, @@ -70,8 +65,11 @@ class MessagesHistoryViewModelImpl( messages = messages, showKeyboard = showKeyboard, dialog = dialog, - syncUiMessages = ::syncUiMessages, - onPinnedMessageChanged = pinnedMessageHandler::update + syncUiMessages = ::syncUiMessages + ) + + private val messageActions = MessagesHistoryMessageActions( + sendEditActions = messageSendEditActions ) private val messageTransportActions = MessagesHistoryMessageTransportActions( @@ -110,6 +108,17 @@ class MessagesHistoryViewModelImpl( 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( screenState = screenState, messages = messages, @@ -146,14 +155,7 @@ class MessagesHistoryViewModelImpl( } override fun onTopBarClicked() { - val cmId = messages.value.firstOrNull()?.cmId ?: return - - navigation.setValue { - MessageNavigation.ChatMaterials( - peerId = screenState.value.convoId, - cmId = cmId - ) - } + navigationActions.onTopBarClicked() } override fun onDialogConfirmed(dialog: MessageDialog, bundle: Bundle) = @@ -238,22 +240,7 @@ class MessagesHistoryViewModelImpl( messageActions.onKeyboardShown() override suspend fun loadMessageReadPeers(peerId: Long, cmId: Long): Int = - suspendCancellableCoroutine { - viewModelScope.launch { - getMessageReadPeersUseCase - .invoke(peerId = peerId, cmId = cmId) - .listenValue(viewModelScope) { state -> - state.processState( - error = { error -> - it.resume(-1) - }, - success = { count -> - it.resume(count) - } - ) - } - } - } + readPeersLoader.loadMessageReadPeers(peerId = peerId, cmId = cmId) private fun syncUiMessages(): List { val newUiMessages = buildMessagesHistoryUiMessages(