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 e28b339e..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,17 +29,19 @@ 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>> = mutableMapOf() + private val listenersMap: MutableMap>> = + mutableMapOf() fun parseNextUpdate(event: List) { val eventId = event.first().asInt() @@ -49,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, @@ -74,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() @@ -82,20 +100,18 @@ 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 ) - } + ) } } } @@ -111,11 +127,11 @@ class LongPollUpdatesParser( val archiveUnreadUnmutedCount = event[8].asInt() val archiveMentionsCount = event[9].asInt() - listenersMap[ApiEvent.UNREAD_COUNT_UPDATE]?.let { listeners -> + listenersMap[LongPollEvent.UNREAD_COUNTER_UPDATE]?.let { listeners -> listeners.forEach { vkEventCallback -> - (vkEventCallback as VkEventCallback) + (vkEventCallback as VkEventCallback) .onEvent( - LongPollEvent.UnreadCounter( + LongPollParsedEvent.UnreadCounter( unread = unreadCount, unreadUnmuted = unreadUnmutedCount, showOnlyMuted = showOnlyMuted, @@ -129,31 +145,182 @@ class LongPollUpdatesParser( } } - private fun parseConversationPinStateChanged(eventType: ApiEvent, event: List) { + private fun parseMessageSetFlags(eventType: ApiEvent, event: List) { Log.d("LongPollUpdatesParser", "$eventType: $event") - val peerId = event[1].asInt() - val majorId = event[2].asInt() + val messageId = event[1].asInt() + val flags = event[2].asInt() + val peerId = event[3].asInt() - listenersMap[ApiEvent.PIN_UNPIN_CONVERSATION]?.let { listeners -> - listeners.forEach { vkEventCallback -> - (vkEventCallback as VkEventCallback) - .onEvent( - LongPollEvent.VkConversationPinStateChangedEvent( - peerId = peerId, - majorId = majorId - ) + 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 parseMessageSetFlags(eventType: ApiEvent, event: List) { - Log.d("LongPollUpdatesParser", "$eventType: $event") - } - 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) { @@ -161,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)) } } } @@ -182,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)) } } } @@ -206,11 +361,11 @@ class LongPollUpdatesParser( val messageId = event[2].asInt() val unreadCount = event[3].asInt() - listenersMap[ApiEvent.MESSAGE_READ_INCOMING]?.let { listeners -> + listenersMap[LongPollEvent.INCOMING_MESSAGE_READ]?.let { listeners -> listeners.map { vkEventCallback -> - (vkEventCallback as VkEventCallback) + (vkEventCallback as VkEventCallback) .onEvent( - LongPollEvent.VkMessageReadIncomingEvent( + LongPollParsedEvent.IncomingMessageRead( peerId = peerId, messageId = messageId, unreadCount = unreadCount @@ -226,11 +381,11 @@ class LongPollUpdatesParser( val messageId = event[2].asInt() val unreadCount = event[3].asInt() - listenersMap[ApiEvent.MESSAGE_READ_OUTGOING]?.let { listeners -> + listenersMap[LongPollEvent.OUTGOING_MESSAGE_READ]?.let { listeners -> listeners.map { vkEventCallback -> - (vkEventCallback as VkEventCallback) + (vkEventCallback as VkEventCallback) .onEvent( - LongPollEvent.VkMessageReadOutgoingEvent( + LongPollParsedEvent.OutgoingMessageRead( peerId = peerId, messageId = messageId, unreadCount = unreadCount @@ -240,14 +395,72 @@ class LongPollUpdatesParser( } } - 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), @@ -256,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 } @@ -267,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 3cd1806c..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,45 +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 - - data class UnreadCounter( - val unread: Int, - val unreadUnmuted: Int, - val showOnlyMuted: Boolean, - val business: Int, - val archive: Int, - val archiveUnmuted: Int, - val archiveMentions: 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..0e4a13ac 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() } @@ -383,11 +385,11 @@ class ConversationsViewModelImpl( }, 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 +402,7 @@ class ConversationsViewModelImpl( } } - private fun handleNewMessage(event: LongPollEvent.VkMessageNewEvent) { + private fun handleNewMessage(event: LongPollParsedEvent.NewMessage) { val message = event.message val newConversations = conversations.value.toMutableList() @@ -487,7 +489,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 +518,7 @@ class ConversationsViewModelImpl( } } - private fun handleReadIncomingMessage(event: LongPollEvent.VkMessageReadIncomingEvent) { + private fun handleReadIncomingMessage(event: LongPollParsedEvent.IncomingMessageRead) { val newConversations = conversations.value.toMutableList() val conversationIndex = @@ -546,7 +548,7 @@ class ConversationsViewModelImpl( } } - private fun handleReadOutgoingMessage(event: LongPollEvent.VkMessageReadOutgoingEvent) { + private fun handleReadOutgoingMessage(event: LongPollParsedEvent.OutgoingMessageRead) { val newConversations = conversations.value.toMutableList() val conversationIndex = @@ -575,47 +577,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 + ) + } + ) } } } @@ -629,7 +697,7 @@ class ConversationsViewModelImpl( private object 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 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 b08e199e..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,7 +223,7 @@ 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 @@ -257,7 +257,7 @@ class MessagesHistoryViewModelImpl( } } - private fun handleReadOutgoingEvent(event: LongPollEvent.VkMessageReadOutgoingEvent) { + private fun handleReadOutgoingEvent(event: LongPollParsedEvent.OutgoingMessageRead) { if (event.peerId != screenState.value.conversationId) return val messages = messages.value