From 0eb31464283eb8b6f669154771c8793d16f7a633 Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Sun, 23 Mar 2025 17:55:28 +0300 Subject: [PATCH] Release 0.1.9 (#140) * improvements in longpoll's stuff --- .../service/longpolling/LongPollingService.kt | 8 +- .../fast/domain/LongPollUpdatesParser.kt | 516 +++++++++++++----- .../kotlin/dev/meloda/fast/model/ApiEvent.kt | 5 +- .../meloda/fast/model/ConversationFlags.kt | 17 + .../dev/meloda/fast/model/LongPollEvent.kt | 56 +- .../meloda/fast/model/LongPollParsedEvent.kt | 85 +++ .../dev/meloda/fast/model/MessageFlags.kt | 31 ++ .../conversations/ConversationsViewModel.kt | 179 +++--- .../MessagesHistoryViewModel.kt | 40 +- 9 files changed, 685 insertions(+), 252 deletions(-) create mode 100644 core/model/src/main/kotlin/dev/meloda/fast/model/ConversationFlags.kt create mode 100644 core/model/src/main/kotlin/dev/meloda/fast/model/LongPollParsedEvent.kt create mode 100644 core/model/src/main/kotlin/dev/meloda/fast/model/MessageFlags.kt diff --git a/app/src/main/kotlin/dev/meloda/fast/service/longpolling/LongPollingService.kt b/app/src/main/kotlin/dev/meloda/fast/service/longpolling/LongPollingService.kt index 30056e51..4a9089ab 100644 --- a/app/src/main/kotlin/dev/meloda/fast/service/longpolling/LongPollingService.kt +++ b/app/src/main/kotlin/dev/meloda/fast/service/longpolling/LongPollingService.kt @@ -16,11 +16,11 @@ import dev.meloda.fast.common.LongPollController import dev.meloda.fast.common.VkConstants import dev.meloda.fast.common.extensions.listenValue import dev.meloda.fast.common.model.LongPollState -import dev.meloda.fast.domain.LongPollUpdatesParser -import dev.meloda.fast.domain.LongPollUseCase import dev.meloda.fast.data.UserConfig import dev.meloda.fast.data.processState import dev.meloda.fast.datastore.AppSettings +import dev.meloda.fast.domain.LongPollUpdatesParser +import dev.meloda.fast.domain.LongPollUseCase import dev.meloda.fast.model.api.data.LongPollUpdates import dev.meloda.fast.model.api.data.VkLongPollData import dev.meloda.fast.ui.R @@ -249,6 +249,7 @@ class LongPollingService : Service() { override fun onDestroy() { Log.d(STATE_TAG, "onDestroy") longPollController.updateCurrentState(LongPollState.Stopped) + updatesParser.clearListeners() try { AppSettings.edit { putBoolean(KEY_LONG_POLL_WAS_DESTROYED, true) } job.cancel() @@ -259,8 +260,7 @@ class LongPollingService : Service() { } override fun onTrimMemory(level: Int) { - Log.d(STATE_TAG, "onTrimMemory") - longPollController.updateCurrentState(LongPollState.Stopped) + Log.d(STATE_TAG, "onTrimMemory. Level: $level") super.onTrimMemory(level) } diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/LongPollUpdatesParser.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/LongPollUpdatesParser.kt index 65037ee5..e66b4734 100644 --- a/core/domain/src/main/kotlin/dev/meloda/fast/domain/LongPollUpdatesParser.kt +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/LongPollUpdatesParser.kt @@ -11,11 +11,15 @@ import dev.meloda.fast.data.processState import dev.meloda.fast.model.ApiEvent 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.VkMessage import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlin.coroutines.CoroutineContext import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine @@ -25,23 +29,24 @@ class LongPollUpdatesParser( ) { private val job = SupervisorJob() - private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> - Log.d("LongPollUpdatesParser", "error: $throwable") - throwable.printStackTrace() - } + private val exceptionHandler = + CoroutineExceptionHandler { _, throwable -> + Log.e("LongPollUpdatesParser", "error: $throwable") + throwable.printStackTrace() + } private val coroutineContext: CoroutineContext get() = Dispatchers.Default + job + exceptionHandler private val coroutineScope = CoroutineScope(coroutineContext) - private val listenersMap: MutableMap>> = + private val listenersMap: MutableMap>> = mutableMapOf() fun parseNextUpdate(event: List) { val eventId = event.first().asInt() - when (val eventType = ApiEvent.parseOrNull(eventId)) { + when (val eventType = ApiEvent.parseOrNull(eventId)) { null -> Log.d("LongPollUpdatesParser", "parseNextUpdate: unknownEvent: $event") ApiEvent.MESSAGE_SET_FLAGS -> parseMessageSetFlags(eventType, event) @@ -50,8 +55,11 @@ class LongPollUpdatesParser( 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.PIN_UNPIN_CONVERSATION -> parseConversationPinStateChanged(eventType, event) + ApiEvent.CHAT_MAJOR_CHANGED -> parseChatMajorChanged(eventType, event) + ApiEvent.CHAT_MINOR_CHANGED -> parseChatMinorChanged(eventType, event) ApiEvent.TYPING, ApiEvent.AUDIO_MESSAGE_RECORDING, @@ -59,14 +67,10 @@ class LongPollUpdatesParser( ApiEvent.VIDEO_UPLOADING, ApiEvent.FILE_UPLOADING -> parseInteraction(eventType, event) - ApiEvent.UNREAD_COUNT_UPDATE -> onNewEvent(eventType, event) + ApiEvent.UNREAD_COUNT_UPDATE -> parseUnreadCounterUpdate(eventType, event) } } - private fun onNewEvent(eventType: ApiEvent, event: List) { - Log.d("LongPollUpdatesParser", "newEvent: $eventType: $event") - } - private fun parseInteraction(eventType: ApiEvent, event: List) { Log.d("LongPollUpdatesParser", "$eventType: $event") @@ -79,6 +83,15 @@ class LongPollUpdatesParser( 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].asInt() val userIds = event[2].toList(Any::asInt).filter { it != UserConfig.userId } val totalCount = event[3].asInt() @@ -87,51 +100,227 @@ class LongPollUpdatesParser( // if userIds contains only account's id, then we don't need to show our status if (userIds.isEmpty()) return - coroutineScope.launch { - listenersMap[eventType]?.let { listeners -> - listeners.forEach { vkEventCallback -> - (vkEventCallback as VkEventCallback) - .onEvent( - LongPollEvent.Interaction( - interactionType = interactionType, - peerId = peerId, - userIds = userIds, - totalCount = totalCount, - timestamp = timestamp - ) + listenersMap[longPollEvent]?.let { listeners -> + listeners.forEach { vkEventCallback -> + (vkEventCallback as VkEventCallback) + .onEvent( + LongPollParsedEvent.Interaction( + interactionType = interactionType, + peerId = peerId, + userIds = userIds, + totalCount = totalCount, + timestamp = timestamp ) - } + ) } } } - private fun parseConversationPinStateChanged(eventType: ApiEvent, event: List) { - Log.d("LongPollUpdatesParser", "$eventType: $event") + private fun parseUnreadCounterUpdate(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType $event") - val peerId = event[1].asInt() - val majorId = event[2].asInt() + 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() - coroutineScope.launch { - listenersMap[ApiEvent.PIN_UNPIN_CONVERSATION]?.let { listeners -> - listeners.forEach { vkEventCallback -> - (vkEventCallback as VkEventCallback) - .onEvent( - LongPollEvent.VkConversationPinStateChangedEvent( - peerId = peerId, - majorId = majorId - ) + listenersMap[LongPollEvent.UNREAD_COUNTER_UPDATE]?.let { listeners -> + listeners.forEach { vkEventCallback -> + (vkEventCallback as VkEventCallback) + .onEvent( + LongPollParsedEvent.UnreadCounter( + unread = unreadCount, + unreadUnmuted = unreadUnmutedCount, + showOnlyMuted = showOnlyMuted, + business = businessNotifyUnreadCount, + archive = archiveUnreadCount, + archiveUnmuted = archiveUnreadUnmutedCount, + archiveMentions = archiveMentionsCount ) - } + ) } } } private fun parseMessageSetFlags(eventType: ApiEvent, event: List) { Log.d("LongPollUpdatesParser", "$eventType: $event") + + val messageId = event[1].asInt() + val flags = event[2].asInt() + val peerId = event[3].asInt() + + val eventsToSend = mutableListOf() + + val parsedFlags = MessageFlags.parse(flags) + parsedFlags.forEach { flag -> + when (flag) { + MessageFlags.IMPORTANT -> { // marked as important + val eventToSend = LongPollParsedEvent.MessageMarkedAsImportant( + peerId = peerId, + messageId = messageId, + marked = true + ) + eventsToSend += eventToSend + + listenersMap[LongPollEvent.MARKED_AS_IMPORTANT]?.let { listeners -> + listeners.map { vkEventCallback -> + (vkEventCallback as? VkEventCallback) + ?.onEvent(eventToSend) + } + } + } + + MessageFlags.SPAM -> { // marked as spam + val eventToSend = LongPollParsedEvent.MessageMarkedAsSpam( + peerId = peerId, + messageId = messageId + ) + eventsToSend += eventToSend + + listenersMap[LongPollEvent.MARKED_AS_SPAM]?.let { listeners -> + listeners.map { vkEventCallback -> + (vkEventCallback as? VkEventCallback) + ?.onEvent(eventToSend) + } + } + } + + MessageFlags.DELETED -> { + val eventToSend = + if (parsedFlags.contains(MessageFlags.DELETED_FOR_ALL)) { // deleted for all + LongPollParsedEvent.MessageDeleted( + peerId = peerId, + messageId = messageId, + forAll = true + ) + } else { // deleted only for me + LongPollParsedEvent.MessageDeleted( + peerId = peerId, + messageId = messageId, + forAll = false + ) + } + eventsToSend += eventToSend + + listenersMap[LongPollEvent.MESSAGE_DELETED]?.let { listeners -> + listeners.map { vkEventCallback -> + (vkEventCallback as? VkEventCallback) + ?.onEvent(eventToSend) + } + } + } + + MessageFlags.AUDIO_LISTENED -> { // audio message listened + val eventToSend = LongPollParsedEvent.AudioMessageListened( + peerId = peerId, + messageId = messageId + ) + eventsToSend += eventToSend + + listenersMap[LongPollEvent.AUDIO_MESSAGE_LISTENED]?.let { listeners -> + listeners.map { vkEventCallback -> + (vkEventCallback as? VkEventCallback) + ?.onEvent(eventToSend) + } + } + } + + else -> Unit + } + } + + eventsToSend.forEach { eventToSend -> + listenersMap[LongPollEvent.MESSAGE_SET_FLAGS]?.let { listeners -> + listeners.map { vkEventCallback -> + (vkEventCallback as? VkEventCallback)?.onEvent(eventToSend) + } + } + } } private fun parseMessageClearFlags(eventType: ApiEvent, event: List) { Log.d("LongPollUpdatesParser", "$eventType: $event") + + val messageId = event[1].asInt() + val flags = event[2].asInt() + val peerId = event[3].asInt() + + val eventsToSend = mutableListOf() + + val parsedFlags = MessageFlags.parse(flags) + + coroutineScope.launch { + parsedFlags.forEach { flag -> + when (flag) { + MessageFlags.IMPORTANT -> { // not important anymore + val eventToSend = LongPollParsedEvent.MessageMarkedAsImportant( + peerId = peerId, + messageId = messageId, + marked = false + ) + eventsToSend += eventToSend + + listenersMap[LongPollEvent.MARKED_AS_IMPORTANT]?.let { listeners -> + listeners.map { vkEventCallback -> + (vkEventCallback as? VkEventCallback) + ?.onEvent(eventToSend) + } + } + } + + MessageFlags.SPAM -> { + if (parsedFlags.contains(MessageFlags.CANCEL_SPAM)) { // not spam anymore + withContext(Dispatchers.IO) { + val message = loadMessage(messageId) + message?.let { + val eventToSend = + LongPollParsedEvent.MessageMarkedAsNotSpam(message = message) + eventsToSend += eventToSend + + listenersMap[LongPollEvent.MARKED_AS_NOT_SPAM]?.let { listeners -> + listeners.map { vkEventCallback -> + (vkEventCallback as? VkEventCallback) + ?.onEvent(eventToSend) + } + } + } + } + } + } + + MessageFlags.DELETED -> { // restored + withContext(Dispatchers.IO) { + val message = loadMessage(messageId) + message?.let { + val eventToSend = + LongPollParsedEvent.MessageRestored(message = message) + eventsToSend += eventToSend + + listenersMap[LongPollEvent.MESSAGE_RESTORED]?.let { listeners -> + listeners.map { vkEventCallback -> + (vkEventCallback as? VkEventCallback) + ?.onEvent(eventToSend) + } + } + } + } + } + + else -> Unit + } + } + + eventsToSend.forEach { eventToSend -> + listenersMap[LongPollEvent.MESSAGE_CLEAR_FLAGS]?.let { listeners -> + listeners.map { vkEventCallback -> + vkEventCallback.onEvent(eventToSend) + } + } + } + } } private fun parseMessageNew(eventType: ApiEvent, event: List) { @@ -139,17 +328,11 @@ class LongPollUpdatesParser( val messageId = event[1].asInt() coroutineScope.launch(Dispatchers.IO) { - val newMessageEvent: LongPollEvent.VkMessageNewEvent? = - loadNormalMessage( - eventType, - messageId - ) - - newMessageEvent?.let { event -> - listenersMap[ApiEvent.MESSAGE_NEW]?.let { + loadMessage(messageId)?.let { message -> + listenersMap[LongPollEvent.MESSAGE_NEW]?.let { it.map { vkEventCallback -> - (vkEventCallback as VkEventCallback) - .onEvent(event) + (vkEventCallback as VkEventCallback) + .onEvent(LongPollParsedEvent.NewMessage(message)) } } } @@ -160,18 +343,12 @@ class LongPollUpdatesParser( Log.d("LongPollUpdatesParser", "$eventType: $event") val messageId = event[1].asInt() - coroutineScope.launch { - val editedMessageEvent: LongPollEvent.VkMessageEditEvent? = - loadNormalMessage( - eventType, - messageId - ) - - editedMessageEvent?.let { event -> - listenersMap[ApiEvent.MESSAGE_EDIT]?.let { + coroutineScope.launch(Dispatchers.IO) { + loadMessage(messageId)?.let { message -> + listenersMap[LongPollEvent.MESSAGE_EDITED]?.let { it.map { vkEventCallback -> - (vkEventCallback as VkEventCallback) - .onEvent(event) + (vkEventCallback as VkEventCallback) + .onEvent(LongPollParsedEvent.MessageEdited(message)) } } } @@ -184,18 +361,16 @@ class LongPollUpdatesParser( val messageId = event[2].asInt() val unreadCount = event[3].asInt() - coroutineScope.launch { - listenersMap[ApiEvent.MESSAGE_READ_INCOMING]?.let { listeners -> - listeners.map { vkEventCallback -> - (vkEventCallback as VkEventCallback) - .onEvent( - LongPollEvent.VkMessageReadIncomingEvent( - peerId = peerId, - messageId = messageId, - unreadCount = unreadCount - ) + listenersMap[LongPollEvent.INCOMING_MESSAGE_READ]?.let { listeners -> + listeners.map { vkEventCallback -> + (vkEventCallback as VkEventCallback) + .onEvent( + LongPollParsedEvent.IncomingMessageRead( + peerId = peerId, + messageId = messageId, + unreadCount = unreadCount ) - } + ) } } } @@ -206,30 +381,86 @@ class LongPollUpdatesParser( val messageId = event[2].asInt() val unreadCount = event[3].asInt() - coroutineScope.launch { - listenersMap[ApiEvent.MESSAGE_READ_OUTGOING]?.let { listeners -> - listeners.map { vkEventCallback -> - (vkEventCallback as VkEventCallback) - .onEvent( - LongPollEvent.VkMessageReadOutgoingEvent( - peerId = peerId, - messageId = messageId, - unreadCount = unreadCount - ) + listenersMap[LongPollEvent.OUTGOING_MESSAGE_READ]?.let { listeners -> + listeners.map { vkEventCallback -> + (vkEventCallback as VkEventCallback) + .onEvent( + LongPollParsedEvent.OutgoingMessageRead( + peerId = peerId, + messageId = messageId, + unreadCount = unreadCount ) - } + ) } } } - private fun parseMessagesDeleted(eventType: ApiEvent, event: List) { + private fun parseChatClearFlags(eventType: ApiEvent, event: List) { Log.d("LongPollUpdatesParser", "$eventType: $event") } - private suspend inline fun loadNormalMessage( - eventType: ApiEvent, - messageId: Int - ): T? = suspendCoroutine { continuation -> + private fun parseChatSetFlags(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType: $event") + } + + private fun parseMessagesDeleted(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType: $event") + + val peerId = event[1].asInt() + val messageId = event[2].asInt() + + listenersMap[LongPollEvent.CHAT_CLEARED]?.let { listeners -> + listeners.forEach { vkEventCallback -> + (vkEventCallback as VkEventCallback) + .onEvent( + LongPollParsedEvent.ChatCleared( + peerId = peerId, + toMessageId = messageId + ) + ) + } + } + } + + private fun parseChatMajorChanged(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType: $event") + + val peerId = event[1].asInt() + val majorId = event[2].asInt() + + listenersMap[LongPollEvent.CHAT_MAJOR_CHANGED]?.let { listeners -> + listeners.forEach { vkEventCallback -> + (vkEventCallback as VkEventCallback) + .onEvent( + LongPollParsedEvent.ChatMajorChanged( + peerId = peerId, + majorId = majorId, + ) + ) + } + } + } + + private fun parseChatMinorChanged(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType: $event") + + val peerId = event[1].asInt() + val minorId = event[2].asInt() + + listenersMap[LongPollEvent.CHAT_MINOR_CHANGED]?.let { listeners -> + listeners.forEach { vkEventCallback -> + (vkEventCallback as VkEventCallback) + .onEvent( + LongPollParsedEvent.ChatMinorChanged( + peerId = peerId, + minorId = minorId, + ) + ) + } + } + } + + private suspend fun loadMessage(messageId: Int): VkMessage? = suspendCoroutine { continuation -> coroutineScope.launch(Dispatchers.IO) { messagesUseCase.getById( messageIds = listOf(messageId), @@ -238,10 +469,11 @@ class LongPollUpdatesParser( ).listenValue(this) { state -> state.processState( error = { error -> - Log.e("LongPollUpdatesParser", "loadNormalMessage: error: $error") + Log.e("LongPollUpdatesParser", "loadMessage: error: $error") + continuation.resume(null) }, - success = { messages -> - val message = messages.singleOrNull() ?: run { + success = { response -> + val message = response.singleOrNull() ?: run { continuation.resume(null) return@listenValue } @@ -249,107 +481,113 @@ class LongPollUpdatesParser( VkMemoryCache[message.id] = message messagesUseCase.storeMessage(message) - val resumeValue: LongPollEvent? = when (eventType) { - ApiEvent.MESSAGE_NEW -> LongPollEvent.VkMessageNewEvent(message) - ApiEvent.MESSAGE_EDIT -> LongPollEvent.VkMessageEditEvent(message) - - else -> { - continuation.resume(null) - null - } - } - - resumeValue?.let { value -> continuation.resume(value as T) } + continuation.resume(message) } ) } } } - private fun registerListener( - eventType: ApiEvent, + @Suppress("UNCHECKED_CAST") + private fun registerListener( + eventType: LongPollEvent, listener: VkEventCallback ) { listenersMap.let { map -> - map[eventType] = (map[eventType] ?: mutableListOf()).also { it.add(listener) } + map[eventType] = (map[eventType] ?: mutableListOf()) + .also { + it.add(listener as VkEventCallback) + } } } - private fun registerListeners( - eventTypes: List, + private fun registerListeners( + eventTypes: List, listener: VkEventCallback ) { eventTypes.forEach { eventType -> registerListener(eventType, listener) } } - fun onConversationPinStateChanged(listener: VkEventCallback) { - registerListener(ApiEvent.PIN_UNPIN_CONVERSATION, listener) + fun onMessageSetFlags(block: (LongPollParsedEvent) -> Unit) { + registerListener(LongPollEvent.MESSAGE_SET_FLAGS, assembleEventCallback(block)) } - fun onConversationPinStateChanged(block: (LongPollEvent.VkConversationPinStateChangedEvent) -> Unit) { - onConversationPinStateChanged(assembleEventCallback(block)) + fun onMessageMarkedAsImportant(block: (LongPollParsedEvent.MessageMarkedAsImportant) -> Unit) { + registerListener(LongPollEvent.MARKED_AS_IMPORTANT, assembleEventCallback(block)) } - fun onMessageIncomingRead(listener: VkEventCallback) { - registerListener(ApiEvent.MESSAGE_READ_INCOMING, listener) + fun onMessageMarkedAsSpam(block: (LongPollParsedEvent.MessageMarkedAsSpam) -> Unit) { + registerListener(LongPollEvent.MARKED_AS_SPAM, assembleEventCallback(block)) } - fun onMessageIncomingRead(block: (LongPollEvent.VkMessageReadIncomingEvent) -> Unit) { - onMessageIncomingRead(assembleEventCallback(block)) + fun onMessageDeleted(block: (LongPollParsedEvent.MessageDeleted) -> Unit) { + registerListener(LongPollEvent.MESSAGE_DELETED, assembleEventCallback(block)) } - fun onMessageOutgoingRead(listener: VkEventCallback) { - registerListener(ApiEvent.MESSAGE_READ_OUTGOING, listener) + fun onMessageClearFlags(block: (LongPollParsedEvent) -> Unit) { + registerListener(LongPollEvent.MESSAGE_CLEAR_FLAGS, assembleEventCallback(block)) } - fun onMessageOutgoingRead(block: (LongPollEvent.VkMessageReadOutgoingEvent) -> Unit) { - onMessageOutgoingRead(assembleEventCallback(block)) + fun onMessageMarkedAsNotSpam(block: (LongPollParsedEvent.MessageMarkedAsNotSpam) -> Unit) { + registerListener(LongPollEvent.MARKED_AS_NOT_SPAM, assembleEventCallback(block)) } - fun onNewMessage(listener: VkEventCallback) { - registerListener(ApiEvent.MESSAGE_NEW, listener) + fun onMessageRestored(block: (LongPollParsedEvent.MessageRestored) -> Unit) { + registerListener(LongPollEvent.MESSAGE_RESTORED, assembleEventCallback(block)) } - fun onNewMessage(block: (LongPollEvent.VkMessageNewEvent) -> Unit) { - onNewMessage(assembleEventCallback(block)) + fun onNewMessage(block: (LongPollParsedEvent.NewMessage) -> Unit) { + registerListener(LongPollEvent.MESSAGE_NEW, assembleEventCallback(block)) } - fun onMessageEdited(listener: VkEventCallback) { - registerListener(ApiEvent.MESSAGE_EDIT, listener) + fun onMessageEdited(block: (LongPollParsedEvent.MessageEdited) -> Unit) { + registerListener(LongPollEvent.MESSAGE_EDITED, assembleEventCallback(block)) } - fun onMessageEdited(block: (LongPollEvent.VkMessageEditEvent) -> Unit) { - onMessageEdited(assembleEventCallback(block)) + fun onMessageIncomingRead(block: (LongPollParsedEvent.IncomingMessageRead) -> Unit) { + registerListener(LongPollEvent.INCOMING_MESSAGE_READ, assembleEventCallback(block)) } - fun onInteractions(listener: VkEventCallback) { + fun onMessageOutgoingRead(block: (LongPollParsedEvent.OutgoingMessageRead) -> Unit) { + registerListener(LongPollEvent.OUTGOING_MESSAGE_READ, assembleEventCallback(block)) + } + + fun onChatCleared(block: (LongPollParsedEvent.ChatCleared) -> Unit) { + registerListener(LongPollEvent.CHAT_CLEARED, assembleEventCallback(block)) + } + + fun onChatMajorChanged(block: (LongPollParsedEvent.ChatMajorChanged) -> Unit) { + registerListener(LongPollEvent.CHAT_MAJOR_CHANGED, assembleEventCallback(block)) + } + + fun onChatMinorChanged(block: (LongPollParsedEvent.ChatMinorChanged) -> Unit) { + registerListener(LongPollEvent.CHAT_MINOR_CHANGED, assembleEventCallback(block)) + } + + fun onInteractions(block: (LongPollParsedEvent.Interaction) -> Unit) { registerListeners( eventTypes = listOf( - ApiEvent.TYPING, - ApiEvent.AUDIO_MESSAGE_RECORDING, - ApiEvent.PHOTO_UPLOADING, - ApiEvent.VIDEO_UPLOADING, - ApiEvent.FILE_UPLOADING + LongPollEvent.TYPING, + LongPollEvent.AUDIO_MESSAGE_RECORDING, + LongPollEvent.PHOTO_UPLOADING, + LongPollEvent.VIDEO_UPLOADING, + LongPollEvent.FILE_UPLOADING ), - listener = listener + listener = assembleEventCallback(block) ) } - fun onInteractions(block: (LongPollEvent.Interaction) -> Unit) { - onInteractions(assembleEventCallback(block)) - } - fun clearListeners() { listenersMap.clear() } } -internal inline fun assembleEventCallback( +internal inline fun assembleEventCallback( crossinline block: (R) -> Unit, ): VkEventCallback { return VkEventCallback { event -> block.invoke(event) } } -fun interface VkEventCallback { +fun interface VkEventCallback { fun onEvent(event: T) } diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/ApiEvent.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/ApiEvent.kt index 74067e29..1e2cbc14 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/ApiEvent.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/ApiEvent.kt @@ -7,8 +7,11 @@ enum class ApiEvent(val value: Int) { MESSAGE_EDIT(5), MESSAGE_READ_INCOMING(6), MESSAGE_READ_OUTGOING(7), + CHAT_CLEAR_FLAGS(10), + CHAT_SET_FLAGS(12), MESSAGES_DELETED(13), - PIN_UNPIN_CONVERSATION(20), + CHAT_MAJOR_CHANGED(20), + CHAT_MINOR_CHANGED(21), TYPING(63), AUDIO_MESSAGE_RECORDING(64), PHOTO_UPLOADING(65), diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/ConversationFlags.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/ConversationFlags.kt new file mode 100644 index 00000000..19ab4de6 --- /dev/null +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/ConversationFlags.kt @@ -0,0 +1,17 @@ +package dev.meloda.fast.model + +enum class ConversationFlags(val value: Int) { + DISABLE_PUSH(16), + DISABLE_SOUND(32), + INCOMING_CHAT_REQUEST(256), + DECLINED_CHAT_REQUEST(512), + MENTION(1024), + HIDE_CHAT_FROM_SEARCH(2048), + BUSINESS_CHAT(8192), + MARKED_MESSAGE(16384), // mention or disappearing message + DO_NOT_NOTIFY_MENTIONS_ALL_ONLINE(262144), + DO_NOT_NOTIFY_ALL_MENTIONS(524288), + MARKED_AS_UNREAD(1048576), + ARCHIVED(8388608), + CALL_IN_PROGRESS(16777216), +} diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/LongPollEvent.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/LongPollEvent.kt index 3acf2ca3..2c25bc13 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/LongPollEvent.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/LongPollEvent.kt @@ -1,35 +1,27 @@ package dev.meloda.fast.model -import dev.meloda.fast.model.api.domain.VkMessage - -sealed interface LongPollEvent { - - data class VkMessageNewEvent(val message: VkMessage) : LongPollEvent - - data class VkMessageEditEvent(val message: VkMessage) : LongPollEvent - - data class VkMessageReadIncomingEvent( - val peerId: Int, - val messageId: Int, - val unreadCount: Int, - ) : LongPollEvent - - data class VkMessageReadOutgoingEvent( - val peerId: Int, - val messageId: Int, - val unreadCount: Int, - ) : LongPollEvent - - data class VkConversationPinStateChangedEvent( - val peerId: Int, - val majorId: Int, - ) : LongPollEvent - - data class Interaction( - val interactionType: InteractionType, - val peerId: Int, - val userIds: List, - val totalCount: Int, - val timestamp: Int - ) : LongPollEvent +enum class LongPollEvent { + MESSAGE_SET_FLAGS, + MESSAGE_CLEAR_FLAGS, + MESSAGE_NEW, + MESSAGE_EDITED, + INCOMING_MESSAGE_READ, + OUTGOING_MESSAGE_READ, + CHAT_SET_FLAGS, + CHAT_CLEAR_FLAGS, + CHAT_MAJOR_CHANGED, + CHAT_MINOR_CHANGED, + TYPING, + AUDIO_MESSAGE_RECORDING, + PHOTO_UPLOADING, + VIDEO_UPLOADING, + FILE_UPLOADING, + UNREAD_COUNTER_UPDATE, + MARKED_AS_IMPORTANT, + MARKED_AS_SPAM, + MARKED_AS_NOT_SPAM, + MESSAGE_DELETED, + MESSAGE_RESTORED, + AUDIO_MESSAGE_LISTENED, + CHAT_CLEARED } diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/LongPollParsedEvent.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/LongPollParsedEvent.kt new file mode 100644 index 00000000..822a6cf8 --- /dev/null +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/LongPollParsedEvent.kt @@ -0,0 +1,85 @@ +package dev.meloda.fast.model + +import dev.meloda.fast.model.api.domain.VkMessage + +sealed interface LongPollParsedEvent { + + data class NewMessage(val message: VkMessage) : LongPollParsedEvent + + data class MessageEdited(val message: VkMessage) : LongPollParsedEvent + + data class IncomingMessageRead( + val peerId: Int, + val messageId: Int, + val unreadCount: Int, + ) : LongPollParsedEvent + + data class OutgoingMessageRead( + val peerId: Int, + val messageId: Int, + val unreadCount: Int, + ) : LongPollParsedEvent + + data class ChatMajorChanged( + val peerId: Int, + val majorId: Int, + ) : LongPollParsedEvent + + data class ChatMinorChanged( + val peerId: Int, + val minorId: Int + ) : LongPollParsedEvent + + data class Interaction( + val interactionType: InteractionType, + val peerId: Int, + val userIds: List, + val totalCount: Int, + val timestamp: Int + ) : LongPollParsedEvent + + data class UnreadCounter( + val unread: Int, + val unreadUnmuted: Int, + val showOnlyMuted: Boolean, + val business: Int, + val archive: Int, + val archiveUnmuted: Int, + val archiveMentions: Int + ) : LongPollParsedEvent + + data class MessageMarkedAsImportant( + val peerId: Int, + val messageId: Int, + val marked: Boolean + ) : LongPollParsedEvent + + data class MessageMarkedAsSpam( + val peerId: Int, + val messageId: Int + ) : LongPollParsedEvent + + data class MessageMarkedAsNotSpam( + val message: VkMessage + ) : LongPollParsedEvent + + data class MessageDeleted( + val peerId: Int, + val messageId: Int, + val forAll: Boolean + ) : LongPollParsedEvent + + data class MessageRestored( + val message: VkMessage + ) : LongPollParsedEvent + + data class AudioMessageListened( + val peerId: Int, + val messageId: Int + ) : LongPollParsedEvent + + data class ChatCleared( + val peerId: Int, + val toMessageId: Int + ): LongPollParsedEvent +} diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/MessageFlags.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/MessageFlags.kt new file mode 100644 index 00000000..c19b4bfc --- /dev/null +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/MessageFlags.kt @@ -0,0 +1,31 @@ +package dev.meloda.fast.model + +enum class MessageFlags(val value: Int) { + UNREAD(1), + OUTGOING(2), + IMPORTANT(8), + SPAM(64), + DELETED(128), + AUDIO_LISTENED(4096), + FROM_GROUP_CHAT(8192), + CANCEL_SPAM(32768), + DELETED_FOR_ALL(131072), + DO_NOT_SHOW_NOTIFICATION(1048576), + MESSAGE_WITH_REPLY(2097152), + REACTION(16777216); + + companion object { + + fun parse(mask: Int): List { + val flags = mutableListOf() + + entries.forEach { flag -> + if (mask and flag.value > 0) { + flags.add(flag) + } + } + + return flags + } + } +} diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/ConversationsViewModel.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/ConversationsViewModel.kt index 2551d953..d6b6cf2f 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/ConversationsViewModel.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/ConversationsViewModel.kt @@ -23,7 +23,7 @@ import dev.meloda.fast.domain.LongPollUpdatesParser import dev.meloda.fast.domain.MessagesUseCase import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.InteractionType -import dev.meloda.fast.model.LongPollEvent +import dev.meloda.fast.model.LongPollParsedEvent import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.network.VkErrorCode import dev.meloda.fast.ui.model.api.ConversationOption @@ -104,8 +104,10 @@ class ConversationsViewModelImpl( updatesParser.onMessageEdited(::handleEditedMessage) updatesParser.onMessageIncomingRead(::handleReadIncomingMessage) updatesParser.onMessageOutgoingRead(::handleReadOutgoingMessage) - updatesParser.onConversationPinStateChanged(::handlePinStateChanged) updatesParser.onInteractions(::handleInteraction) + updatesParser.onChatMajorChanged(::handleChatMajorChanged) + updatesParser.onChatMinorChanged(::handleChatMinorChanged) + updatesParser.onChatCleared(::handleChatClearing) loadConversations() } @@ -348,9 +350,7 @@ class ConversationsViewModelImpl( private fun deleteConversation(peerId: Int) { conversationsUseCase.delete(peerId).listenValue(viewModelScope) { state -> state.processState( - error = { error -> - - }, + error = {}, success = { val newConversations = conversations.value.toMutableList() val conversationIndex = @@ -359,16 +359,7 @@ class ConversationsViewModelImpl( newConversations.removeAt(conversationIndex) conversations.update { newConversations } - screenState.setValue { old -> - old.copy( - conversations = newConversations.map { - it.asPresentation( - resources = resources, - useContactName = useContactNames - ) - } - ) - } + sortConversations() } ) screenState.emit(screenState.value.copy(isLoading = state.isLoading())) @@ -379,15 +370,13 @@ class ConversationsViewModelImpl( conversationsUseCase.changePinState(peerId, pin) .listenValue(viewModelScope) { state -> state.processState( - error = { error -> - - }, + error = {}, success = { - handlePinStateChanged( - LongPollEvent.VkConversationPinStateChangedEvent( + handleChatMajorChanged( + LongPollParsedEvent.ChatMajorChanged( peerId = peerId, majorId = if (pin) { - (pinnedConversationsCount.value + 1) * 16 + pinnedConversationsCount.value.plus(1) * 16 } else { 0 } @@ -400,7 +389,7 @@ class ConversationsViewModelImpl( } } - private fun handleNewMessage(event: LongPollEvent.VkMessageNewEvent) { + private fun handleNewMessage(event: LongPollParsedEvent.NewMessage) { val message = event.message val newConversations = conversations.value.toMutableList() @@ -411,28 +400,14 @@ class ConversationsViewModelImpl( loadConversationsByIdUseCase(peerIds = listOf(message.peerId)) .listenValue(viewModelScope) { state -> state.processState( - error = { error -> - - }, + error = {}, success = { response -> val conversation = (response.firstOrNull() ?: return@listenValue) .copy(lastMessage = message) - // TODO: 22-Dec-24, Danil Nikolaev: handle interactions and pinned state - newConversations.add(pinnedConversationsCount.value, conversation) conversations.update { newConversations } - - screenState.setValue { old -> - old.copy( - conversations = newConversations.map { - it.asPresentation( - resources = resources, - useContactName = useContactNames - ) - } - ) - } + sortConversations() } ) } @@ -487,7 +462,7 @@ class ConversationsViewModelImpl( } } - private fun handleEditedMessage(event: LongPollEvent.VkMessageEditEvent) { + private fun handleEditedMessage(event: LongPollParsedEvent.MessageEdited) { val message = event.message val newConversations = conversations.value.toMutableList() @@ -516,7 +491,7 @@ class ConversationsViewModelImpl( } } - private fun handleReadIncomingMessage(event: LongPollEvent.VkMessageReadIncomingEvent) { + private fun handleReadIncomingMessage(event: LongPollParsedEvent.IncomingMessageRead) { val newConversations = conversations.value.toMutableList() val conversationIndex = @@ -546,7 +521,7 @@ class ConversationsViewModelImpl( } } - private fun handleReadOutgoingMessage(event: LongPollEvent.VkMessageReadOutgoingEvent) { + private fun handleReadOutgoingMessage(event: LongPollParsedEvent.OutgoingMessageRead) { val newConversations = conversations.value.toMutableList() val conversationIndex = @@ -575,47 +550,113 @@ class ConversationsViewModelImpl( } } - private fun handlePinStateChanged(event: LongPollEvent.VkConversationPinStateChangedEvent) { - var pinnedCount = pinnedConversationsCount.value + private fun handleChatMajorChanged(event: LongPollParsedEvent.ChatMajorChanged) { val newConversations = conversations.value.toMutableList() - val conversationIndex = newConversations.indexOfFirstOrNull { it.id == event.peerId } if (conversationIndex == null) { // диалога нет в списке // pizdets } else { - val pin = event.majorId > 0 + newConversations[conversationIndex] = + newConversations[conversationIndex].copy(majorId = event.majorId) - val conversation = newConversations[conversationIndex].copy(majorId = event.majorId) + conversations.setValue { newConversations } + screenState.setValue { old -> + old.copy( + conversations = newConversations.map { + it.asPresentation( + resources = resources, + useContactName = useContactNames + ) + } + ) + } + sortConversations() + } + } - newConversations.removeAt(conversationIndex) + private fun handleChatMinorChanged(event: LongPollParsedEvent.ChatMinorChanged) { + val newConversations = conversations.value.toMutableList() + val conversationIndex = + newConversations.indexOfFirstOrNull { it.id == event.peerId } - if (pin) { - newConversations.add(0, conversation) - } else { - pinnedCount -= 1 + if (conversationIndex == null) { // диалога нет в списке + // pizdets + } else { + newConversations[conversationIndex] = + newConversations[conversationIndex].copy(minorId = event.minorId) - newConversations.add(conversation) + conversations.setValue { newConversations } + screenState.setValue { old -> + old.copy( + conversations = newConversations.map { + it.asPresentation( + resources = resources, + useContactName = useContactNames + ) + } + ) + } + sortConversations() + } + } - val pinnedSubList = newConversations.filter(VkConversation::isPinned) - val unpinnedSubList = newConversations - .filterNot(VkConversation::isPinned) - .sortedByDescending { it.lastMessage?.date } + private fun sortConversations() { + val newConversations = conversations.value.toMutableList() + val pinnedConversations = newConversations + .filter(VkConversation::isPinned) + .sortedWith { c1, c2 -> + val diff = c2.majorId - c1.majorId - newConversations.clear() - newConversations += pinnedSubList + unpinnedSubList + if (diff == 0) { + c2.minorId - c1.minorId + } else { + diff + } } - conversations.update { newConversations } + newConversations.removeAll(pinnedConversations) + newConversations.sortWith { c1, c2 -> + (c2.lastMessage?.date ?: 0) - (c1.lastMessage?.date ?: 0) + } - screenState.setValue { old -> - old.copy(conversations = newConversations.map { + newConversations.addAll(0, pinnedConversations) + + conversations.update { newConversations } + screenState.setValue { old -> + old.copy( + conversations = newConversations.map { it.asPresentation( resources = resources, useContactName = useContactNames ) - }) + } + ) + } + } + + private fun handleChatClearing(event: LongPollParsedEvent.ChatCleared) { + val newConversations = conversations.value.toMutableList() + + val conversationIndex = newConversations.indexOfFirstOrNull { it.id == event.peerId } + + if (conversationIndex == null) { // диалога нет в списке + // pizdets + } else { + newConversations.removeAt(conversationIndex) + + conversations.setValue { newConversations } + + screenState.setValue { old -> + old.copy( + conversations = newConversations.map { + it.asPresentation( + resources = resources, + useContactName = useContactNames + ) + } + ) } } } @@ -627,9 +668,9 @@ class ConversationsViewModelImpl( val timerJob: Job ) - private object NewInteractionException : CancellationException() + private class NewInteractionException : CancellationException() - private fun handleInteraction(event: LongPollEvent.Interaction) { + private fun handleInteraction(event: LongPollParsedEvent.Interaction) { val interactionType = event.interactionType val peerId = event.peerId val userIds = event.userIds @@ -638,9 +679,7 @@ class ConversationsViewModelImpl( val conversationAndIndex = newConversations.findWithIndex { it.id == peerId } - if (conversationAndIndex == null) { // диалога нет в списке - // pizdets - } else { + if (conversationAndIndex != null) { newConversations[conversationAndIndex.first] = conversationAndIndex.second.copy( interactionType = interactionType.value, @@ -662,7 +701,7 @@ class ConversationsViewModelImpl( interactionsTimers[peerId]?.let { interactionJob -> if (interactionJob.interactionType == interactionType) { - interactionJob.timerJob.cancel(NewInteractionException) + interactionJob.timerJob.cancel(NewInteractionException()) } } @@ -721,9 +760,7 @@ class ConversationsViewModelImpl( startMessageId = startMessageId ).listenValue(viewModelScope) { state -> state.processState( - error = { error -> - - }, + error = {}, success = { val newConversations = conversations.value.toMutableList() val conversationIndex = diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt index 27f046df..345830e5 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt @@ -32,7 +32,7 @@ import dev.meloda.fast.messageshistory.util.extractAvatar import dev.meloda.fast.messageshistory.util.extractTitle import dev.meloda.fast.messageshistory.util.findMessageById import dev.meloda.fast.model.BaseError -import dev.meloda.fast.model.LongPollEvent +import dev.meloda.fast.model.LongPollParsedEvent import dev.meloda.fast.model.api.domain.VkAttachment import dev.meloda.fast.model.api.domain.VkMessage import kotlinx.coroutines.Dispatchers @@ -158,7 +158,7 @@ class MessagesHistoryViewModelImpl( loadMessagesHistory() } - private fun handleNewMessage(event: LongPollEvent.VkMessageNewEvent) { + private fun handleNewMessage(event: LongPollParsedEvent.NewMessage) { val message = event.message Log.d("MessagesHistoryViewModel", "handleNewMessage: $message") @@ -200,7 +200,7 @@ class MessagesHistoryViewModelImpl( screenState.setValue { old -> old.copy(messages = newMessages) } } - private fun handleEditedMessage(event: LongPollEvent.VkMessageEditEvent) { + private fun handleEditedMessage(event: LongPollParsedEvent.MessageEdited) { val message = event.message if (message.peerId != screenState.value.conversationId) return @@ -223,11 +223,41 @@ class MessagesHistoryViewModelImpl( } } - private fun handleReadIncomingEvent(event: LongPollEvent.VkMessageReadIncomingEvent) { + private fun handleReadIncomingEvent(event: LongPollParsedEvent.IncomingMessageRead) { + if (event.peerId != screenState.value.conversationId) return + val messages = messages.value + val messageIndex = + messages.indexOfFirstOrNull { it.id == event.messageId } + + if (messageIndex == null) { // диалога нет в списке + // pizdets + } else { + val newConversation = screenState.value.conversation.copy( + inRead = event.messageId + ) + + val uiMessages = messages.mapIndexed { index, item -> + item.asPresentation( + resourceProvider = resourceProvider, + showName = false, + prevMessage = messages.getOrNull(index + 1), + nextMessage = messages.getOrNull(index - 1), + showTimeInActionMessages = userSettings.showTimeInActionMessages.value, + conversation = newConversation + ) + } + + screenState.setValue { old -> + old.copy( + conversation = newConversation, + messages = uiMessages, + ) + } + } } - private fun handleReadOutgoingEvent(event: LongPollEvent.VkMessageReadOutgoingEvent) { + private fun handleReadOutgoingEvent(event: LongPollParsedEvent.OutgoingMessageRead) { if (event.peerId != screenState.value.conversationId) return val messages = messages.value