From f24eae82092714c3eef67f28b8c09cf9495db3dd Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 14 May 2026 18:24:46 +0300 Subject: [PATCH] refactor: split message transport actions --- .../MessagesHistoryMessageActions.kt | 204 ---------------- .../MessagesHistoryMessageTransportActions.kt | 222 ++++++++++++++++++ .../MessagesHistoryViewModelImpl.kt | 29 ++- 3 files changed, 241 insertions(+), 214 deletions(-) create mode 100644 feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryMessageTransportActions.kt 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 9928e56f..f5d3151f 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,12 +1,6 @@ package dev.meloda.fast.messageshistory -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.graphics.Bitmap -import android.os.Build import android.util.Log -import android.widget.Toast import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextRange @@ -15,21 +9,12 @@ 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 androidx.core.content.FileProvider -import androidx.core.graphics.drawable.toBitmapOrNull -import coil.imageLoader -import coil.request.ImageRequest -import com.conena.nanokt.collections.indexOfFirstOrNull import dev.meloda.fast.common.extensions.listenValue -import dev.meloda.fast.common.extensions.orDots 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.State import dev.meloda.fast.data.UserConfig import dev.meloda.fast.data.VkMemoryCache -import dev.meloda.fast.data.VkUtils -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 @@ -37,32 +22,22 @@ 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.BaseError import dev.meloda.fast.model.api.domain.FormatDataType import dev.meloda.fast.model.api.domain.VkMessage -import dev.meloda.fast.model.api.domain.VkPhotoDomain -import dev.meloda.fast.ui.R -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import kotlinx.serialization.json.add import kotlinx.serialization.json.buildJsonArray import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put -import java.io.File -import java.io.FileOutputStream import kotlin.random.Random internal class MessagesHistoryMessageActions( - private val applicationContext: Context, private val viewModelScope: CoroutineScope, private val messagesUseCase: MessagesUseCase, private val resourceProvider: ResourceProvider, private val screenState: MutableStateFlow, private val messages: MutableStateFlow>, - private val baseError: MutableStateFlow, private val showKeyboard: MutableStateFlow, private val dialog: MutableStateFlow, private val syncUiMessages: () -> Unit, @@ -285,180 +260,6 @@ internal class MessagesHistoryMessageActions( Log.d("MessagesHistoryViewModelImpl", "editMessage: $newMessage") } - fun markAsImportant(messageIds: List, important: Boolean) { - messagesUseCase.markAsImportant( - peerId = screenState.value.convoId, - messageIds = messageIds, - important = important - ).listenValue(viewModelScope) { state -> - state.processState( - error = ::handleError, - success = { - val newMessages = messages.value - .toMutableList() - .map { message -> - if (message.id in messageIds) { - message.copy(isImportant = important) - } else { - message - } - } - messages.setValue { newMessages } - syncUiMessages() - } - ) - } - } - - fun deleteMessage( - messageIds: List, - spam: Boolean = false, - deleteForAll: Boolean = false, - onSuccess: () -> Unit = {} - ) { - messagesUseCase.delete( - peerId = screenState.value.convoId, - messageIds = messageIds, - spam = spam, - deleteForAll = deleteForAll - ).listenValue(viewModelScope) { state -> - state.processState( - error = ::handleError, - success = { - onSuccess() - val newMessages = messages.value.toMutableList() - val messagesToDelete = newMessages.filter { it.id in messageIds } - newMessages.removeAll(messagesToDelete) - messages.setValue { newMessages } - syncUiMessages() - } - ) - } - } - - fun pinMessage(messageId: Long) { - messagesUseCase.pin( - peerId = screenState.value.convoId, - messageId = messageId, - cmId = null - ).listenValue(viewModelScope) { state -> - state.processState( - error = ::handleError, - success = { pinnedMessage -> - onPinnedMessageChanged(pinnedMessage) - - val newMessages = messages.value.toMutableList() - val index = newMessages.indexOfFirstOrNull { it.id == messageId } - if (index != null) { - newMessages[index] = pinnedMessage - messages.setValue { newMessages } - syncUiMessages() - } - } - ) - } - } - - fun unpinMessage(messageId: Long) { - messagesUseCase.unpin(screenState.value.convoId) - .listenValue(viewModelScope) { state -> - state.processState( - error = ::handleError, - success = { - val newMessages = messages.value.toMutableList() - val index = newMessages.indexOfFirstOrNull { it.id == messageId } - if (index != null) { - newMessages[index] = newMessages[index].copy(isPinned = false) - messages.setValue { newMessages } - syncUiMessages() - } - - onPinnedMessageChanged(null) - } - ) - } - } - - fun readMessage(message: VkMessage) { - messagesUseCase.markAsRead( - peerId = screenState.value.convoId, - startMessageId = message.id - ).listenValue(viewModelScope) { state -> - state.processState( - error = ::handleError, - success = { - val oldConvo = screenState.value.convo - val newConvo = oldConvo.copy( - inRead = if (!message.isOut) message.id else oldConvo.inRead, - outRead = if (message.isOut) message.id else oldConvo.outRead - ) - - screenState.setValue { old -> - old.copy(convo = newConvo) - } - - syncUiMessages() - } - ) - } - } - - fun copyMessage(message: VkMessage) { - val clipboardManager = - applicationContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - - val messageToCopy = message.text.orEmpty().trim() - if (messageToCopy.isEmpty()) { - val photo = with(message.attachments.orEmpty()) { - if (size == 1 && all { it is VkPhotoDomain }) { - first() as? VkPhotoDomain - } else null - } ?: return - - val photoMaxSize = photo.getMaxSize() ?: return - - viewModelScope.launch(Dispatchers.IO) { - val drawable = applicationContext.imageLoader.execute( - ImageRequest.Builder(applicationContext) - .data(photoMaxSize.url) - .build() - ).drawable ?: return@launch - - val imagesDir = File(applicationContext.cacheDir, "images") - if (!imagesDir.exists()) imagesDir.mkdirs() - val imageFile = File(imagesDir, "shared_image_id${photo.id}.png") - FileOutputStream(imageFile).use { - drawable.toBitmapOrNull()?.compress(Bitmap.CompressFormat.PNG, 100, it) - } - - val uri = FileProvider.getUriForFile( - applicationContext, - "${applicationContext.packageName}.provider", - imageFile - ) - - val clip = ClipData.newUri(applicationContext.contentResolver, "Image", uri) - clipboardManager.setPrimaryClip(clip) - - withContext(Dispatchers.Main) { - Toast.makeText( - applicationContext, - "Image copied to clipboard", - Toast.LENGTH_SHORT - ).show() - } - } - return - } - - clipboardManager.setPrimaryClip(ClipData.newPlainText("Message", messageToCopy)) - - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) { - Toast.makeText(applicationContext, R.string.copied_to_clipboard, Toast.LENGTH_SHORT) - .show() - } - } - private fun updateFormatting(type: FormatDataType) { val selectionRange = screenState.value.message.selection val newItems = formatData.items.toMutableList() @@ -512,9 +313,4 @@ internal class MessagesHistoryMessageActions( } } - private fun handleError(error: State.Error) { - VkUtils.parseError(error)?.let { newBaseError -> - baseError.setValue { newBaseError } - } - } } diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryMessageTransportActions.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryMessageTransportActions.kt new file mode 100644 index 00000000..a7333248 --- /dev/null +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryMessageTransportActions.kt @@ -0,0 +1,222 @@ +package dev.meloda.fast.messageshistory + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.graphics.Bitmap +import android.os.Build +import android.widget.Toast +import androidx.core.content.FileProvider +import androidx.core.graphics.drawable.toBitmapOrNull +import coil.imageLoader +import coil.request.ImageRequest +import com.conena.nanokt.collections.indexOfFirstOrNull +import dev.meloda.fast.common.extensions.listenValue +import dev.meloda.fast.common.extensions.setValue +import dev.meloda.fast.data.State +import dev.meloda.fast.data.VkUtils +import dev.meloda.fast.data.processState +import dev.meloda.fast.domain.MessagesUseCase +import dev.meloda.fast.messageshistory.model.MessagesHistoryScreenState +import dev.meloda.fast.model.BaseError +import dev.meloda.fast.model.api.domain.VkMessage +import dev.meloda.fast.model.api.domain.VkPhotoDomain +import dev.meloda.fast.ui.R +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileOutputStream + +internal class MessagesHistoryMessageTransportActions( + private val applicationContext: Context, + private val viewModelScope: CoroutineScope, + private val messagesUseCase: MessagesUseCase, + private val screenState: MutableStateFlow, + private val messages: MutableStateFlow>, + private val baseError: MutableStateFlow, + private val syncUiMessages: () -> Unit, + private val onPinnedMessageChanged: (VkMessage?) -> Unit +) { + fun markAsImportant(messageIds: List, important: Boolean) { + messagesUseCase.markAsImportant( + peerId = screenState.value.convoId, + messageIds = messageIds, + important = important + ).listenValue(viewModelScope) { state -> + state.processState( + error = ::handleError, + success = { + val newMessages = messages.value + .toMutableList() + .map { message -> + if (message.id in messageIds) { + message.copy(isImportant = important) + } else { + message + } + } + messages.setValue { newMessages } + syncUiMessages() + } + ) + } + } + + fun deleteMessage( + messageIds: List, + spam: Boolean = false, + deleteForAll: Boolean = false, + onSuccess: () -> Unit = {} + ) { + messagesUseCase.delete( + peerId = screenState.value.convoId, + messageIds = messageIds, + spam = spam, + deleteForAll = deleteForAll + ).listenValue(viewModelScope) { state -> + state.processState( + error = ::handleError, + success = { + onSuccess() + val newMessages = messages.value.toMutableList() + val messagesToDelete = newMessages.filter { it.id in messageIds } + newMessages.removeAll(messagesToDelete) + messages.setValue { newMessages } + syncUiMessages() + } + ) + } + } + + fun pinMessage(messageId: Long) { + messagesUseCase.pin( + peerId = screenState.value.convoId, + messageId = messageId, + cmId = null + ).listenValue(viewModelScope) { state -> + state.processState( + error = ::handleError, + success = { pinnedMessage -> + onPinnedMessageChanged(pinnedMessage) + + val newMessages = messages.value.toMutableList() + val index = newMessages.indexOfFirstOrNull { it.id == messageId } + if (index != null) { + newMessages[index] = pinnedMessage + messages.setValue { newMessages } + syncUiMessages() + } + } + ) + } + } + + fun unpinMessage(messageId: Long) { + messagesUseCase.unpin(screenState.value.convoId) + .listenValue(viewModelScope) { state -> + state.processState( + error = ::handleError, + success = { + val newMessages = messages.value.toMutableList() + val index = newMessages.indexOfFirstOrNull { it.id == messageId } + if (index != null) { + newMessages[index] = newMessages[index].copy(isPinned = false) + messages.setValue { newMessages } + syncUiMessages() + } + + onPinnedMessageChanged(null) + } + ) + } + } + + fun readMessage(message: VkMessage) { + messagesUseCase.markAsRead( + peerId = screenState.value.convoId, + startMessageId = message.id + ).listenValue(viewModelScope) { state -> + state.processState( + error = ::handleError, + success = { + val oldConvo = screenState.value.convo + val newConvo = oldConvo.copy( + inRead = if (!message.isOut) message.id else oldConvo.inRead, + outRead = if (message.isOut) message.id else oldConvo.outRead + ) + + screenState.setValue { old -> + old.copy(convo = newConvo) + } + + syncUiMessages() + } + ) + } + } + + fun copyMessage(message: VkMessage) { + val clipboardManager = + applicationContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + + val messageToCopy = message.text.orEmpty().trim() + if (messageToCopy.isEmpty()) { + val photo = with(message.attachments.orEmpty()) { + if (size == 1 && all { it is VkPhotoDomain }) { + first() as? VkPhotoDomain + } else null + } ?: return + + val photoMaxSize = photo.getMaxSize() ?: return + + viewModelScope.launch(Dispatchers.IO) { + val drawable = applicationContext.imageLoader.execute( + ImageRequest.Builder(applicationContext) + .data(photoMaxSize.url) + .build() + ).drawable ?: return@launch + + val imagesDir = File(applicationContext.cacheDir, "images") + if (!imagesDir.exists()) imagesDir.mkdirs() + val imageFile = File(imagesDir, "shared_image_id${photo.id}.png") + FileOutputStream(imageFile).use { + drawable.toBitmapOrNull()?.compress(Bitmap.CompressFormat.PNG, 100, it) + } + + val uri = FileProvider.getUriForFile( + applicationContext, + "${applicationContext.packageName}.provider", + imageFile + ) + + val clip = ClipData.newUri(applicationContext.contentResolver, "Image", uri) + clipboardManager.setPrimaryClip(clip) + + withContext(Dispatchers.Main) { + Toast.makeText( + applicationContext, + "Image copied to clipboard", + Toast.LENGTH_SHORT + ).show() + } + } + return + } + + clipboardManager.setPrimaryClip(ClipData.newPlainText("Message", messageToCopy)) + + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) { + Toast.makeText(applicationContext, R.string.copied_to_clipboard, Toast.LENGTH_SHORT) + .show() + } + } + + private fun handleError(error: State.Error) { + VkUtils.parseError(error)?.let { newBaseError -> + baseError.setValue { newBaseError } + } + } +} 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 561ed12d..72a87cc4 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 @@ -66,19 +66,28 @@ class MessagesHistoryViewModelImpl( ) private val messageActions = MessagesHistoryMessageActions( - applicationContext = applicationContext, viewModelScope = viewModelScope, messagesUseCase = messagesUseCase, resourceProvider = resourceProvider, screenState = screenState, messages = messages, - baseError = baseError, showKeyboard = showKeyboard, dialog = dialog, syncUiMessages = ::syncUiMessages, onPinnedMessageChanged = pinnedMessageHandler::update ) + private val messageTransportActions = MessagesHistoryMessageTransportActions( + applicationContext = applicationContext, + viewModelScope = viewModelScope, + messagesUseCase = messagesUseCase, + screenState = screenState, + messages = messages, + baseError = baseError, + syncUiMessages = ::syncUiMessages, + onPinnedMessageChanged = pinnedMessageHandler::update + ) + private val loaders = MessagesHistoryLoaders( convoUseCase = convoUseCase, messagesUseCase = messagesUseCase, @@ -151,7 +160,7 @@ class MessagesHistoryViewModelImpl( return } - messageActions.deleteMessage( + messageTransportActions.deleteMessage( messageIds = listOf(dialog.message.id), deleteForAll = deleteForEveryone ) @@ -166,7 +175,7 @@ class MessagesHistoryViewModelImpl( .filter { it.id > 0 } .map(VkMessage::id) - messageActions.deleteMessage( + messageTransportActions.deleteMessage( messageIds = messageIdsToDelete, deleteForAll = deleteForEveryone, onSuccess = { @@ -180,15 +189,15 @@ class MessagesHistoryViewModelImpl( } is MessageDialog.MessagePin -> { - messageActions.pinMessage(dialog.messageId) + messageTransportActions.pinMessage(dialog.messageId) } is MessageDialog.MessageUnpin -> { - messageActions.unpinMessage(dialog.messageId) + messageTransportActions.unpinMessage(dialog.messageId) } is MessageDialog.MessageMarkImportance -> { - messageActions.markAsImportant( + messageTransportActions.markAsImportant( messageIds = listOf(dialog.message.id), important = dialog.isImportant ) @@ -196,7 +205,7 @@ class MessagesHistoryViewModelImpl( is MessageDialog.MessageSpam -> { if (dialog.isSpam) { - messageActions.deleteMessage( + messageTransportActions.deleteMessage( messageIds = listOf(dialog.message.id), spam = true ) @@ -247,11 +256,11 @@ class MessagesHistoryViewModelImpl( } MessageOption.Read -> { - messageActions.readMessage(dialog.message) + messageTransportActions.readMessage(dialog.message) } MessageOption.Copy -> { - messageActions.copyMessage(dialog.message) + messageTransportActions.copyMessage(dialog.message) } MessageOption.MarkAsImportant,