refactor: split message actions and parsers
This commit is contained in:
@@ -0,0 +1,558 @@
|
|||||||
|
package dev.meloda.fast.domain
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import dev.meloda.fast.common.VkConstants
|
||||||
|
import dev.meloda.fast.common.extensions.asInt
|
||||||
|
import dev.meloda.fast.common.extensions.asLong
|
||||||
|
import dev.meloda.fast.common.extensions.listenValue
|
||||||
|
import dev.meloda.fast.common.extensions.toList
|
||||||
|
import dev.meloda.fast.data.UserConfig
|
||||||
|
import dev.meloda.fast.data.processState
|
||||||
|
import dev.meloda.fast.model.ApiEvent
|
||||||
|
import dev.meloda.fast.model.ConvoFlags
|
||||||
|
import dev.meloda.fast.model.InteractionType
|
||||||
|
import dev.meloda.fast.model.LongPollEvent
|
||||||
|
import dev.meloda.fast.model.LongPollParsedEvent
|
||||||
|
import dev.meloda.fast.model.MessageFlags
|
||||||
|
import dev.meloda.fast.model.api.domain.VkConvo
|
||||||
|
import dev.meloda.fast.model.api.domain.VkMessage
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
|
internal class LongPollEventParser(
|
||||||
|
private val coroutineScope: CoroutineScope,
|
||||||
|
private val convoUseCase: ConvoUseCase,
|
||||||
|
private val messagesUseCase: MessagesUseCase,
|
||||||
|
private val dispatch: (LongPollEvent, LongPollParsedEvent) -> Unit,
|
||||||
|
private val dispatchAll: (LongPollEvent, List<LongPollParsedEvent>) -> Unit
|
||||||
|
) {
|
||||||
|
fun parseNextUpdate(event: List<Any>) {
|
||||||
|
val eventId = event.first().asInt()
|
||||||
|
|
||||||
|
when (val eventType = ApiEvent.parseOrNull(eventId)) {
|
||||||
|
null -> Log.d("LongPollEventParser", "parseNextUpdate: unknownEvent: $event")
|
||||||
|
|
||||||
|
ApiEvent.MESSAGE_SET_FLAGS -> parseMessageSetFlags(eventType, event)
|
||||||
|
ApiEvent.MESSAGE_CLEAR_FLAGS -> parseMessageClearFlags(eventType, event)
|
||||||
|
ApiEvent.MESSAGE_NEW -> parseMessageNew(eventType, event)
|
||||||
|
ApiEvent.MESSAGE_EDIT -> parseMessageEdit(eventType, event)
|
||||||
|
ApiEvent.MESSAGE_READ_INCOMING -> parseMessageReadIncoming(eventType, event)
|
||||||
|
ApiEvent.MESSAGE_READ_OUTGOING -> parseMessageReadOutgoing(eventType, event)
|
||||||
|
ApiEvent.CHAT_CLEAR_FLAGS -> parseChatClearFlags(eventType, event)
|
||||||
|
ApiEvent.CHAT_SET_FLAGS -> parseChatSetFlags(eventType, event)
|
||||||
|
ApiEvent.MESSAGES_DELETED -> parseMessagesDeleted(eventType, event)
|
||||||
|
ApiEvent.CHAT_MAJOR_CHANGED -> parseChatMajorChanged(eventType, event)
|
||||||
|
ApiEvent.CHAT_MINOR_CHANGED -> parseChatMinorChanged(eventType, event)
|
||||||
|
|
||||||
|
ApiEvent.TYPING,
|
||||||
|
ApiEvent.AUDIO_MESSAGE_RECORDING,
|
||||||
|
ApiEvent.PHOTO_UPLOADING,
|
||||||
|
ApiEvent.VIDEO_UPLOADING,
|
||||||
|
ApiEvent.FILE_UPLOADING -> parseInteraction(eventType, event)
|
||||||
|
|
||||||
|
ApiEvent.UNREAD_COUNT_UPDATE -> parseUnreadCounterUpdate(eventType, event)
|
||||||
|
ApiEvent.MESSAGE_UPDATED -> parseMessageUpdated(eventType, event)
|
||||||
|
ApiEvent.MESSAGE_CACHE_CLEAR -> parseMessageCacheClear(eventType, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseMessageSetFlags(eventType: ApiEvent, event: List<Any>) {
|
||||||
|
Log.d("LongPollEventParser", "$eventType: $event")
|
||||||
|
|
||||||
|
val cmId = event[1].asLong()
|
||||||
|
val flags = event[2].asInt()
|
||||||
|
val peerId = event[3].asLong()
|
||||||
|
|
||||||
|
val eventsToSend = mutableListOf<LongPollParsedEvent>()
|
||||||
|
|
||||||
|
val parsedFlags = MessageFlags.parse(flags)
|
||||||
|
parsedFlags.forEach { flag ->
|
||||||
|
when (flag) {
|
||||||
|
MessageFlags.IMPORTANT -> {
|
||||||
|
val eventToSend = LongPollParsedEvent.MessageMarkedAsImportant(
|
||||||
|
peerId = peerId,
|
||||||
|
cmId = cmId,
|
||||||
|
marked = true
|
||||||
|
)
|
||||||
|
eventsToSend += eventToSend
|
||||||
|
dispatch(LongPollEvent.MARKED_AS_IMPORTANT, eventToSend)
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageFlags.SPAM -> {
|
||||||
|
val eventToSend = LongPollParsedEvent.MessageMarkedAsSpam(
|
||||||
|
peerId = peerId,
|
||||||
|
cmId = cmId
|
||||||
|
)
|
||||||
|
eventsToSend += eventToSend
|
||||||
|
dispatch(LongPollEvent.MARKED_AS_SPAM, eventToSend)
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageFlags.DELETED -> {
|
||||||
|
val eventToSend =
|
||||||
|
if (parsedFlags.contains(MessageFlags.DELETED_FOR_ALL)) {
|
||||||
|
LongPollParsedEvent.MessageDeleted(
|
||||||
|
peerId = peerId,
|
||||||
|
cmId = cmId,
|
||||||
|
forAll = true
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
LongPollParsedEvent.MessageDeleted(
|
||||||
|
peerId = peerId,
|
||||||
|
cmId = cmId,
|
||||||
|
forAll = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
eventsToSend += eventToSend
|
||||||
|
dispatch(LongPollEvent.MESSAGE_DELETED, eventToSend)
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageFlags.AUDIO_LISTENED -> {
|
||||||
|
val eventToSend = LongPollParsedEvent.AudioMessageListened(
|
||||||
|
peerId = peerId,
|
||||||
|
cmId = cmId
|
||||||
|
)
|
||||||
|
eventsToSend += eventToSend
|
||||||
|
dispatch(LongPollEvent.AUDIO_MESSAGE_LISTENED, eventToSend)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchAll(LongPollEvent.MESSAGE_SET_FLAGS, eventsToSend)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseMessageClearFlags(eventType: ApiEvent, event: List<Any>) {
|
||||||
|
Log.d("LongPollEventParser", "$eventType: $event")
|
||||||
|
|
||||||
|
val cmId = event[1].asLong()
|
||||||
|
val flags = event[2].asInt()
|
||||||
|
val peerId = event[3].asLong()
|
||||||
|
|
||||||
|
val eventsToSend = mutableListOf<LongPollParsedEvent>()
|
||||||
|
|
||||||
|
val parsedFlags = MessageFlags.parse(flags)
|
||||||
|
|
||||||
|
coroutineScope.launch {
|
||||||
|
parsedFlags.forEach { flag ->
|
||||||
|
when (flag) {
|
||||||
|
MessageFlags.IMPORTANT -> {
|
||||||
|
val eventToSend = LongPollParsedEvent.MessageMarkedAsImportant(
|
||||||
|
peerId = peerId,
|
||||||
|
cmId = cmId,
|
||||||
|
marked = false
|
||||||
|
)
|
||||||
|
eventsToSend += eventToSend
|
||||||
|
dispatch(LongPollEvent.MARKED_AS_IMPORTANT, eventToSend)
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageFlags.SPAM -> {
|
||||||
|
if (parsedFlags.contains(MessageFlags.CANCEL_SPAM)) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val message = loadMessage(
|
||||||
|
peerId = peerId,
|
||||||
|
cmId = cmId
|
||||||
|
)
|
||||||
|
message?.let {
|
||||||
|
val eventToSend =
|
||||||
|
LongPollParsedEvent.MessageMarkedAsNotSpam(message = message)
|
||||||
|
eventsToSend += eventToSend
|
||||||
|
dispatch(LongPollEvent.MARKED_AS_NOT_SPAM, eventToSend)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageFlags.DELETED -> {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val message = loadMessage(
|
||||||
|
peerId = peerId,
|
||||||
|
cmId = cmId
|
||||||
|
)
|
||||||
|
message?.let {
|
||||||
|
val eventToSend =
|
||||||
|
LongPollParsedEvent.MessageRestored(message = message)
|
||||||
|
eventsToSend += eventToSend
|
||||||
|
dispatch(LongPollEvent.MESSAGE_RESTORED, eventToSend)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchAll(LongPollEvent.MESSAGE_CLEAR_FLAGS, eventsToSend)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseMessageNew(eventType: ApiEvent, event: List<Any>) {
|
||||||
|
Log.d("LongPollEventParser", "$eventType: $event")
|
||||||
|
|
||||||
|
val cmId = event[1].asLong()
|
||||||
|
val peerId = event[4].asLong()
|
||||||
|
|
||||||
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
|
val message =
|
||||||
|
async { loadMessage(peerId = peerId, cmId = cmId) }.await()
|
||||||
|
|
||||||
|
val convo =
|
||||||
|
async {
|
||||||
|
loadConvo(
|
||||||
|
peerId = peerId,
|
||||||
|
extended = true,
|
||||||
|
fields = VkConstants.ALL_FIELDS
|
||||||
|
)
|
||||||
|
}.await()
|
||||||
|
|
||||||
|
message?.let {
|
||||||
|
dispatch(
|
||||||
|
LongPollEvent.MESSAGE_NEW,
|
||||||
|
LongPollParsedEvent.NewMessage(
|
||||||
|
message = message,
|
||||||
|
inArchive = convo?.isArchived == true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseMessageEdit(eventType: ApiEvent, event: List<Any>) {
|
||||||
|
Log.d("LongPollEventParser", "$eventType: $event")
|
||||||
|
|
||||||
|
val cmId = event[1].asLong()
|
||||||
|
val peerId = event[3].asLong()
|
||||||
|
|
||||||
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
|
loadMessage(
|
||||||
|
peerId = peerId,
|
||||||
|
cmId = cmId
|
||||||
|
)?.let { message ->
|
||||||
|
dispatch(LongPollEvent.MESSAGE_EDITED, LongPollParsedEvent.MessageEdited(message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseMessageReadIncoming(eventType: ApiEvent, event: List<Any>) {
|
||||||
|
Log.d("LongPollEventParser", "$eventType: $event")
|
||||||
|
val peerId = event[1].asLong()
|
||||||
|
val cmId = event[2].asLong()
|
||||||
|
val unreadCount = event[3].asInt()
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
LongPollEvent.INCOMING_MESSAGE_READ,
|
||||||
|
LongPollParsedEvent.IncomingMessageRead(
|
||||||
|
peerId = peerId,
|
||||||
|
cmId = cmId,
|
||||||
|
unreadCount = unreadCount
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseMessageReadOutgoing(eventType: ApiEvent, event: List<Any>) {
|
||||||
|
Log.d("LongPollEventParser", "$eventType: $event")
|
||||||
|
val peerId = event[1].asLong()
|
||||||
|
val cmId = event[2].asLong()
|
||||||
|
val unreadCount = event[3].asInt()
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
LongPollEvent.OUTGOING_MESSAGE_READ,
|
||||||
|
LongPollParsedEvent.OutgoingMessageRead(
|
||||||
|
peerId = peerId,
|
||||||
|
cmId = cmId,
|
||||||
|
unreadCount = unreadCount
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseChatClearFlags(eventType: ApiEvent, event: List<Any>) {
|
||||||
|
Log.d("LongPollEventParser", "$eventType: $event")
|
||||||
|
|
||||||
|
val peerId = event[1].asLong()
|
||||||
|
val flags = event[2].asInt()
|
||||||
|
|
||||||
|
val eventsToSend = mutableListOf<LongPollParsedEvent>()
|
||||||
|
|
||||||
|
val parsedFlags = ConvoFlags.parse(flags)
|
||||||
|
|
||||||
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
|
parsedFlags.forEach { flag ->
|
||||||
|
when (flag) {
|
||||||
|
ConvoFlags.ARCHIVED -> {
|
||||||
|
val convo = loadConvo(
|
||||||
|
peerId = peerId,
|
||||||
|
extended = true,
|
||||||
|
fields = VkConstants.ALL_FIELDS
|
||||||
|
) ?: return@forEach
|
||||||
|
|
||||||
|
val message = loadMessage(
|
||||||
|
peerId = peerId,
|
||||||
|
cmId = convo.lastCmId
|
||||||
|
)
|
||||||
|
|
||||||
|
val eventToSend = LongPollParsedEvent.ChatArchived(
|
||||||
|
convo = convo.copy(lastMessage = message),
|
||||||
|
archived = false
|
||||||
|
)
|
||||||
|
eventsToSend += eventToSend
|
||||||
|
dispatch(LongPollEvent.CHAT_ARCHIVED, eventToSend)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchAll(LongPollEvent.CHAT_CLEAR_FLAGS, eventsToSend)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseChatSetFlags(eventType: ApiEvent, event: List<Any>) {
|
||||||
|
Log.d("LongPollEventParser", "$eventType: $event")
|
||||||
|
|
||||||
|
val peerId = event[1].asLong()
|
||||||
|
val flags = event[2].asInt()
|
||||||
|
|
||||||
|
val eventsToSend = mutableListOf<LongPollParsedEvent>()
|
||||||
|
|
||||||
|
val parsedFlags = ConvoFlags.parse(flags)
|
||||||
|
|
||||||
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
|
parsedFlags.forEach { flag ->
|
||||||
|
when (flag) {
|
||||||
|
ConvoFlags.ARCHIVED -> {
|
||||||
|
val convo = loadConvo(
|
||||||
|
peerId = peerId,
|
||||||
|
extended = true,
|
||||||
|
fields = VkConstants.ALL_FIELDS
|
||||||
|
) ?: return@forEach
|
||||||
|
|
||||||
|
val message = loadMessage(
|
||||||
|
peerId = peerId,
|
||||||
|
cmId = convo.lastCmId
|
||||||
|
)
|
||||||
|
|
||||||
|
val eventToSend = LongPollParsedEvent.ChatArchived(
|
||||||
|
convo = convo.copy(lastMessage = message),
|
||||||
|
archived = true
|
||||||
|
)
|
||||||
|
eventsToSend += eventToSend
|
||||||
|
dispatch(LongPollEvent.CHAT_ARCHIVED, eventToSend)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchAll(LongPollEvent.CHAT_SET_FLAGS, eventsToSend)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseMessagesDeleted(eventType: ApiEvent, event: List<Any>) {
|
||||||
|
Log.d("LongPollEventParser", "$eventType: $event")
|
||||||
|
|
||||||
|
val peerId = event[1].asLong()
|
||||||
|
val cmId = event[2].asLong()
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
LongPollEvent.CHAT_CLEARED,
|
||||||
|
LongPollParsedEvent.ChatCleared(
|
||||||
|
peerId = peerId,
|
||||||
|
toCmId = cmId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseChatMajorChanged(eventType: ApiEvent, event: List<Any>) {
|
||||||
|
Log.d("LongPollEventParser", "$eventType: $event")
|
||||||
|
|
||||||
|
val peerId = event[1].asLong()
|
||||||
|
val majorId = event[2].asInt()
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
LongPollEvent.CHAT_MAJOR_CHANGED,
|
||||||
|
LongPollParsedEvent.ChatMajorChanged(
|
||||||
|
peerId = peerId,
|
||||||
|
majorId = majorId,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseChatMinorChanged(eventType: ApiEvent, event: List<Any>) {
|
||||||
|
Log.d("LongPollEventParser", "$eventType: $event")
|
||||||
|
|
||||||
|
val peerId = event[1].asLong()
|
||||||
|
val minorId = event[2].asInt()
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
LongPollEvent.CHAT_MINOR_CHANGED,
|
||||||
|
LongPollParsedEvent.ChatMinorChanged(
|
||||||
|
peerId = peerId,
|
||||||
|
minorId = minorId,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseInteraction(eventType: ApiEvent, event: List<Any>) {
|
||||||
|
Log.d("LongPollEventParser", "$eventType: $event")
|
||||||
|
|
||||||
|
val interactionType = when (eventType) {
|
||||||
|
ApiEvent.TYPING -> InteractionType.Typing
|
||||||
|
ApiEvent.AUDIO_MESSAGE_RECORDING -> InteractionType.VoiceMessage
|
||||||
|
ApiEvent.PHOTO_UPLOADING -> InteractionType.Photo
|
||||||
|
ApiEvent.VIDEO_UPLOADING -> InteractionType.Video
|
||||||
|
ApiEvent.FILE_UPLOADING -> InteractionType.File
|
||||||
|
else -> return
|
||||||
|
}
|
||||||
|
|
||||||
|
val longPollEvent: LongPollEvent = when (eventType) {
|
||||||
|
ApiEvent.TYPING -> LongPollEvent.TYPING
|
||||||
|
ApiEvent.AUDIO_MESSAGE_RECORDING -> LongPollEvent.AUDIO_MESSAGE_RECORDING
|
||||||
|
ApiEvent.PHOTO_UPLOADING -> LongPollEvent.PHOTO_UPLOADING
|
||||||
|
ApiEvent.VIDEO_UPLOADING -> LongPollEvent.VIDEO_UPLOADING
|
||||||
|
ApiEvent.FILE_UPLOADING -> LongPollEvent.FILE_UPLOADING
|
||||||
|
else -> return
|
||||||
|
}
|
||||||
|
|
||||||
|
val peerId = event[1].asLong()
|
||||||
|
val userIds = event[2].toList(Any::asLong).filter { it != UserConfig.userId }
|
||||||
|
val totalCount = event[3].asInt()
|
||||||
|
val timestamp = event[4].asInt()
|
||||||
|
|
||||||
|
if (userIds.isEmpty()) return
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
longPollEvent,
|
||||||
|
LongPollParsedEvent.Interaction(
|
||||||
|
interactionType = interactionType,
|
||||||
|
peerId = peerId,
|
||||||
|
userIds = userIds,
|
||||||
|
totalCount = totalCount,
|
||||||
|
timestamp = timestamp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseUnreadCounterUpdate(eventType: ApiEvent, event: List<Any>) {
|
||||||
|
Log.d("LongPollEventParser", "$eventType $event")
|
||||||
|
|
||||||
|
val unreadCount = event[1].asInt()
|
||||||
|
val unreadUnmutedCount = event[2].asInt()
|
||||||
|
val showOnlyMuted = event[3].asInt() == 1
|
||||||
|
val businessNotifyUnreadCount = event[4].asInt()
|
||||||
|
val archiveUnreadCount = event[7].asInt()
|
||||||
|
val archiveUnreadUnmutedCount = event[8].asInt()
|
||||||
|
val archiveMentionsCount = event[9].asInt()
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
LongPollEvent.UNREAD_COUNTER_UPDATE,
|
||||||
|
LongPollParsedEvent.UnreadCounter(
|
||||||
|
unread = unreadCount,
|
||||||
|
unreadUnmuted = unreadUnmutedCount,
|
||||||
|
showOnlyMuted = showOnlyMuted,
|
||||||
|
business = businessNotifyUnreadCount,
|
||||||
|
archive = archiveUnreadCount,
|
||||||
|
archiveUnmuted = archiveUnreadUnmutedCount,
|
||||||
|
archiveMentions = archiveMentionsCount
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseMessageUpdated(eventType: ApiEvent, event: List<Any>) {
|
||||||
|
Log.d("LongPollEventParser", "$eventType $event")
|
||||||
|
|
||||||
|
val cmId = event[1].asLong()
|
||||||
|
val peerId = event[4].asLong()
|
||||||
|
|
||||||
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
|
loadMessage(
|
||||||
|
peerId = peerId,
|
||||||
|
cmId = cmId
|
||||||
|
)?.let { message ->
|
||||||
|
dispatch(LongPollEvent.MESSAGE_UPDATED, LongPollParsedEvent.MessageUpdated(message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseMessageCacheClear(eventType: ApiEvent, event: List<Any>) {
|
||||||
|
Log.d("LongPollEventParser", "$eventType $event")
|
||||||
|
|
||||||
|
val messageId = event[1].asLong()
|
||||||
|
|
||||||
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
|
loadMessage(messageId = messageId)?.let { message ->
|
||||||
|
dispatch(
|
||||||
|
LongPollEvent.MESSAGE_CACHE_CLEAR,
|
||||||
|
LongPollParsedEvent.MessageCacheClear(message)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadMessage(
|
||||||
|
peerId: Long? = null,
|
||||||
|
cmId: Long? = null,
|
||||||
|
messageId: Long? = null
|
||||||
|
): VkMessage? = suspendCoroutine { continuation ->
|
||||||
|
require((peerId != null && cmId != null) || messageId != null)
|
||||||
|
|
||||||
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
|
messagesUseCase.getById(
|
||||||
|
peerCmIds = null,
|
||||||
|
peerId = peerId,
|
||||||
|
messageIds = messageId?.let(::listOf),
|
||||||
|
cmIds = cmId?.let(::listOf),
|
||||||
|
extended = true,
|
||||||
|
fields = VkConstants.ALL_FIELDS
|
||||||
|
).listenValue(this) { state ->
|
||||||
|
state.processState(
|
||||||
|
error = { error ->
|
||||||
|
Log.e("LongPollEventParser", "loadMessage: error: $error")
|
||||||
|
continuation.resume(null)
|
||||||
|
},
|
||||||
|
success = { response ->
|
||||||
|
val message = response.singleOrNull() ?: run {
|
||||||
|
continuation.resume(null)
|
||||||
|
return@listenValue
|
||||||
|
}
|
||||||
|
|
||||||
|
continuation.resume(message)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadConvo(
|
||||||
|
peerId: Long,
|
||||||
|
extended: Boolean = false,
|
||||||
|
fields: String? = null
|
||||||
|
): VkConvo? = suspendCoroutine { continuation ->
|
||||||
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
|
convoUseCase.getById(
|
||||||
|
peerIds = listOf(peerId),
|
||||||
|
extended = extended,
|
||||||
|
fields = fields
|
||||||
|
).listenValue(coroutineScope) { state ->
|
||||||
|
state.processState(
|
||||||
|
error = { error ->
|
||||||
|
Log.e("LongPollEventParser", "loadConvo: error: $error")
|
||||||
|
continuation.resume(null)
|
||||||
|
},
|
||||||
|
success = { response ->
|
||||||
|
val convo = response.singleOrNull() ?: run {
|
||||||
|
continuation.resume(null)
|
||||||
|
return@listenValue
|
||||||
|
}
|
||||||
|
|
||||||
|
continuation.resume(convo)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,35 +1,17 @@
|
|||||||
package dev.meloda.fast.domain
|
package dev.meloda.fast.domain
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import dev.meloda.fast.common.VkConstants
|
|
||||||
import dev.meloda.fast.common.extensions.asInt
|
|
||||||
import dev.meloda.fast.common.extensions.asLong
|
|
||||||
import dev.meloda.fast.common.extensions.listenValue
|
|
||||||
import dev.meloda.fast.common.extensions.toList
|
|
||||||
import dev.meloda.fast.data.UserConfig
|
|
||||||
import dev.meloda.fast.data.processState
|
|
||||||
import dev.meloda.fast.model.ApiEvent
|
|
||||||
import dev.meloda.fast.model.ConvoFlags
|
|
||||||
import dev.meloda.fast.model.InteractionType
|
|
||||||
import dev.meloda.fast.model.LongPollEvent
|
import dev.meloda.fast.model.LongPollEvent
|
||||||
import dev.meloda.fast.model.LongPollParsedEvent
|
import dev.meloda.fast.model.LongPollParsedEvent
|
||||||
import dev.meloda.fast.model.MessageFlags
|
|
||||||
import dev.meloda.fast.model.api.domain.VkConvo
|
|
||||||
import dev.meloda.fast.model.api.domain.VkMessage
|
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.suspendCoroutine
|
|
||||||
|
|
||||||
class LongPollUpdatesParser(
|
class LongPollUpdatesParser(
|
||||||
private val convoUseCase: ConvoUseCase,
|
convoUseCase: ConvoUseCase,
|
||||||
private val messagesUseCase: MessagesUseCase
|
messagesUseCase: MessagesUseCase
|
||||||
) {
|
) {
|
||||||
private val job = SupervisorJob()
|
private val job = SupervisorJob()
|
||||||
|
|
||||||
@@ -44,534 +26,16 @@ class LongPollUpdatesParser(
|
|||||||
|
|
||||||
private val coroutineScope = CoroutineScope(coroutineContext)
|
private val coroutineScope = CoroutineScope(coroutineContext)
|
||||||
private val eventDispatcher = LongPollEventDispatcher()
|
private val eventDispatcher = LongPollEventDispatcher()
|
||||||
|
private val eventParser = LongPollEventParser(
|
||||||
|
coroutineScope = coroutineScope,
|
||||||
|
convoUseCase = convoUseCase,
|
||||||
|
messagesUseCase = messagesUseCase,
|
||||||
|
dispatch = eventDispatcher::dispatch,
|
||||||
|
dispatchAll = eventDispatcher::dispatchAll
|
||||||
|
)
|
||||||
|
|
||||||
fun parseNextUpdate(event: List<Any>) {
|
fun parseNextUpdate(event: List<Any>) {
|
||||||
val eventId = event.first().asInt()
|
eventParser.parseNextUpdate(event)
|
||||||
|
|
||||||
when (val eventType = ApiEvent.parseOrNull(eventId)) {
|
|
||||||
null -> Log.d("LongPollUpdatesParser", "parseNextUpdate: unknownEvent: $event")
|
|
||||||
|
|
||||||
ApiEvent.MESSAGE_SET_FLAGS -> parseMessageSetFlags(eventType, event)
|
|
||||||
ApiEvent.MESSAGE_CLEAR_FLAGS -> parseMessageClearFlags(eventType, event)
|
|
||||||
ApiEvent.MESSAGE_NEW -> parseMessageNew(eventType, event)
|
|
||||||
ApiEvent.MESSAGE_EDIT -> parseMessageEdit(eventType, event)
|
|
||||||
ApiEvent.MESSAGE_READ_INCOMING -> parseMessageReadIncoming(eventType, event)
|
|
||||||
ApiEvent.MESSAGE_READ_OUTGOING -> parseMessageReadOutgoing(eventType, event)
|
|
||||||
ApiEvent.CHAT_CLEAR_FLAGS -> parseChatClearFlags(eventType, event)
|
|
||||||
ApiEvent.CHAT_SET_FLAGS -> parseChatSetFlags(eventType, event)
|
|
||||||
ApiEvent.MESSAGES_DELETED -> parseMessagesDeleted(eventType, event)
|
|
||||||
ApiEvent.CHAT_MAJOR_CHANGED -> parseChatMajorChanged(eventType, event)
|
|
||||||
ApiEvent.CHAT_MINOR_CHANGED -> parseChatMinorChanged(eventType, event)
|
|
||||||
|
|
||||||
ApiEvent.TYPING,
|
|
||||||
ApiEvent.AUDIO_MESSAGE_RECORDING,
|
|
||||||
ApiEvent.PHOTO_UPLOADING,
|
|
||||||
ApiEvent.VIDEO_UPLOADING,
|
|
||||||
ApiEvent.FILE_UPLOADING -> parseInteraction(eventType, event)
|
|
||||||
|
|
||||||
ApiEvent.UNREAD_COUNT_UPDATE -> parseUnreadCounterUpdate(eventType, event)
|
|
||||||
ApiEvent.MESSAGE_UPDATED -> parseMessageUpdated(eventType, event)
|
|
||||||
ApiEvent.MESSAGE_CACHE_CLEAR -> parseMessageCacheClear(eventType, event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseMessageSetFlags(eventType: ApiEvent, event: List<Any>) {
|
|
||||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
|
||||||
|
|
||||||
val cmId = event[1].asLong()
|
|
||||||
val flags = event[2].asInt()
|
|
||||||
val peerId = event[3].asLong()
|
|
||||||
|
|
||||||
val eventsToSend = mutableListOf<LongPollParsedEvent>()
|
|
||||||
|
|
||||||
val parsedFlags = MessageFlags.parse(flags)
|
|
||||||
parsedFlags.forEach { flag ->
|
|
||||||
when (flag) {
|
|
||||||
MessageFlags.IMPORTANT -> {
|
|
||||||
val eventToSend = LongPollParsedEvent.MessageMarkedAsImportant(
|
|
||||||
peerId = peerId,
|
|
||||||
cmId = cmId,
|
|
||||||
marked = true
|
|
||||||
)
|
|
||||||
eventsToSend += eventToSend
|
|
||||||
eventDispatcher.dispatch(LongPollEvent.MARKED_AS_IMPORTANT, eventToSend)
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageFlags.SPAM -> {
|
|
||||||
val eventToSend = LongPollParsedEvent.MessageMarkedAsSpam(
|
|
||||||
peerId = peerId,
|
|
||||||
cmId = cmId
|
|
||||||
)
|
|
||||||
eventsToSend += eventToSend
|
|
||||||
eventDispatcher.dispatch(LongPollEvent.MARKED_AS_SPAM, eventToSend)
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageFlags.DELETED -> {
|
|
||||||
val eventToSend =
|
|
||||||
if (parsedFlags.contains(MessageFlags.DELETED_FOR_ALL)) {
|
|
||||||
LongPollParsedEvent.MessageDeleted(
|
|
||||||
peerId = peerId,
|
|
||||||
cmId = cmId,
|
|
||||||
forAll = true
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
LongPollParsedEvent.MessageDeleted(
|
|
||||||
peerId = peerId,
|
|
||||||
cmId = cmId,
|
|
||||||
forAll = false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
eventsToSend += eventToSend
|
|
||||||
eventDispatcher.dispatch(LongPollEvent.MESSAGE_DELETED, eventToSend)
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageFlags.AUDIO_LISTENED -> {
|
|
||||||
val eventToSend = LongPollParsedEvent.AudioMessageListened(
|
|
||||||
peerId = peerId,
|
|
||||||
cmId = cmId
|
|
||||||
)
|
|
||||||
eventsToSend += eventToSend
|
|
||||||
eventDispatcher.dispatch(LongPollEvent.AUDIO_MESSAGE_LISTENED, eventToSend)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
eventDispatcher.dispatchAll(LongPollEvent.MESSAGE_SET_FLAGS, eventsToSend)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseMessageClearFlags(eventType: ApiEvent, event: List<Any>) {
|
|
||||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
|
||||||
|
|
||||||
val cmId = event[1].asLong()
|
|
||||||
val flags = event[2].asInt()
|
|
||||||
val peerId = event[3].asLong()
|
|
||||||
|
|
||||||
val eventsToSend = mutableListOf<LongPollParsedEvent>()
|
|
||||||
|
|
||||||
val parsedFlags = MessageFlags.parse(flags)
|
|
||||||
|
|
||||||
coroutineScope.launch {
|
|
||||||
parsedFlags.forEach { flag ->
|
|
||||||
when (flag) {
|
|
||||||
MessageFlags.IMPORTANT -> {
|
|
||||||
val eventToSend = LongPollParsedEvent.MessageMarkedAsImportant(
|
|
||||||
peerId = peerId,
|
|
||||||
cmId = cmId,
|
|
||||||
marked = false
|
|
||||||
)
|
|
||||||
eventsToSend += eventToSend
|
|
||||||
eventDispatcher.dispatch(LongPollEvent.MARKED_AS_IMPORTANT, eventToSend)
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageFlags.SPAM -> {
|
|
||||||
if (parsedFlags.contains(MessageFlags.CANCEL_SPAM)) {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
val message = loadMessage(
|
|
||||||
peerId = peerId,
|
|
||||||
cmId = cmId
|
|
||||||
)
|
|
||||||
message?.let {
|
|
||||||
val eventToSend =
|
|
||||||
LongPollParsedEvent.MessageMarkedAsNotSpam(message = message)
|
|
||||||
eventsToSend += eventToSend
|
|
||||||
eventDispatcher.dispatch(LongPollEvent.MARKED_AS_NOT_SPAM, eventToSend)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageFlags.DELETED -> {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
val message = loadMessage(
|
|
||||||
peerId = peerId,
|
|
||||||
cmId = cmId
|
|
||||||
)
|
|
||||||
message?.let {
|
|
||||||
val eventToSend =
|
|
||||||
LongPollParsedEvent.MessageRestored(message = message)
|
|
||||||
eventsToSend += eventToSend
|
|
||||||
eventDispatcher.dispatch(LongPollEvent.MESSAGE_RESTORED, eventToSend)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
eventDispatcher.dispatchAll(LongPollEvent.MESSAGE_CLEAR_FLAGS, eventsToSend)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseMessageNew(eventType: ApiEvent, event: List<Any>) {
|
|
||||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
|
||||||
|
|
||||||
val cmId = event[1].asLong()
|
|
||||||
val peerId = event[4].asLong()
|
|
||||||
|
|
||||||
coroutineScope.launch(Dispatchers.IO) {
|
|
||||||
val message =
|
|
||||||
async { loadMessage(peerId = peerId, cmId = cmId) }.await()
|
|
||||||
|
|
||||||
val convo =
|
|
||||||
async {
|
|
||||||
loadConvo(
|
|
||||||
peerId = peerId,
|
|
||||||
extended = true,
|
|
||||||
fields = VkConstants.ALL_FIELDS
|
|
||||||
)
|
|
||||||
}.await()
|
|
||||||
|
|
||||||
message?.let {
|
|
||||||
eventDispatcher.dispatch(
|
|
||||||
LongPollEvent.MESSAGE_NEW,
|
|
||||||
LongPollParsedEvent.NewMessage(
|
|
||||||
message = message,
|
|
||||||
inArchive = convo?.isArchived == true
|
|
||||||
// TODO: 03-Apr-25, Danil Nikolaev:
|
|
||||||
// load user settings about restoring chats with
|
|
||||||
// enabled notifications from archive
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseMessageEdit(eventType: ApiEvent, event: List<Any>) {
|
|
||||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
|
||||||
|
|
||||||
val cmId = event[1].asLong()
|
|
||||||
val peerId = event[3].asLong()
|
|
||||||
|
|
||||||
coroutineScope.launch(Dispatchers.IO) {
|
|
||||||
loadMessage(
|
|
||||||
peerId = peerId,
|
|
||||||
cmId = cmId
|
|
||||||
)?.let { message ->
|
|
||||||
eventDispatcher.dispatch(LongPollEvent.MESSAGE_EDITED, LongPollParsedEvent.MessageEdited(message))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseMessageReadIncoming(eventType: ApiEvent, event: List<Any>) {
|
|
||||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
|
||||||
val peerId = event[1].asLong()
|
|
||||||
val cmId = event[2].asLong()
|
|
||||||
val unreadCount = event[3].asInt()
|
|
||||||
|
|
||||||
eventDispatcher.dispatch(
|
|
||||||
LongPollEvent.INCOMING_MESSAGE_READ,
|
|
||||||
LongPollParsedEvent.IncomingMessageRead(
|
|
||||||
peerId = peerId,
|
|
||||||
cmId = cmId,
|
|
||||||
unreadCount = unreadCount
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseMessageReadOutgoing(eventType: ApiEvent, event: List<Any>) {
|
|
||||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
|
||||||
val peerId = event[1].asLong()
|
|
||||||
val cmId = event[2].asLong()
|
|
||||||
val unreadCount = event[3].asInt()
|
|
||||||
|
|
||||||
eventDispatcher.dispatch(
|
|
||||||
LongPollEvent.OUTGOING_MESSAGE_READ,
|
|
||||||
LongPollParsedEvent.OutgoingMessageRead(
|
|
||||||
peerId = peerId,
|
|
||||||
cmId = cmId,
|
|
||||||
unreadCount = unreadCount
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseChatClearFlags(eventType: ApiEvent, event: List<Any>) {
|
|
||||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
|
||||||
|
|
||||||
val peerId = event[1].asLong()
|
|
||||||
val flags = event[2].asInt()
|
|
||||||
|
|
||||||
val eventsToSend = mutableListOf<LongPollParsedEvent>()
|
|
||||||
|
|
||||||
val parsedFlags = ConvoFlags.parse(flags)
|
|
||||||
|
|
||||||
coroutineScope.launch(Dispatchers.IO) {
|
|
||||||
parsedFlags.forEach { flag ->
|
|
||||||
when (flag) {
|
|
||||||
ConvoFlags.ARCHIVED -> {
|
|
||||||
val convo = loadConvo(
|
|
||||||
peerId = peerId,
|
|
||||||
extended = true,
|
|
||||||
fields = VkConstants.ALL_FIELDS
|
|
||||||
) ?: return@forEach
|
|
||||||
|
|
||||||
val message = loadMessage(
|
|
||||||
peerId = peerId,
|
|
||||||
cmId = convo.lastCmId
|
|
||||||
)
|
|
||||||
|
|
||||||
val eventToSend = LongPollParsedEvent.ChatArchived(
|
|
||||||
convo = convo.copy(lastMessage = message),
|
|
||||||
archived = false
|
|
||||||
)
|
|
||||||
eventsToSend += eventToSend
|
|
||||||
eventDispatcher.dispatch(LongPollEvent.CHAT_ARCHIVED, eventToSend)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
eventDispatcher.dispatchAll(LongPollEvent.CHAT_CLEAR_FLAGS, eventsToSend)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseChatSetFlags(eventType: ApiEvent, event: List<Any>) {
|
|
||||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
|
||||||
|
|
||||||
val peerId = event[1].asLong()
|
|
||||||
val flags = event[2].asInt()
|
|
||||||
|
|
||||||
val eventsToSend = mutableListOf<LongPollParsedEvent>()
|
|
||||||
|
|
||||||
val parsedFlags = ConvoFlags.parse(flags)
|
|
||||||
|
|
||||||
coroutineScope.launch(Dispatchers.IO) {
|
|
||||||
parsedFlags.forEach { flag ->
|
|
||||||
when (flag) {
|
|
||||||
ConvoFlags.ARCHIVED -> {
|
|
||||||
val convo = loadConvo(
|
|
||||||
peerId = peerId,
|
|
||||||
extended = true,
|
|
||||||
fields = VkConstants.ALL_FIELDS
|
|
||||||
) ?: return@forEach
|
|
||||||
|
|
||||||
val message = loadMessage(
|
|
||||||
peerId = peerId,
|
|
||||||
cmId = convo.lastCmId
|
|
||||||
)
|
|
||||||
|
|
||||||
val eventToSend = LongPollParsedEvent.ChatArchived(
|
|
||||||
convo = convo.copy(lastMessage = message),
|
|
||||||
archived = true
|
|
||||||
)
|
|
||||||
eventsToSend += eventToSend
|
|
||||||
eventDispatcher.dispatch(LongPollEvent.CHAT_ARCHIVED, eventToSend)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
eventDispatcher.dispatchAll(LongPollEvent.CHAT_SET_FLAGS, eventsToSend)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseMessagesDeleted(eventType: ApiEvent, event: List<Any>) {
|
|
||||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
|
||||||
|
|
||||||
val peerId = event[1].asLong()
|
|
||||||
val cmId = event[2].asLong()
|
|
||||||
|
|
||||||
eventDispatcher.dispatch(
|
|
||||||
LongPollEvent.CHAT_CLEARED,
|
|
||||||
LongPollParsedEvent.ChatCleared(
|
|
||||||
peerId = peerId,
|
|
||||||
toCmId = cmId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseChatMajorChanged(eventType: ApiEvent, event: List<Any>) {
|
|
||||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
|
||||||
|
|
||||||
val peerId = event[1].asLong()
|
|
||||||
val majorId = event[2].asInt()
|
|
||||||
|
|
||||||
eventDispatcher.dispatch(
|
|
||||||
LongPollEvent.CHAT_MAJOR_CHANGED,
|
|
||||||
LongPollParsedEvent.ChatMajorChanged(
|
|
||||||
peerId = peerId,
|
|
||||||
majorId = majorId,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseChatMinorChanged(eventType: ApiEvent, event: List<Any>) {
|
|
||||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
|
||||||
|
|
||||||
val peerId = event[1].asLong()
|
|
||||||
val minorId = event[2].asInt()
|
|
||||||
|
|
||||||
eventDispatcher.dispatch(
|
|
||||||
LongPollEvent.CHAT_MINOR_CHANGED,
|
|
||||||
LongPollParsedEvent.ChatMinorChanged(
|
|
||||||
peerId = peerId,
|
|
||||||
minorId = minorId,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseInteraction(eventType: ApiEvent, event: List<Any>) {
|
|
||||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
|
||||||
|
|
||||||
val interactionType = when (eventType) {
|
|
||||||
ApiEvent.TYPING -> InteractionType.Typing
|
|
||||||
ApiEvent.AUDIO_MESSAGE_RECORDING -> InteractionType.VoiceMessage
|
|
||||||
ApiEvent.PHOTO_UPLOADING -> InteractionType.Photo
|
|
||||||
ApiEvent.VIDEO_UPLOADING -> InteractionType.Video
|
|
||||||
ApiEvent.FILE_UPLOADING -> InteractionType.File
|
|
||||||
else -> return
|
|
||||||
}
|
|
||||||
|
|
||||||
val longPollEvent: LongPollEvent = when (eventType) {
|
|
||||||
ApiEvent.TYPING -> LongPollEvent.TYPING
|
|
||||||
ApiEvent.AUDIO_MESSAGE_RECORDING -> LongPollEvent.AUDIO_MESSAGE_RECORDING
|
|
||||||
ApiEvent.PHOTO_UPLOADING -> LongPollEvent.PHOTO_UPLOADING
|
|
||||||
ApiEvent.VIDEO_UPLOADING -> LongPollEvent.VIDEO_UPLOADING
|
|
||||||
ApiEvent.FILE_UPLOADING -> LongPollEvent.FILE_UPLOADING
|
|
||||||
else -> return
|
|
||||||
}
|
|
||||||
|
|
||||||
val peerId = event[1].asLong()
|
|
||||||
val userIds = event[2].toList(Any::asLong).filter { it != UserConfig.userId }
|
|
||||||
val totalCount = event[3].asInt()
|
|
||||||
val timestamp = event[4].asInt()
|
|
||||||
|
|
||||||
// if userIds contains only account's id, then we don't need to show our status
|
|
||||||
if (userIds.isEmpty()) return
|
|
||||||
|
|
||||||
eventDispatcher.dispatch(
|
|
||||||
longPollEvent,
|
|
||||||
LongPollParsedEvent.Interaction(
|
|
||||||
interactionType = interactionType,
|
|
||||||
peerId = peerId,
|
|
||||||
userIds = userIds,
|
|
||||||
totalCount = totalCount,
|
|
||||||
timestamp = timestamp
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseUnreadCounterUpdate(eventType: ApiEvent, event: List<Any>) {
|
|
||||||
Log.d("LongPollUpdatesParser", "$eventType $event")
|
|
||||||
|
|
||||||
val unreadCount = event[1].asInt()
|
|
||||||
val unreadUnmutedCount = event[2].asInt()
|
|
||||||
val showOnlyMuted = event[3].asInt() == 1
|
|
||||||
val businessNotifyUnreadCount = event[4].asInt()
|
|
||||||
val archiveUnreadCount = event[7].asInt()
|
|
||||||
val archiveUnreadUnmutedCount = event[8].asInt()
|
|
||||||
val archiveMentionsCount = event[9].asInt()
|
|
||||||
|
|
||||||
eventDispatcher.dispatch(
|
|
||||||
LongPollEvent.UNREAD_COUNTER_UPDATE,
|
|
||||||
LongPollParsedEvent.UnreadCounter(
|
|
||||||
unread = unreadCount,
|
|
||||||
unreadUnmuted = unreadUnmutedCount,
|
|
||||||
showOnlyMuted = showOnlyMuted,
|
|
||||||
business = businessNotifyUnreadCount,
|
|
||||||
archive = archiveUnreadCount,
|
|
||||||
archiveUnmuted = archiveUnreadUnmutedCount,
|
|
||||||
archiveMentions = archiveMentionsCount
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseMessageUpdated(eventType: ApiEvent, event: List<Any>) {
|
|
||||||
Log.d("LongPollUpdatesParser", "$eventType $event")
|
|
||||||
|
|
||||||
val cmId = event[1].asLong()
|
|
||||||
val peerId = event[4].asLong()
|
|
||||||
|
|
||||||
coroutineScope.launch(Dispatchers.IO) {
|
|
||||||
loadMessage(
|
|
||||||
peerId = peerId,
|
|
||||||
cmId = cmId
|
|
||||||
)?.let { message ->
|
|
||||||
eventDispatcher.dispatch(LongPollEvent.MESSAGE_UPDATED, LongPollParsedEvent.MessageUpdated(message))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseMessageCacheClear(eventType: ApiEvent, event: List<Any>) {
|
|
||||||
Log.d("LongPollUpdatesParser", "$eventType $event")
|
|
||||||
|
|
||||||
val messageId = event[1].asLong()
|
|
||||||
|
|
||||||
coroutineScope.launch(Dispatchers.IO) {
|
|
||||||
loadMessage(messageId = messageId)?.let { message ->
|
|
||||||
eventDispatcher.dispatch(
|
|
||||||
LongPollEvent.MESSAGE_CACHE_CLEAR,
|
|
||||||
LongPollParsedEvent.MessageCacheClear(message)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun loadMessage(
|
|
||||||
peerId: Long? = null,
|
|
||||||
cmId: Long? = null,
|
|
||||||
messageId: Long? = null
|
|
||||||
): VkMessage? = suspendCoroutine { continuation ->
|
|
||||||
require((peerId != null && cmId != null) || messageId != null)
|
|
||||||
|
|
||||||
coroutineScope.launch(Dispatchers.IO) {
|
|
||||||
messagesUseCase.getById(
|
|
||||||
peerCmIds = null,
|
|
||||||
peerId = peerId,
|
|
||||||
messageIds = messageId?.let(::listOf),
|
|
||||||
cmIds = cmId?.let(::listOf),
|
|
||||||
extended = true,
|
|
||||||
fields = VkConstants.ALL_FIELDS
|
|
||||||
).listenValue(this) { state ->
|
|
||||||
state.processState(
|
|
||||||
error = { error ->
|
|
||||||
Log.e("LongPollUpdatesParser", "loadMessage: error: $error")
|
|
||||||
continuation.resume(null)
|
|
||||||
},
|
|
||||||
success = { response ->
|
|
||||||
val message = response.singleOrNull() ?: run {
|
|
||||||
continuation.resume(null)
|
|
||||||
return@listenValue
|
|
||||||
}
|
|
||||||
|
|
||||||
continuation.resume(message)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun loadConvo(
|
|
||||||
peerId: Long,
|
|
||||||
extended: Boolean = false,
|
|
||||||
fields: String? = null
|
|
||||||
): VkConvo? = suspendCoroutine { continuation ->
|
|
||||||
coroutineScope.launch(Dispatchers.IO) {
|
|
||||||
convoUseCase.getById(
|
|
||||||
peerIds = listOf(peerId),
|
|
||||||
extended = extended,
|
|
||||||
fields = fields
|
|
||||||
).listenValue(coroutineScope) { state ->
|
|
||||||
state.processState(
|
|
||||||
error = { error ->
|
|
||||||
Log.e("LongPollUpdatesParser", "loadConvo: error: $error")
|
|
||||||
continuation.resume(null)
|
|
||||||
},
|
|
||||||
success = { response ->
|
|
||||||
val convo = response.singleOrNull() ?: run {
|
|
||||||
continuation.resume(null)
|
|
||||||
return@listenValue
|
|
||||||
}
|
|
||||||
|
|
||||||
continuation.resume(convo)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onMessageSetFlags(block: (LongPollParsedEvent) -> Unit) {
|
fun onMessageSetFlags(block: (LongPollParsedEvent) -> Unit) {
|
||||||
|
|||||||
+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
|
package dev.meloda.fast.messageshistory
|
||||||
|
|
||||||
import android.content.ClipData
|
|
||||||
import android.content.ClipboardManager
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
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.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 androidx.core.content.FileProvider
|
|
||||||
import androidx.core.graphics.drawable.toBitmapOrNull
|
|
||||||
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 coil.imageLoader
|
|
||||||
import coil.request.ImageRequest
|
|
||||||
import com.conena.nanokt.collections.indexOfFirstOrNull
|
import com.conena.nanokt.collections.indexOfFirstOrNull
|
||||||
import dev.meloda.fast.common.VkConstants
|
import dev.meloda.fast.common.VkConstants
|
||||||
import dev.meloda.fast.common.extensions.getParcelableCompat
|
import dev.meloda.fast.common.extensions.getParcelableCompat
|
||||||
import dev.meloda.fast.common.extensions.listenValue
|
import dev.meloda.fast.common.extensions.listenValue
|
||||||
import dev.meloda.fast.common.extensions.orDots
|
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.extensions.setValue
|
||||||
import dev.meloda.fast.common.provider.ResourceProvider
|
import dev.meloda.fast.common.provider.ResourceProvider
|
||||||
import dev.meloda.fast.common.paging.canPaginate as canPaginatePage
|
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.loadingFlags
|
||||||
import dev.meloda.fast.common.paging.mergePage
|
import dev.meloda.fast.common.paging.mergePage
|
||||||
import dev.meloda.fast.data.State
|
import dev.meloda.fast.data.State
|
||||||
import dev.meloda.fast.data.UserConfig
|
|
||||||
import dev.meloda.fast.data.VkUtils
|
import dev.meloda.fast.data.VkUtils
|
||||||
import dev.meloda.fast.data.VkMemoryCache
|
import dev.meloda.fast.data.VkMemoryCache
|
||||||
import dev.meloda.fast.data.processState
|
import dev.meloda.fast.data.processState
|
||||||
import dev.meloda.fast.datastore.AppSettings
|
import dev.meloda.fast.datastore.AppSettings
|
||||||
import dev.meloda.fast.datastore.UserSettings
|
|
||||||
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.LoadConvosByIdUseCase
|
import dev.meloda.fast.domain.LoadConvosByIdUseCase
|
||||||
import dev.meloda.fast.domain.LongPollUpdatesParser
|
import dev.meloda.fast.domain.LongPollUpdatesParser
|
||||||
import dev.meloda.fast.domain.MessagesUseCase
|
import dev.meloda.fast.domain.MessagesUseCase
|
||||||
import dev.meloda.fast.domain.util.extractAvatar
|
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.domain.util.extractTitle
|
||||||
import dev.meloda.fast.messageshistory.model.ActionMode
|
import dev.meloda.fast.messageshistory.model.ActionMode
|
||||||
import dev.meloda.fast.messageshistory.model.MessageDialog
|
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.model.MessagesHistoryScreenState
|
||||||
import dev.meloda.fast.messageshistory.navigation.MessagesHistory
|
import dev.meloda.fast.messageshistory.navigation.MessagesHistory
|
||||||
import dev.meloda.fast.model.BaseError
|
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.VkMessage
|
||||||
import dev.meloda.fast.model.api.domain.VkPhotoDomain
|
|
||||||
import dev.meloda.fast.ui.R
|
|
||||||
import dev.meloda.fast.ui.model.vk.MessageUiItem
|
import dev.meloda.fast.ui.model.vk.MessageUiItem
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
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.coroutines.resume
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.random.Random
|
|
||||||
|
|
||||||
class MessagesHistoryViewModelImpl(
|
class MessagesHistoryViewModelImpl(
|
||||||
private val applicationContext: Context,
|
private val applicationContext: Context,
|
||||||
private val messagesUseCase: MessagesUseCase,
|
private val messagesUseCase: MessagesUseCase,
|
||||||
private val convoUseCase: ConvoUseCase,
|
private val convoUseCase: ConvoUseCase,
|
||||||
private val resourceProvider: ResourceProvider,
|
private val resourceProvider: ResourceProvider,
|
||||||
private val userSettings: UserSettings,
|
|
||||||
private val loadConvosByIdUseCase: LoadConvosByIdUseCase,
|
private val loadConvosByIdUseCase: LoadConvosByIdUseCase,
|
||||||
private val getMessageReadPeersUseCase: GetMessageReadPeersUseCase,
|
private val getMessageReadPeersUseCase: GetMessageReadPeersUseCase,
|
||||||
updatesParser: LongPollUpdatesParser,
|
updatesParser: LongPollUpdatesParser,
|
||||||
@@ -110,14 +77,19 @@ class MessagesHistoryViewModelImpl(
|
|||||||
override val messages = MutableStateFlow<List<VkMessage>>(emptyList())
|
override val messages = MutableStateFlow<List<VkMessage>>(emptyList())
|
||||||
override val uiMessages = MutableStateFlow<List<MessageUiItem>>(emptyList())
|
override val uiMessages = MutableStateFlow<List<MessageUiItem>>(emptyList())
|
||||||
|
|
||||||
private var lastMessageText: String? = null
|
private val messageActions = MessagesHistoryMessageActions(
|
||||||
|
applicationContext = applicationContext,
|
||||||
private val sendingMessages: MutableList<VkMessage> = mutableListOf()
|
viewModelScope = viewModelScope,
|
||||||
private val failedMessages: MutableList<VkMessage> = mutableListOf()
|
messagesUseCase = messagesUseCase,
|
||||||
|
resourceProvider = resourceProvider,
|
||||||
private var replyToCmId: Long? = null
|
screenState = screenState,
|
||||||
|
messages = messages,
|
||||||
private var editMessage: VkMessage? = null
|
baseError = baseError,
|
||||||
|
showKeyboard = showKeyboard,
|
||||||
|
dialog = dialog,
|
||||||
|
syncUiMessages = ::syncUiMessages,
|
||||||
|
onPinnedMessageChanged = ::handlePinnedMessage
|
||||||
|
)
|
||||||
|
|
||||||
private val longPollEventHandler = MessagesHistoryLongPollEventHandler(
|
private val longPollEventHandler = MessagesHistoryLongPollEventHandler(
|
||||||
screenState = screenState,
|
screenState = screenState,
|
||||||
@@ -177,7 +149,7 @@ class MessagesHistoryViewModelImpl(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteMessage(
|
messageActions.deleteMessage(
|
||||||
messageIds = listOf(dialog.message.id),
|
messageIds = listOf(dialog.message.id),
|
||||||
deleteForAll = deleteForEveryone
|
deleteForAll = deleteForEveryone
|
||||||
)
|
)
|
||||||
@@ -192,7 +164,7 @@ class MessagesHistoryViewModelImpl(
|
|||||||
.filter { it.id > 0 }
|
.filter { it.id > 0 }
|
||||||
.map(VkMessage::id)
|
.map(VkMessage::id)
|
||||||
|
|
||||||
deleteMessage(
|
messageActions.deleteMessage(
|
||||||
messageIds = messageIdsToDelete,
|
messageIds = messageIdsToDelete,
|
||||||
deleteForAll = deleteForEveryone,
|
deleteForAll = deleteForEveryone,
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
@@ -206,15 +178,15 @@ class MessagesHistoryViewModelImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
is MessageDialog.MessagePin -> {
|
is MessageDialog.MessagePin -> {
|
||||||
pinMessage(dialog.messageId)
|
messageActions.pinMessage(dialog.messageId)
|
||||||
}
|
}
|
||||||
|
|
||||||
is MessageDialog.MessageUnpin -> {
|
is MessageDialog.MessageUnpin -> {
|
||||||
unpinMessage(dialog.messageId)
|
messageActions.unpinMessage(dialog.messageId)
|
||||||
}
|
}
|
||||||
|
|
||||||
is MessageDialog.MessageMarkImportance -> {
|
is MessageDialog.MessageMarkImportance -> {
|
||||||
markAsImportant(
|
messageActions.markAsImportant(
|
||||||
messageIds = listOf(dialog.message.id),
|
messageIds = listOf(dialog.message.id),
|
||||||
important = dialog.isImportant
|
important = dialog.isImportant
|
||||||
)
|
)
|
||||||
@@ -222,7 +194,7 @@ class MessagesHistoryViewModelImpl(
|
|||||||
|
|
||||||
is MessageDialog.MessageSpam -> {
|
is MessageDialog.MessageSpam -> {
|
||||||
if (dialog.isSpam) {
|
if (dialog.isSpam) {
|
||||||
deleteMessage(
|
messageActions.deleteMessage(
|
||||||
messageIds = listOf(dialog.message.id),
|
messageIds = listOf(dialog.message.id),
|
||||||
spam = true
|
spam = true
|
||||||
)
|
)
|
||||||
@@ -250,7 +222,7 @@ class MessagesHistoryViewModelImpl(
|
|||||||
// TODO: 28-Mar-25, Danil Nikolaev: retry sending
|
// TODO: 28-Mar-25, Danil Nikolaev: retry sending
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageOption.Reply -> replyToMessage(cmId)
|
MessageOption.Reply -> messageActions.replyToMessage(cmId)
|
||||||
|
|
||||||
MessageOption.ForwardHere -> {
|
MessageOption.ForwardHere -> {
|
||||||
|
|
||||||
@@ -273,11 +245,11 @@ class MessagesHistoryViewModelImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
MessageOption.Read -> {
|
MessageOption.Read -> {
|
||||||
readMessage(dialog.message)
|
messageActions.readMessage(dialog.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageOption.Copy -> {
|
MessageOption.Copy -> {
|
||||||
copyMessage(dialog.message)
|
messageActions.copyMessage(dialog.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageOption.MarkAsImportant,
|
MessageOption.MarkAsImportant,
|
||||||
@@ -301,7 +273,7 @@ class MessagesHistoryViewModelImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
MessageOption.Edit -> {
|
MessageOption.Edit -> {
|
||||||
editMessage(cmId)
|
messageActions.editMessage(cmId)
|
||||||
syncUiMessages()
|
syncUiMessages()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,7 +304,7 @@ class MessagesHistoryViewModelImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (screenState.value.editCmId != null) {
|
if (screenState.value.editCmId != null) {
|
||||||
stopEditMessage()
|
messageActions.stopEditMessage()
|
||||||
}
|
}
|
||||||
|
|
||||||
syncUiMessages()
|
syncUiMessages()
|
||||||
@@ -380,9 +352,9 @@ class MessagesHistoryViewModelImpl(
|
|||||||
|
|
||||||
override fun onActionButtonClicked() {
|
override fun onActionButtonClicked() {
|
||||||
when (screenState.value.actionMode) {
|
when (screenState.value.actionMode) {
|
||||||
ActionMode.DELETE -> confirmDeleteCurrentEditMessage()
|
ActionMode.DELETE -> messageActions.confirmDeleteCurrentEditMessage()
|
||||||
|
|
||||||
ActionMode.EDIT -> editCurrentEditMessage()
|
ActionMode.EDIT -> messageActions.editCurrentEditMessage()
|
||||||
|
|
||||||
ActionMode.RECORD_AUDIO -> {
|
ActionMode.RECORD_AUDIO -> {
|
||||||
screenState.setValue { it.copy(actionMode = ActionMode.RECORD_VIDEO) }
|
screenState.setValue { it.copy(actionMode = ActionMode.RECORD_VIDEO) }
|
||||||
@@ -392,7 +364,7 @@ class MessagesHistoryViewModelImpl(
|
|||||||
screenState.setValue { it.copy(actionMode = ActionMode.RECORD_AUDIO) }
|
screenState.setValue { it.copy(actionMode = ActionMode.RECORD_AUDIO) }
|
||||||
}
|
}
|
||||||
|
|
||||||
ActionMode.SEND -> sendMessage()
|
ActionMode.SEND -> messageActions.sendMessage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,7 +435,7 @@ class MessagesHistoryViewModelImpl(
|
|||||||
|
|
||||||
selectedMessages.setValue { emptyList() }
|
selectedMessages.setValue { emptyList() }
|
||||||
|
|
||||||
editMessage(cmId)
|
messageActions.editMessage(cmId)
|
||||||
|
|
||||||
syncUiMessages()
|
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() {
|
override fun onBoldClicked() {
|
||||||
val selectionRange = screenState.value.message.selection
|
messageActions.onBoldClicked()
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItalicClicked() {
|
override fun onItalicClicked() {
|
||||||
val selectionRange = screenState.value.message.selection
|
messageActions.onItalicClicked()
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUnderlineClicked() {
|
override fun onUnderlineClicked() {
|
||||||
val selectionRange = screenState.value.message.selection
|
messageActions.onUnderlineClicked()
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLinkClicked() {
|
override fun onLinkClicked() {
|
||||||
@@ -650,23 +463,15 @@ class MessagesHistoryViewModelImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onRegularClicked() {
|
override fun onRegularClicked() {
|
||||||
formatData = formatData.copy(items = emptyList())
|
messageActions.onRegularClicked()
|
||||||
updateStyles()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReplyCloseClicked() {
|
override fun onReplyCloseClicked() {
|
||||||
replyToCmId = null
|
messageActions.onReplyCloseClicked()
|
||||||
|
|
||||||
screenState.setValue { old ->
|
|
||||||
old.copy(
|
|
||||||
replyTitle = null,
|
|
||||||
replyText = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRequestReplyToMessage(cmId: Long) {
|
override fun onRequestReplyToMessage(cmId: Long) {
|
||||||
replyToMessage(cmId)
|
messageActions.replyToMessage(cmId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onKeyboardShown() {
|
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> {
|
private fun syncUiMessages(): List<MessageUiItem> {
|
||||||
val newUiMessages = buildMessagesHistoryUiMessages(
|
val newUiMessages = buildMessagesHistoryUiMessages(
|
||||||
messages = messages.value,
|
messages = messages.value,
|
||||||
|
|||||||
Reference in New Issue
Block a user