forked from melod1n/fast-messenger
refactor: split message actions and parsers
This commit is contained in:
+520
@@ -0,0 +1,520 @@
|
||||
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
|
||||
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 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
|
||||
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<MessagesHistoryScreenState>,
|
||||
private val messages: MutableStateFlow<List<VkMessage>>,
|
||||
private val baseError: MutableStateFlow<BaseError?>,
|
||||
private val showKeyboard: MutableStateFlow<Boolean>,
|
||||
private val dialog: MutableStateFlow<MessageDialog?>,
|
||||
private val syncUiMessages: () -> Unit,
|
||||
private val onPinnedMessageChanged: (VkMessage?) -> 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 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 onReplyCloseClicked() {
|
||||
replyToCmId = null
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
replyTitle = null,
|
||||
replyText = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 ->
|
||||
val newMessages = messages.value.toMutableList()
|
||||
newMessages[newMessages.indexOf(newMessage)] = newMessage.copy(
|
||||
id = response.messageId,
|
||||
cmId = response.cmId
|
||||
)
|
||||
messages.setValue { newMessages }
|
||||
syncUiMessages()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun confirmDeleteCurrentEditMessage() {
|
||||
val currentMessage = editMessage ?: return
|
||||
dialog.setValue { MessageDialog.MessageDelete(currentMessage) }
|
||||
}
|
||||
|
||||
fun editCurrentEditMessage() {
|
||||
replyToCmId = null
|
||||
|
||||
val newText = screenState.value.message.text
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
syncUiMessages()
|
||||
|
||||
val newMessage = editMessage?.copy(
|
||||
replyMessage = if (replyToCmId == null) null else editMessage?.replyMessage,
|
||||
text = newText
|
||||
) ?: return
|
||||
|
||||
Log.d("MessagesHistoryViewModelImpl", "editMessage: $newMessage")
|
||||
}
|
||||
|
||||
fun markAsImportant(messageIds: List<Long>, 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<Long>,
|
||||
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()
|
||||
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 handleError(error: State.Error) {
|
||||
VkUtils.parseError(error)?.let { newBaseError ->
|
||||
baseError.setValue { newBaseError }
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
-560
@@ -1,34 +1,18 @@
|
||||
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.os.Bundle
|
||||
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
|
||||
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 androidx.core.content.FileProvider
|
||||
import androidx.core.graphics.drawable.toBitmapOrNull
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import coil.imageLoader
|
||||
import coil.request.ImageRequest
|
||||
import com.conena.nanokt.collections.indexOfFirstOrNull
|
||||
import dev.meloda.fast.common.VkConstants
|
||||
import dev.meloda.fast.common.extensions.getParcelableCompat
|
||||
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.common.paging.canPaginate as canPaginatePage
|
||||
@@ -36,20 +20,16 @@ import dev.meloda.fast.common.paging.isPaginationExhausted as isPaginationExhaus
|
||||
import dev.meloda.fast.common.paging.loadingFlags
|
||||
import dev.meloda.fast.common.paging.mergePage
|
||||
import dev.meloda.fast.data.State
|
||||
import dev.meloda.fast.data.UserConfig
|
||||
import dev.meloda.fast.data.VkUtils
|
||||
import dev.meloda.fast.data.VkMemoryCache
|
||||
import dev.meloda.fast.data.processState
|
||||
import dev.meloda.fast.datastore.AppSettings
|
||||
import dev.meloda.fast.datastore.UserSettings
|
||||
import dev.meloda.fast.domain.ConvoUseCase
|
||||
import dev.meloda.fast.domain.GetMessageReadPeersUseCase
|
||||
import dev.meloda.fast.domain.LoadConvosByIdUseCase
|
||||
import dev.meloda.fast.domain.LongPollUpdatesParser
|
||||
import dev.meloda.fast.domain.MessagesUseCase
|
||||
import dev.meloda.fast.domain.util.extractAvatar
|
||||
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
|
||||
@@ -58,33 +38,20 @@ import dev.meloda.fast.messageshistory.model.MessageOption
|
||||
import dev.meloda.fast.messageshistory.model.MessagesHistoryScreenState
|
||||
import dev.meloda.fast.messageshistory.navigation.MessagesHistory
|
||||
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 dev.meloda.fast.ui.model.vk.MessageUiItem
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
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.coroutines.resume
|
||||
import kotlin.math.abs
|
||||
import kotlin.random.Random
|
||||
|
||||
class MessagesHistoryViewModelImpl(
|
||||
private val applicationContext: Context,
|
||||
private val messagesUseCase: MessagesUseCase,
|
||||
private val convoUseCase: ConvoUseCase,
|
||||
private val resourceProvider: ResourceProvider,
|
||||
private val userSettings: UserSettings,
|
||||
private val loadConvosByIdUseCase: LoadConvosByIdUseCase,
|
||||
private val getMessageReadPeersUseCase: GetMessageReadPeersUseCase,
|
||||
updatesParser: LongPollUpdatesParser,
|
||||
@@ -110,14 +77,19 @@ class MessagesHistoryViewModelImpl(
|
||||
override val messages = MutableStateFlow<List<VkMessage>>(emptyList())
|
||||
override val uiMessages = MutableStateFlow<List<MessageUiItem>>(emptyList())
|
||||
|
||||
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 val messageActions = MessagesHistoryMessageActions(
|
||||
applicationContext = applicationContext,
|
||||
viewModelScope = viewModelScope,
|
||||
messagesUseCase = messagesUseCase,
|
||||
resourceProvider = resourceProvider,
|
||||
screenState = screenState,
|
||||
messages = messages,
|
||||
baseError = baseError,
|
||||
showKeyboard = showKeyboard,
|
||||
dialog = dialog,
|
||||
syncUiMessages = ::syncUiMessages,
|
||||
onPinnedMessageChanged = ::handlePinnedMessage
|
||||
)
|
||||
|
||||
private val longPollEventHandler = MessagesHistoryLongPollEventHandler(
|
||||
screenState = screenState,
|
||||
@@ -177,7 +149,7 @@ class MessagesHistoryViewModelImpl(
|
||||
return
|
||||
}
|
||||
|
||||
deleteMessage(
|
||||
messageActions.deleteMessage(
|
||||
messageIds = listOf(dialog.message.id),
|
||||
deleteForAll = deleteForEveryone
|
||||
)
|
||||
@@ -192,7 +164,7 @@ class MessagesHistoryViewModelImpl(
|
||||
.filter { it.id > 0 }
|
||||
.map(VkMessage::id)
|
||||
|
||||
deleteMessage(
|
||||
messageActions.deleteMessage(
|
||||
messageIds = messageIdsToDelete,
|
||||
deleteForAll = deleteForEveryone,
|
||||
onSuccess = {
|
||||
@@ -206,15 +178,15 @@ class MessagesHistoryViewModelImpl(
|
||||
}
|
||||
|
||||
is MessageDialog.MessagePin -> {
|
||||
pinMessage(dialog.messageId)
|
||||
messageActions.pinMessage(dialog.messageId)
|
||||
}
|
||||
|
||||
is MessageDialog.MessageUnpin -> {
|
||||
unpinMessage(dialog.messageId)
|
||||
messageActions.unpinMessage(dialog.messageId)
|
||||
}
|
||||
|
||||
is MessageDialog.MessageMarkImportance -> {
|
||||
markAsImportant(
|
||||
messageActions.markAsImportant(
|
||||
messageIds = listOf(dialog.message.id),
|
||||
important = dialog.isImportant
|
||||
)
|
||||
@@ -222,7 +194,7 @@ class MessagesHistoryViewModelImpl(
|
||||
|
||||
is MessageDialog.MessageSpam -> {
|
||||
if (dialog.isSpam) {
|
||||
deleteMessage(
|
||||
messageActions.deleteMessage(
|
||||
messageIds = listOf(dialog.message.id),
|
||||
spam = true
|
||||
)
|
||||
@@ -250,7 +222,7 @@ class MessagesHistoryViewModelImpl(
|
||||
// TODO: 28-Mar-25, Danil Nikolaev: retry sending
|
||||
}
|
||||
|
||||
MessageOption.Reply -> replyToMessage(cmId)
|
||||
MessageOption.Reply -> messageActions.replyToMessage(cmId)
|
||||
|
||||
MessageOption.ForwardHere -> {
|
||||
|
||||
@@ -273,11 +245,11 @@ class MessagesHistoryViewModelImpl(
|
||||
}
|
||||
|
||||
MessageOption.Read -> {
|
||||
readMessage(dialog.message)
|
||||
messageActions.readMessage(dialog.message)
|
||||
}
|
||||
|
||||
MessageOption.Copy -> {
|
||||
copyMessage(dialog.message)
|
||||
messageActions.copyMessage(dialog.message)
|
||||
}
|
||||
|
||||
MessageOption.MarkAsImportant,
|
||||
@@ -301,7 +273,7 @@ class MessagesHistoryViewModelImpl(
|
||||
}
|
||||
|
||||
MessageOption.Edit -> {
|
||||
editMessage(cmId)
|
||||
messageActions.editMessage(cmId)
|
||||
syncUiMessages()
|
||||
}
|
||||
|
||||
@@ -332,7 +304,7 @@ class MessagesHistoryViewModelImpl(
|
||||
}
|
||||
|
||||
if (screenState.value.editCmId != null) {
|
||||
stopEditMessage()
|
||||
messageActions.stopEditMessage()
|
||||
}
|
||||
|
||||
syncUiMessages()
|
||||
@@ -380,9 +352,9 @@ class MessagesHistoryViewModelImpl(
|
||||
|
||||
override fun onActionButtonClicked() {
|
||||
when (screenState.value.actionMode) {
|
||||
ActionMode.DELETE -> confirmDeleteCurrentEditMessage()
|
||||
ActionMode.DELETE -> messageActions.confirmDeleteCurrentEditMessage()
|
||||
|
||||
ActionMode.EDIT -> editCurrentEditMessage()
|
||||
ActionMode.EDIT -> messageActions.editCurrentEditMessage()
|
||||
|
||||
ActionMode.RECORD_AUDIO -> {
|
||||
screenState.setValue { it.copy(actionMode = ActionMode.RECORD_VIDEO) }
|
||||
@@ -392,7 +364,7 @@ class MessagesHistoryViewModelImpl(
|
||||
screenState.setValue { it.copy(actionMode = ActionMode.RECORD_AUDIO) }
|
||||
}
|
||||
|
||||
ActionMode.SEND -> sendMessage()
|
||||
ActionMode.SEND -> messageActions.sendMessage()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,7 +435,7 @@ class MessagesHistoryViewModelImpl(
|
||||
|
||||
selectedMessages.setValue { emptyList() }
|
||||
|
||||
editMessage(cmId)
|
||||
messageActions.editMessage(cmId)
|
||||
|
||||
syncUiMessages()
|
||||
}
|
||||
@@ -474,175 +446,16 @@ class MessagesHistoryViewModelImpl(
|
||||
}
|
||||
}
|
||||
|
||||
private 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun editMessage(cmId: Long) {
|
||||
this.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 }
|
||||
}
|
||||
|
||||
private 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,
|
||||
|
||||
// TODO: 13/03/2026, Danil Nikolaev: use last reply
|
||||
replyTitle = null,
|
||||
replyText = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private var formatData = VkMessage.FormatData("1", emptyList())
|
||||
|
||||
private fun updateStyles() {
|
||||
val annotations =
|
||||
mutableListOf<AnnotatedString.Range<out AnnotatedString.Annotation>>()
|
||||
|
||||
formatData.items.forEachIndexed { index, 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))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBoldClicked() {
|
||||
val selectionRange = screenState.value.message.selection
|
||||
val newItems = formatData.items.toMutableList()
|
||||
val wasRemoved = newItems.removeIfCompat {
|
||||
it.type == FormatDataType.BOLD &&
|
||||
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 = FormatDataType.BOLD,
|
||||
url = null
|
||||
)
|
||||
}
|
||||
|
||||
formatData = formatData.copy(items = newItems)
|
||||
updateStyles()
|
||||
messageActions.onBoldClicked()
|
||||
}
|
||||
|
||||
override fun onItalicClicked() {
|
||||
val selectionRange = screenState.value.message.selection
|
||||
val newItems = formatData.items.toMutableList()
|
||||
val wasRemoved = newItems.removeIfCompat {
|
||||
it.type == FormatDataType.ITALIC &&
|
||||
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 = FormatDataType.ITALIC,
|
||||
url = null
|
||||
)
|
||||
}
|
||||
|
||||
formatData = formatData.copy(items = newItems)
|
||||
updateStyles()
|
||||
messageActions.onItalicClicked()
|
||||
}
|
||||
|
||||
override fun onUnderlineClicked() {
|
||||
val selectionRange = screenState.value.message.selection
|
||||
val newItems = formatData.items.toMutableList()
|
||||
val wasRemoved = newItems.removeIfCompat {
|
||||
it.type == FormatDataType.UNDERLINE &&
|
||||
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 = FormatDataType.UNDERLINE,
|
||||
url = null
|
||||
)
|
||||
}
|
||||
|
||||
formatData = formatData.copy(items = newItems)
|
||||
updateStyles()
|
||||
messageActions.onUnderlineClicked()
|
||||
}
|
||||
|
||||
override fun onLinkClicked() {
|
||||
@@ -650,23 +463,15 @@ class MessagesHistoryViewModelImpl(
|
||||
}
|
||||
|
||||
override fun onRegularClicked() {
|
||||
formatData = formatData.copy(items = emptyList())
|
||||
updateStyles()
|
||||
messageActions.onRegularClicked()
|
||||
}
|
||||
|
||||
override fun onReplyCloseClicked() {
|
||||
replyToCmId = null
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
replyTitle = null,
|
||||
replyText = null
|
||||
)
|
||||
}
|
||||
messageActions.onReplyCloseClicked()
|
||||
}
|
||||
|
||||
override fun onRequestReplyToMessage(cmId: Long) {
|
||||
replyToMessage(cmId)
|
||||
messageActions.replyToMessage(cmId)
|
||||
}
|
||||
|
||||
override fun onKeyboardShown() {
|
||||
@@ -827,337 +632,6 @@ class MessagesHistoryViewModelImpl(
|
||||
}
|
||||
}
|
||||
|
||||
private 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 ->
|
||||
val newMessages = messages.value.toMutableList()
|
||||
newMessages[newMessages.indexOf(newMessage)] = newMessage.copy(
|
||||
id = response.messageId,
|
||||
cmId = response.cmId
|
||||
)
|
||||
messages.setValue { newMessages }
|
||||
syncUiMessages()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun confirmDeleteCurrentEditMessage() {
|
||||
val currentMessage = editMessage ?: return
|
||||
|
||||
this.dialog.setValue {
|
||||
MessageDialog.MessageDelete(currentMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun editCurrentEditMessage() {
|
||||
replyToCmId = null
|
||||
|
||||
val newText = screenState.value.message.text
|
||||
|
||||
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,
|
||||
|
||||
// TODO: 13/03/2026, Danil Nikolaev: save last reply
|
||||
replyTitle = null,
|
||||
replyText = null
|
||||
)
|
||||
}
|
||||
|
||||
syncUiMessages()
|
||||
|
||||
// TODO: 13/03/2026, Danil Nikolaev: actually edit message
|
||||
|
||||
val newMessage = editMessage?.copy(
|
||||
replyMessage = if (replyToCmId == null) null else editMessage?.replyMessage,
|
||||
text = newText
|
||||
) ?: return
|
||||
|
||||
// TODO: 13/03/2026, Danil Nikolaev: check if message is exact same, then do not edit
|
||||
|
||||
Log.d("MessagesHistoryViewModelImpl", "editMessage: $newMessage")
|
||||
}
|
||||
|
||||
private fun markAsImportant(
|
||||
messageIds: List<Long>,
|
||||
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()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteMessage(
|
||||
messageIds: List<Long>,
|
||||
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()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun pinMessage(messageId: Long) {
|
||||
messagesUseCase.pin(
|
||||
peerId = screenState.value.convoId,
|
||||
messageId = messageId,
|
||||
cmId = null
|
||||
).listenValue(viewModelScope) { state ->
|
||||
state.processState(
|
||||
error = ::handleError,
|
||||
success = { pinnedMessage ->
|
||||
handlePinnedMessage(pinnedMessage)
|
||||
|
||||
val newMessages = messages.value.toMutableList()
|
||||
val index = newMessages.indexOfFirstOrNull { it.id == messageId }
|
||||
|
||||
if (index == null) {// сообщения нет в списке
|
||||
// pizdets
|
||||
} else {
|
||||
newMessages[index] = pinnedMessage
|
||||
messages.setValue { newMessages }
|
||||
syncUiMessages()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private 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) { // сообщения нет в списке
|
||||
// pizdets
|
||||
} else {
|
||||
newMessages[index] = newMessages[index].copy(isPinned = false)
|
||||
messages.setValue { newMessages }
|
||||
syncUiMessages()
|
||||
}
|
||||
|
||||
handlePinnedMessage(null)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private 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()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private 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 syncUiMessages(): List<MessageUiItem> {
|
||||
val newUiMessages = buildMessagesHistoryUiMessages(
|
||||
messages = messages.value,
|
||||
|
||||
Reference in New Issue
Block a user