From fc3b3cfcb3304cb126f7afac4a86e1543f119338 Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Sat, 30 May 2026 17:17:32 +0300 Subject: [PATCH] refactor(longpoll): route parsed long poll events through dedicated handler and persist message/conversation state updates --- .../service/longpolling/LongPollingService.kt | 9 +- .../service/longpolling/di/LongPollModule.kt | 2 + .../12.json | 425 +++++++++++++++ .../dev/meloda/fast/database/CacheDatabase.kt | 2 +- .../dev/meloda/fast/database/dao/ConvoDao.kt | 25 +- .../meloda/fast/database/dao/MessageDao.kt | 11 +- .../fast/domain/LongPollEventsHandler.kt | 164 ++++++ .../fast/domain/LongPollUpdatesParser.kt | 504 +++++++++--------- .../fast/model/api/data/VkMessageData.kt | 3 +- .../model/api/data/VkPinnedMessageData.kt | 1 + .../meloda/fast/model/api/domain/VkMessage.kt | 6 +- .../fast/model/database/VkMessageEntity.kt | 9 +- .../dev/meloda/fast/convos/ConvosViewModel.kt | 1 + .../MessagesHistoryViewModelImpl.kt | 1 + 14 files changed, 889 insertions(+), 274 deletions(-) create mode 100644 core/database/schemas/dev.meloda.fast.database.CacheDatabase/12.json create mode 100644 core/domain/src/main/kotlin/dev/meloda/fast/domain/LongPollEventsHandler.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 2bb3684e..9d452862 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 @@ -19,6 +19,7 @@ import dev.meloda.fast.common.model.LongPollState import dev.meloda.fast.data.UserConfig import dev.meloda.fast.data.processState import dev.meloda.fast.datastore.AppSettings +import dev.meloda.fast.domain.LongPollEventsHandler import dev.meloda.fast.domain.LongPollUpdatesParser import dev.meloda.fast.domain.LongPollUseCase import dev.meloda.fast.model.api.data.LongPollUpdates @@ -56,6 +57,7 @@ class LongPollingService : Service() { private val longPollUseCase: LongPollUseCase by inject() private val updatesParser: LongPollUpdatesParser by inject() + private val eventsHandler: LongPollEventsHandler by inject() private var currentJob: Job? = null @@ -193,7 +195,7 @@ class LongPollingService : Service() { if (updates == null) { failCount++ } else { - updates.forEach(updatesParser::parseNextUpdate) + parseUpdates(updates) } lastUpdatesResponse = getUpdatesResponse(serverInfo.copy(ts = newTs)) @@ -246,6 +248,11 @@ class LongPollingService : Service() { } } + private suspend fun parseUpdates(updates: List>) { + val parsedUpdates = updates.flatMap { updatesParser.parseNextUpdate(it) } + eventsHandler.handleEvents(parsedUpdates) + } + private fun handleError(throwable: Throwable) { Log.e(TAG, "error: $throwable") diff --git a/app/src/main/kotlin/dev/meloda/fast/service/longpolling/di/LongPollModule.kt b/app/src/main/kotlin/dev/meloda/fast/service/longpolling/di/LongPollModule.kt index 96546c83..d1227a1a 100644 --- a/app/src/main/kotlin/dev/meloda/fast/service/longpolling/di/LongPollModule.kt +++ b/app/src/main/kotlin/dev/meloda/fast/service/longpolling/di/LongPollModule.kt @@ -1,5 +1,6 @@ package dev.meloda.fast.service.longpolling.di +import dev.meloda.fast.domain.LongPollEventsHandler import dev.meloda.fast.domain.LongPollUpdatesParser import dev.meloda.fast.domain.LongPollUseCase import dev.meloda.fast.domain.LongPollUseCaseImpl @@ -10,4 +11,5 @@ import org.koin.dsl.module val longPollModule = module { singleOf(::LongPollUseCaseImpl) bind LongPollUseCase::class singleOf(::LongPollUpdatesParser) + singleOf(::LongPollEventsHandler) } diff --git a/core/database/schemas/dev.meloda.fast.database.CacheDatabase/12.json b/core/database/schemas/dev.meloda.fast.database.CacheDatabase/12.json new file mode 100644 index 00000000..1f1bdbea --- /dev/null +++ b/core/database/schemas/dev.meloda.fast.database.CacheDatabase/12.json @@ -0,0 +1,425 @@ +{ + "formatVersion": 1, + "database": { + "version": 12, + "identityHash": "5eca3b3da167aaf7e772977a1f4e56e2", + "entities": [ + { + "tableName": "users", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `firstName` TEXT NOT NULL, `lastName` TEXT NOT NULL, `isOnline` INTEGER NOT NULL, `isOnlineMobile` INTEGER NOT NULL, `onlineAppId` INTEGER, `lastSeen` INTEGER, `lastSeenStatus` TEXT, `birthday` TEXT, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, `photo400Orig` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "firstName", + "columnName": "firstName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastName", + "columnName": "lastName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isOnline", + "columnName": "isOnline", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isOnlineMobile", + "columnName": "isOnlineMobile", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onlineAppId", + "columnName": "onlineAppId", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastSeen", + "columnName": "lastSeen", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastSeenStatus", + "columnName": "lastSeenStatus", + "affinity": "TEXT" + }, + { + "fieldPath": "birthday", + "columnName": "birthday", + "affinity": "TEXT" + }, + { + "fieldPath": "photo50", + "columnName": "photo50", + "affinity": "TEXT" + }, + { + "fieldPath": "photo100", + "columnName": "photo100", + "affinity": "TEXT" + }, + { + "fieldPath": "photo200", + "columnName": "photo200", + "affinity": "TEXT" + }, + { + "fieldPath": "photo400Orig", + "columnName": "photo400Orig", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `screenName` TEXT NOT NULL, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, `membersCount` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "screenName", + "columnName": "screenName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "photo50", + "columnName": "photo50", + "affinity": "TEXT" + }, + { + "fieldPath": "photo100", + "columnName": "photo100", + "affinity": "TEXT" + }, + { + "fieldPath": "photo200", + "columnName": "photo200", + "affinity": "TEXT" + }, + { + "fieldPath": "membersCount", + "columnName": "membersCount", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `cmId` INTEGER NOT NULL, `text` TEXT, `isOut` INTEGER NOT NULL, `peerId` INTEGER NOT NULL, `fromId` INTEGER NOT NULL, `date` INTEGER NOT NULL, `randomId` INTEGER NOT NULL, `action` TEXT, `actionMemberId` INTEGER, `actionText` TEXT, `actionCmId` INTEGER, `actionMessage` TEXT, `updateTime` INTEGER, `isImportant` INTEGER NOT NULL, `forwardIds` TEXT, `attachments` TEXT, `replyMessageId` INTEGER, `geoType` TEXT, `pinnedAt` INTEGER, `isPinned` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `isSpam` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cmId", + "columnName": "cmId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT" + }, + { + "fieldPath": "isOut", + "columnName": "isOut", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "peerId", + "columnName": "peerId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fromId", + "columnName": "fromId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "randomId", + "columnName": "randomId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "action", + "columnName": "action", + "affinity": "TEXT" + }, + { + "fieldPath": "actionMemberId", + "columnName": "actionMemberId", + "affinity": "INTEGER" + }, + { + "fieldPath": "actionText", + "columnName": "actionText", + "affinity": "TEXT" + }, + { + "fieldPath": "actionCmId", + "columnName": "actionCmId", + "affinity": "INTEGER" + }, + { + "fieldPath": "actionMessage", + "columnName": "actionMessage", + "affinity": "TEXT" + }, + { + "fieldPath": "updateTime", + "columnName": "updateTime", + "affinity": "INTEGER" + }, + { + "fieldPath": "isImportant", + "columnName": "isImportant", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "forwardIds", + "columnName": "forwardIds", + "affinity": "TEXT" + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT" + }, + { + "fieldPath": "replyMessageId", + "columnName": "replyMessageId", + "affinity": "INTEGER" + }, + { + "fieldPath": "geoType", + "columnName": "geoType", + "affinity": "TEXT" + }, + { + "fieldPath": "pinnedAt", + "columnName": "pinnedAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "isPinned", + "columnName": "isPinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDeleted", + "columnName": "isDeleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isSpam", + "columnName": "isSpam", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "convos", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `localId` INTEGER NOT NULL, `ownerId` INTEGER, `title` TEXT, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, `isPhantom` INTEGER NOT NULL, `lastCmId` INTEGER NOT NULL, `inReadCmId` INTEGER NOT NULL, `outReadCmId` INTEGER NOT NULL, `inRead` INTEGER NOT NULL, `outRead` INTEGER NOT NULL, `lastMessageId` INTEGER, `unreadCount` INTEGER NOT NULL, `membersCount` INTEGER, `canChangePin` INTEGER NOT NULL, `canChangeInfo` INTEGER NOT NULL, `majorId` INTEGER NOT NULL, `minorId` INTEGER NOT NULL, `pinnedMessageId` INTEGER, `peerType` TEXT NOT NULL, `isArchived` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localId", + "columnName": "localId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ownerId", + "columnName": "ownerId", + "affinity": "INTEGER" + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT" + }, + { + "fieldPath": "photo50", + "columnName": "photo50", + "affinity": "TEXT" + }, + { + "fieldPath": "photo100", + "columnName": "photo100", + "affinity": "TEXT" + }, + { + "fieldPath": "photo200", + "columnName": "photo200", + "affinity": "TEXT" + }, + { + "fieldPath": "isPhantom", + "columnName": "isPhantom", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCmId", + "columnName": "lastCmId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReadCmId", + "columnName": "inReadCmId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "outReadCmId", + "columnName": "outReadCmId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inRead", + "columnName": "inRead", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "outRead", + "columnName": "outRead", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastMessageId", + "columnName": "lastMessageId", + "affinity": "INTEGER" + }, + { + "fieldPath": "unreadCount", + "columnName": "unreadCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "membersCount", + "columnName": "membersCount", + "affinity": "INTEGER" + }, + { + "fieldPath": "canChangePin", + "columnName": "canChangePin", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canChangeInfo", + "columnName": "canChangeInfo", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "majorId", + "columnName": "majorId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minorId", + "columnName": "minorId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinnedMessageId", + "columnName": "pinnedMessageId", + "affinity": "INTEGER" + }, + { + "fieldPath": "peerType", + "columnName": "peerType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isArchived", + "columnName": "isArchived", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5eca3b3da167aaf7e772977a1f4e56e2')" + ] + } +} \ No newline at end of file diff --git a/core/database/src/main/kotlin/dev/meloda/fast/database/CacheDatabase.kt b/core/database/src/main/kotlin/dev/meloda/fast/database/CacheDatabase.kt index 5dbec1e6..a9af18fa 100644 --- a/core/database/src/main/kotlin/dev/meloda/fast/database/CacheDatabase.kt +++ b/core/database/src/main/kotlin/dev/meloda/fast/database/CacheDatabase.kt @@ -21,7 +21,7 @@ import dev.meloda.fast.model.database.VkUserEntity VkConvoEntity::class ], - version = 11 + version = 12 ) @TypeConverters(Converters::class) abstract class CacheDatabase : RoomDatabase() { diff --git a/core/database/src/main/kotlin/dev/meloda/fast/database/dao/ConvoDao.kt b/core/database/src/main/kotlin/dev/meloda/fast/database/dao/ConvoDao.kt index 40494ba1..0ec13919 100644 --- a/core/database/src/main/kotlin/dev/meloda/fast/database/dao/ConvoDao.kt +++ b/core/database/src/main/kotlin/dev/meloda/fast/database/dao/ConvoDao.kt @@ -13,7 +13,7 @@ abstract class ConvoDao : EntityDao { abstract suspend fun getAll(): List @Query("SELECT * FROM convos WHERE id IN (:ids)") - abstract suspend fun getAllByIds(ids: List): List + abstract suspend fun getAllByIds(ids: List): List @Query("SELECT * FROM convos WHERE id IS (:id)") abstract suspend fun getById(id: Long): VkConvoEntity? @@ -23,8 +23,23 @@ abstract class ConvoDao : EntityDao { abstract suspend fun getByIdWithMessage(id: Long): ConvoWithMessage? @Query("DELETE FROM convos WHERE rowid IN (:ids)") - abstract suspend fun deleteByIds(ids: List): Int + abstract suspend fun deleteByIds(ids: List): Int + + @Query("UPDATE convos SET inReadCmId = :cmId, unreadCount = :unreadCount WHERE id = :convoId") + abstract suspend fun updateReadIncoming(convoId: Long, cmId: Long, unreadCount: Int): Int + + @Query("UPDATE convos SET outReadCmId = :cmId, unreadCount = :unreadCount WHERE id = :convoId") + abstract suspend fun updateReadOutgoing(convoId: Long, cmId: Long, unreadCount: Int): Int + + @Query("UPDATE convos SET isArchived = :isArchived WHERE id = :convoId") + abstract suspend fun updateIsArchived(convoId: Long, isArchived: Boolean): Int + + @Query("UPDATE convos SET majorId = :majorId WHERE id = :convoId") + abstract suspend fun updateMajorId(convoId: Long, majorId: Int): Int + + @Query("UPDATE convos SET minorId = :minorId WHERE id = :convoId") + abstract suspend fun updateMinorId(convoId: Long, minorId: Int): Int + + @Query("UPDATE convos SET lastCmId = :cmId WHERE id = :convoId") + abstract suspend fun updateLastCmId(convoId: Long, cmId: Long): Int } - - - diff --git a/core/database/src/main/kotlin/dev/meloda/fast/database/dao/MessageDao.kt b/core/database/src/main/kotlin/dev/meloda/fast/database/dao/MessageDao.kt index 4daecc9b..61510dea 100644 --- a/core/database/src/main/kotlin/dev/meloda/fast/database/dao/MessageDao.kt +++ b/core/database/src/main/kotlin/dev/meloda/fast/database/dao/MessageDao.kt @@ -7,7 +7,7 @@ import dev.meloda.fast.model.database.VkMessageEntity @Dao abstract class MessageDao : EntityDao { - @Query("SELECT * FROM messages") + @Query("SELECT * FROM messages WHERE isDeleted = 0 AND isSpam = 0") abstract suspend fun getAll(): List @Query("SELECT * FROM messages WHERE peerId IS (:convoId)") @@ -21,4 +21,13 @@ abstract class MessageDao : EntityDao { @Query("DELETE FROM messages WHERE id IN (:ids)") abstract suspend fun deleteByIds(ids: List): Int + + @Query("UPDATE messages SET isDeleted = :isDeleted WHERE peerId = :convoId AND cmId = :cmId") + abstract suspend fun markAsDeleted(convoId: Long, cmId: Long, isDeleted: Boolean): Int + + @Query("UPDATE messages SET isImportant = :isImportant WHERE peerId = :convoId AND cmId = :cmId") + abstract suspend fun markAsImportant(convoId: Long, cmId: Long, isImportant: Boolean): Int + + @Query("UPDATE messages SET isSpam = :isSpam WHERE peerId = :convoId AND cmId = :cmId") + abstract suspend fun markAsSpam(convoId: Long, cmId: Long, isSpam: Boolean): Int } diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/LongPollEventsHandler.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/LongPollEventsHandler.kt new file mode 100644 index 00000000..837b5803 --- /dev/null +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/LongPollEventsHandler.kt @@ -0,0 +1,164 @@ +package dev.meloda.fast.domain + +import android.util.Log +import dev.meloda.fast.database.dao.ConvoDao +import dev.meloda.fast.database.dao.MessageDao +import dev.meloda.fast.model.LongPollParsedEvent +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlin.coroutines.CoroutineContext + +class LongPollEventsHandler( + private val convoUseCase: ConvoUseCase, + private val messagesUseCase: MessagesUseCase, + private val convoDao: ConvoDao, + private val messageDao: MessageDao, +) { + private val job = SupervisorJob() + + 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) + + suspend fun handleEvents(events: List) { + events.forEach { handleNextEvent(it) } + } + + private suspend fun handleNextEvent(event: LongPollParsedEvent) { + when (event) { + is LongPollParsedEvent.AudioMessageListened -> { + + } + + is LongPollParsedEvent.ChatArchived -> { + val affectedRows = convoDao.updateIsArchived( + convoId = event.convo.id, + isArchived = event.convo.isArchived + ) + + Log.d( + "LongPollEventsHandler", + "isArchived ${event.convo.isArchived}: updated $affectedRows rows." + ) + } + + is LongPollParsedEvent.ChatCleared -> { + val affectedRows = convoDao.updateLastCmId( + convoId = event.peerId, + cmId = event.toCmId + ) + + Log.d("LongPollEventsHandler", "updateLastCmId: updated $affectedRows rows.") + } + + is LongPollParsedEvent.ChatMajorChanged -> { + val affectedRows = convoDao.updateMajorId( + convoId = event.peerId, + majorId = event.majorId + ) + + Log.d("LongPollEventsHandler", "updateMajorId: updated $affectedRows rows.") + } + + is LongPollParsedEvent.ChatMinorChanged -> { + val affectedRows = convoDao.updateMinorId( + convoId = event.peerId, + minorId = event.minorId + ) + + Log.d("LongPollEventsHandler", "updateMinorId: updated $affectedRows rows.") + } + + is LongPollParsedEvent.Interaction -> { + + } + + is LongPollParsedEvent.MessageCacheClear -> { + messagesUseCase.storeMessage(event.message) + } + + is LongPollParsedEvent.MessageDeleted -> { + val affectedRows = messageDao.markAsDeleted( + convoId = event.peerId, + cmId = event.cmId, + isDeleted = true + ) + + Log.d("LongPollEventsHandler", "markDeleted: updated $affectedRows rows.") + } + + is LongPollParsedEvent.MessageEdited -> { + messagesUseCase.storeMessage(event.message) + } + + is LongPollParsedEvent.MessageMarkedAsImportant -> { + val affectedRows = messageDao.markAsImportant( + convoId = event.peerId, + cmId = event.cmId, + isImportant = event.marked + ) + + Log.d("LongPollEventsHandler", "markImportant: updated $affectedRows rows.") + } + + is LongPollParsedEvent.MessageMarkedAsNotSpam -> { + messagesUseCase.storeMessage(event.message) + } + + is LongPollParsedEvent.MessageMarkedAsSpam -> { + val affectedRows = messageDao.markAsSpam( + convoId = event.peerId, + cmId = event.cmId, + isSpam = true + ) + + Log.d("LongPollEventsHandler", "markSpam: updated $affectedRows rows.") + } + + is LongPollParsedEvent.MessageRestored -> { + messagesUseCase.storeMessage(event.message) + } + + is LongPollParsedEvent.MessageUpdated -> { + messagesUseCase.storeMessage(event.message) + } + + is LongPollParsedEvent.NewMessage -> { + messagesUseCase.storeMessage(event.message) + } + + is LongPollParsedEvent.IncomingMessageRead -> { + val affectedRows = convoDao.updateReadIncoming( + convoId = event.peerId, + cmId = event.cmId, + unreadCount = event.unreadCount + ) + + Log.d("LongPollEventsHandler", "inMessageRead: updated $affectedRows rows.") + } + + is LongPollParsedEvent.OutgoingMessageRead -> { + val affectedRows = convoDao.updateReadOutgoing( + convoId = event.peerId, + cmId = event.cmId, + unreadCount = event.unreadCount + ) + + Log.d("LongPollEventsHandler", "outMessageRead: updated $affectedRows rows.") + } + + is LongPollParsedEvent.UnreadCounter -> { + + } + } + } +} 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 43497f23..2e044963 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 @@ -22,10 +22,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.async import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import kotlinx.coroutines.suspendCancellableCoroutine import kotlin.coroutines.CoroutineContext import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine class LongPollUpdatesParser( private val convoUseCase: ConvoUseCase, @@ -47,11 +46,14 @@ class LongPollUpdatesParser( private val listenersMap: MutableMap>> = mutableMapOf() - fun parseNextUpdate(event: List) { + suspend fun parseNextUpdate(event: List): List { val eventId = event.first().asInt() - when (val eventType = ApiEvent.parseOrNull(eventId)) { - null -> Log.d("LongPollUpdatesParser", "parseNextUpdate: unknownEvent: $event") + return when (val eventType = ApiEvent.parseOrNull(eventId)) { + null -> { + Log.d("LongPollUpdatesParser", "parseNextUpdate: unknownEvent: $event") + emptyList() + } ApiEvent.MESSAGE_SET_FLAGS -> parseMessageSetFlags(eventType, event) ApiEvent.MESSAGE_CLEAR_FLAGS -> parseMessageClearFlags(eventType, event) @@ -77,7 +79,10 @@ class LongPollUpdatesParser( } } - private fun parseMessageSetFlags(eventType: ApiEvent, event: List) { + private fun parseMessageSetFlags( + eventType: ApiEvent, + event: List + ): List { Log.d("LongPollUpdatesParser", "$eventType: $event") val cmId = event[1].asLong() @@ -97,12 +102,8 @@ class LongPollUpdatesParser( ) eventsToSend += eventToSend - listenersMap[LongPollEvent.MARKED_AS_IMPORTANT]?.let { listeners -> - listeners.map { vkEventCallback -> - (vkEventCallback as? VkEventCallback) - ?.onEvent(eventToSend) - } - } + listenersMap[LongPollEvent.MARKED_AS_IMPORTANT] + ?.forEach { it.onEvent(eventToSend) } } MessageFlags.SPAM -> { @@ -111,13 +112,7 @@ class LongPollUpdatesParser( cmId = cmId ) eventsToSend += eventToSend - - listenersMap[LongPollEvent.MARKED_AS_SPAM]?.let { listeners -> - listeners.map { vkEventCallback -> - (vkEventCallback as? VkEventCallback) - ?.onEvent(eventToSend) - } - } + listenersMap[LongPollEvent.MARKED_AS_SPAM]?.forEach { it.onEvent(eventToSend) } } MessageFlags.DELETED -> { @@ -136,13 +131,7 @@ class LongPollUpdatesParser( ) } eventsToSend += eventToSend - - listenersMap[LongPollEvent.MESSAGE_DELETED]?.let { listeners -> - listeners.map { vkEventCallback -> - (vkEventCallback as? VkEventCallback) - ?.onEvent(eventToSend) - } - } + listenersMap[LongPollEvent.MESSAGE_DELETED]?.forEach { it.onEvent(eventToSend) } } MessageFlags.AUDIO_LISTENED -> { @@ -152,28 +141,36 @@ class LongPollUpdatesParser( ) eventsToSend += eventToSend - listenersMap[LongPollEvent.AUDIO_MESSAGE_LISTENED]?.let { listeners -> - listeners.map { vkEventCallback -> - (vkEventCallback as? VkEventCallback) - ?.onEvent(eventToSend) - } - } + listenersMap[LongPollEvent.AUDIO_MESSAGE_LISTENED] + ?.forEach { it.onEvent(eventToSend) } } - else -> Unit + MessageFlags.UNREAD -> Unit + MessageFlags.OUTGOING -> Unit + MessageFlags.FROM_GROUP_CHAT -> Unit + MessageFlags.CANCEL_SPAM -> Unit + MessageFlags.DELETED_FOR_ALL -> Unit + MessageFlags.DO_NOT_SHOW_NOTIFICATION -> Unit + MessageFlags.MESSAGE_WITH_REPLY -> Unit + MessageFlags.REACTION -> Unit } } eventsToSend.forEach { eventToSend -> listenersMap[LongPollEvent.MESSAGE_SET_FLAGS]?.let { listeners -> - listeners.map { vkEventCallback -> - (vkEventCallback as? VkEventCallback)?.onEvent(eventToSend) + listeners.forEach { vkEventCallback -> + vkEventCallback.onEvent(eventToSend) } } } + + return eventsToSend } - private fun parseMessageClearFlags(eventType: ApiEvent, event: List) { + private suspend fun parseMessageClearFlags( + eventType: ApiEvent, + event: List + ): List = suspendCancellableCoroutine { continuation -> Log.d("LongPollUpdatesParser", "$eventType: $event") val cmId = event[1].asLong() @@ -184,7 +181,9 @@ class LongPollUpdatesParser( val parsedFlags = MessageFlags.parse(flags) - coroutineScope.launch { + coroutineScope.launch(Dispatchers.IO) { + val message = loadMessage(peerId = peerId, cmId = cmId) + parsedFlags.forEach { flag -> when (flag) { MessageFlags.IMPORTANT -> { @@ -195,81 +194,65 @@ class LongPollUpdatesParser( ) eventsToSend += eventToSend - listenersMap[LongPollEvent.MARKED_AS_IMPORTANT]?.let { listeners -> - listeners.map { vkEventCallback -> - (vkEventCallback as? VkEventCallback) - ?.onEvent(eventToSend) - } - } + listenersMap[LongPollEvent.MARKED_AS_IMPORTANT] + ?.forEach { it.onEvent(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 + if (message != null) { + val eventToSend = + LongPollParsedEvent.MessageMarkedAsNotSpam(message = message) + eventsToSend += eventToSend - listenersMap[LongPollEvent.MARKED_AS_NOT_SPAM]?.let { listeners -> - listeners.map { vkEventCallback -> - (vkEventCallback as? VkEventCallback) - ?.onEvent(eventToSend) - } - } - } + listenersMap[LongPollEvent.MARKED_AS_NOT_SPAM] + ?.forEach { it.onEvent(eventToSend) } } } } MessageFlags.DELETED -> { - withContext(Dispatchers.IO) { - val message = loadMessage( - peerId = peerId, - cmId = cmId - ) - message?.let { - val eventToSend = - LongPollParsedEvent.MessageRestored(message = message) - eventsToSend += eventToSend + if (message != null) { + val eventToSend = + LongPollParsedEvent.MessageRestored(message = message) + eventsToSend += eventToSend - listenersMap[LongPollEvent.MESSAGE_RESTORED]?.let { listeners -> - listeners.map { vkEventCallback -> - (vkEventCallback as? VkEventCallback) - ?.onEvent(eventToSend) - } - } - } + listenersMap[LongPollEvent.MESSAGE_RESTORED] + ?.forEach { it.onEvent(eventToSend) } } } - else -> Unit + MessageFlags.UNREAD -> Unit + MessageFlags.OUTGOING -> Unit + MessageFlags.AUDIO_LISTENED -> Unit + MessageFlags.FROM_GROUP_CHAT -> Unit + MessageFlags.CANCEL_SPAM -> Unit + MessageFlags.DELETED_FOR_ALL -> Unit + MessageFlags.DO_NOT_SHOW_NOTIFICATION -> Unit + MessageFlags.MESSAGE_WITH_REPLY -> Unit + MessageFlags.REACTION -> Unit } } - eventsToSend.forEach { eventToSend -> - listenersMap[LongPollEvent.MESSAGE_CLEAR_FLAGS]?.let { listeners -> - listeners.map { vkEventCallback -> - vkEventCallback.onEvent(eventToSend) - } - } + listenersMap[LongPollEvent.MESSAGE_CLEAR_FLAGS]?.forEach { listener -> + eventsToSend.forEach { listener.onEvent(it) } } + + continuation.resume(eventsToSend) } } - private fun parseMessageNew(eventType: ApiEvent, event: List) { + private suspend fun parseMessageNew( + eventType: ApiEvent, + event: List + ): List = suspendCancellableCoroutine { continuation -> 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 message = async { loadMessage(peerId = peerId, cmId = cmId) }.await() val convo = async { @@ -280,87 +263,85 @@ class LongPollUpdatesParser( ) }.await() - message?.let { - listenersMap[LongPollEvent.MESSAGE_NEW]?.let { - it.map { vkEventCallback -> - (vkEventCallback as VkEventCallback) - .onEvent( - 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 - ) - ) - } - } + if (message != null) { + val event = 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 + ) + + listenersMap[LongPollEvent.MESSAGE_NEW]?.forEach { it.onEvent(event) } + continuation.resume(listOf(event)) + } else { + continuation.resume(emptyList()) } } } - private fun parseMessageEdit(eventType: ApiEvent, event: List) { + private suspend fun parseMessageEdit( + eventType: ApiEvent, + event: List + ): List = suspendCancellableCoroutine { continuation -> 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 -> - listenersMap[LongPollEvent.MESSAGE_EDITED]?.let { - it.map { vkEventCallback -> - (vkEventCallback as VkEventCallback) - .onEvent(LongPollParsedEvent.MessageEdited(message)) - } - } + val message = loadMessage(peerId = peerId, cmId = cmId) + if (message != null) { + val event = LongPollParsedEvent.MessageEdited(message) + listenersMap[LongPollEvent.MESSAGE_EDITED]?.forEach { it.onEvent(event) } + continuation.resume(listOf(event)) + } else { + continuation.resume(emptyList()) } } } - private fun parseMessageReadIncoming(eventType: ApiEvent, event: List) { + private fun parseMessageReadIncoming( + eventType: ApiEvent, + event: List + ): List { Log.d("LongPollUpdatesParser", "$eventType: $event") val peerId = event[1].asLong() val cmId = event[2].asLong() val unreadCount = event[3].asInt() - listenersMap[LongPollEvent.INCOMING_MESSAGE_READ]?.let { listeners -> - listeners.map { vkEventCallback -> - (vkEventCallback as VkEventCallback) - .onEvent( - LongPollParsedEvent.IncomingMessageRead( - peerId = peerId, - cmId = cmId, - unreadCount = unreadCount - ) - ) - } - } + val event = LongPollParsedEvent.IncomingMessageRead( + peerId = peerId, + cmId = cmId, + unreadCount = unreadCount + ) + listenersMap[LongPollEvent.INCOMING_MESSAGE_READ]?.forEach { it.onEvent(event) } + return listOf(event) } - private fun parseMessageReadOutgoing(eventType: ApiEvent, event: List) { + private fun parseMessageReadOutgoing( + eventType: ApiEvent, + event: List + ): List { Log.d("LongPollUpdatesParser", "$eventType: $event") val peerId = event[1].asLong() val cmId = event[2].asLong() val unreadCount = event[3].asInt() - listenersMap[LongPollEvent.OUTGOING_MESSAGE_READ]?.let { listeners -> - listeners.map { vkEventCallback -> - (vkEventCallback as VkEventCallback) - .onEvent( - LongPollParsedEvent.OutgoingMessageRead( - peerId = peerId, - cmId = cmId, - unreadCount = unreadCount - ) - ) - } - } + val event = LongPollParsedEvent.OutgoingMessageRead( + peerId = peerId, + cmId = cmId, + unreadCount = unreadCount + ) + + listenersMap[LongPollEvent.OUTGOING_MESSAGE_READ]?.forEach { it.onEvent(event) } + return listOf(event) } - private fun parseChatClearFlags(eventType: ApiEvent, event: List) { + private suspend fun parseChatClearFlags( + eventType: ApiEvent, + event: List + ): List = suspendCancellableCoroutine { continuation -> Log.d("LongPollUpdatesParser", "$eventType: $event") val peerId = event[1].asLong() @@ -391,31 +372,36 @@ class LongPollUpdatesParser( ) eventsToSend += eventToSend - listenersMap[LongPollEvent.CHAT_ARCHIVED]?.let { listeners -> - listeners.map { vkEventCallback -> - (vkEventCallback as? VkEventCallback) - ?.onEvent(eventToSend) - } - } + listenersMap[LongPollEvent.CHAT_ARCHIVED]?.forEach { it.onEvent(eventToSend) } } - else -> Unit + ConvoFlags.DISABLE_PUSH -> Unit + ConvoFlags.DISABLE_SOUND -> Unit + ConvoFlags.INCOMING_CHAT_REQUEST -> Unit + ConvoFlags.DECLINED_CHAT_REQUEST -> Unit + ConvoFlags.MENTION -> Unit + ConvoFlags.HIDE_CHAT_FROM_SEARCH -> Unit + ConvoFlags.BUSINESS_CHAT -> Unit + ConvoFlags.MARKED_MESSAGE -> Unit + ConvoFlags.DO_NOT_NOTIFY_MENTIONS_ALL_ONLINE -> Unit + ConvoFlags.DO_NOT_NOTIFY_ALL_MENTIONS -> Unit + ConvoFlags.MARKED_AS_UNREAD -> Unit + ConvoFlags.CALL_IN_PROGRESS -> Unit } } - eventsToSend.forEach { eventToSend -> - listenersMap[LongPollEvent.CHAT_CLEAR_FLAGS]?.let { listeners -> - listeners.map { vkEventCallback -> - (vkEventCallback as? VkEventCallback)?.onEvent( - eventToSend - ) - } - } + listenersMap[LongPollEvent.CHAT_CLEAR_FLAGS]?.forEach { listener -> + eventsToSend.forEach { listener.onEvent(it) } } + + continuation.resume(eventsToSend) } } - private fun parseChatSetFlags(eventType: ApiEvent, event: List) { + private suspend fun parseChatSetFlags( + eventType: ApiEvent, + event: List + ): List = suspendCancellableCoroutine { continuation -> Log.d("LongPollUpdatesParser", "$eventType: $event") val peerId = event[1].asLong() @@ -446,88 +432,87 @@ class LongPollUpdatesParser( ) eventsToSend += eventToSend - listenersMap[LongPollEvent.CHAT_ARCHIVED]?.let { listeners -> - listeners.map { vkEventCallback -> - (vkEventCallback as? VkEventCallback) - ?.onEvent(eventToSend) - } - } + listenersMap[LongPollEvent.CHAT_ARCHIVED]?.forEach { it.onEvent(eventToSend) } } - else -> Unit + ConvoFlags.DISABLE_PUSH -> Unit + ConvoFlags.DISABLE_SOUND -> Unit + ConvoFlags.INCOMING_CHAT_REQUEST -> Unit + ConvoFlags.DECLINED_CHAT_REQUEST -> Unit + ConvoFlags.MENTION -> Unit + ConvoFlags.HIDE_CHAT_FROM_SEARCH -> Unit + ConvoFlags.BUSINESS_CHAT -> Unit + ConvoFlags.MARKED_MESSAGE -> Unit + ConvoFlags.DO_NOT_NOTIFY_MENTIONS_ALL_ONLINE -> Unit + ConvoFlags.DO_NOT_NOTIFY_ALL_MENTIONS -> Unit + ConvoFlags.MARKED_AS_UNREAD -> Unit + ConvoFlags.CALL_IN_PROGRESS -> Unit } } - eventsToSend.forEach { eventToSend -> - listenersMap[LongPollEvent.CHAT_SET_FLAGS]?.let { listeners -> - listeners.map { vkEventCallback -> - (vkEventCallback as? VkEventCallback)?.onEvent( - eventToSend - ) - } - } + listenersMap[LongPollEvent.CHAT_SET_FLAGS]?.forEach { listener -> + eventsToSend.forEach { listener.onEvent(it) } } + + continuation.resume(eventsToSend) } } - private fun parseMessagesDeleted(eventType: ApiEvent, event: List) { + private fun parseMessagesDeleted( + eventType: ApiEvent, + event: List + ): List { Log.d("LongPollUpdatesParser", "$eventType: $event") val peerId = event[1].asLong() val cmId = event[2].asLong() - listenersMap[LongPollEvent.CHAT_CLEARED]?.let { listeners -> - listeners.forEach { vkEventCallback -> - (vkEventCallback as VkEventCallback) - .onEvent( - LongPollParsedEvent.ChatCleared( - peerId = peerId, - toCmId = cmId - ) - ) - } - } + val event = LongPollParsedEvent.ChatCleared( + peerId = peerId, + toCmId = cmId + ) + listenersMap[LongPollEvent.CHAT_CLEARED]?.forEach { it.onEvent(event) } + return listOf(event) } - private fun parseChatMajorChanged(eventType: ApiEvent, event: List) { + private fun parseChatMajorChanged( + eventType: ApiEvent, + event: List + ): List { Log.d("LongPollUpdatesParser", "$eventType: $event") val peerId = event[1].asLong() 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, - ) - ) - } - } + val event = LongPollParsedEvent.ChatMajorChanged( + peerId = peerId, + majorId = majorId, + ) + listenersMap[LongPollEvent.CHAT_MAJOR_CHANGED]?.forEach { it.onEvent(event) } + return listOf(event) } - private fun parseChatMinorChanged(eventType: ApiEvent, event: List) { + private fun parseChatMinorChanged( + eventType: ApiEvent, + event: List + ): List { Log.d("LongPollUpdatesParser", "$eventType: $event") val peerId = event[1].asLong() 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, - ) - ) - } - } + val event = LongPollParsedEvent.ChatMinorChanged( + peerId = peerId, + minorId = minorId, + ) + listenersMap[LongPollEvent.CHAT_MINOR_CHANGED]?.forEach { it.onEvent(event) } + return listOf(event) } - private fun parseInteraction(eventType: ApiEvent, event: List) { + private fun parseInteraction( + eventType: ApiEvent, + event: List + ): List { Log.d("LongPollUpdatesParser", "$eventType: $event") val interactionType = when (eventType) { @@ -536,7 +521,7 @@ class LongPollUpdatesParser( ApiEvent.PHOTO_UPLOADING -> InteractionType.Photo ApiEvent.VIDEO_UPLOADING -> InteractionType.Video ApiEvent.FILE_UPLOADING -> InteractionType.File - else -> return + else -> return emptyList() } val longPollEvent: LongPollEvent = when (eventType) { @@ -545,7 +530,6 @@ class LongPollUpdatesParser( 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() @@ -554,25 +538,24 @@ class LongPollUpdatesParser( 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 + if (userIds.isEmpty()) return emptyList() - listenersMap[longPollEvent]?.let { listeners -> - listeners.forEach { vkEventCallback -> - (vkEventCallback as VkEventCallback) - .onEvent( - LongPollParsedEvent.Interaction( - interactionType = interactionType, - peerId = peerId, - userIds = userIds, - totalCount = totalCount, - timestamp = timestamp - ) - ) - } - } + val event = LongPollParsedEvent.Interaction( + interactionType = interactionType, + peerId = peerId, + userIds = userIds, + totalCount = totalCount, + timestamp = timestamp + ) + + listenersMap[longPollEvent]?.forEach { it.onEvent(event) } + return listOf(event) } - private fun parseUnreadCounterUpdate(eventType: ApiEvent, event: List) { + private fun parseUnreadCounterUpdate( + eventType: ApiEvent, + event: List + ): List { Log.d("LongPollUpdatesParser", "$eventType $event") val unreadCount = event[1].asInt() @@ -583,58 +566,57 @@ class LongPollUpdatesParser( val archiveUnreadUnmutedCount = event[8].asInt() val archiveMentionsCount = event[9].asInt() - 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 - ) - ) - } - } + val event = LongPollParsedEvent.UnreadCounter( + unread = unreadCount, + unreadUnmuted = unreadUnmutedCount, + showOnlyMuted = showOnlyMuted, + business = businessNotifyUnreadCount, + archive = archiveUnreadCount, + archiveUnmuted = archiveUnreadUnmutedCount, + archiveMentions = archiveMentionsCount + ) + listenersMap[LongPollEvent.UNREAD_COUNTER_UPDATE]?.forEach { it.onEvent(event) } + return listOf(event) } - private fun parseMessageUpdated(eventType: ApiEvent, event: List) { + private suspend fun parseMessageUpdated( + eventType: ApiEvent, + event: List + ): List = suspendCancellableCoroutine { continuation -> 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 -> - listenersMap[LongPollEvent.MESSAGE_UPDATED]?.let { - it.map { vkEventCallback -> - (vkEventCallback as VkEventCallback) - .onEvent(LongPollParsedEvent.MessageUpdated(message)) - } - } + val message = loadMessage(peerId = peerId, cmId = cmId) + + if (message != null) { + val event = LongPollParsedEvent.MessageUpdated(message) + listenersMap[LongPollEvent.MESSAGE_UPDATED]?.forEach { it.onEvent(event) } + continuation.resume(listOf(event)) + } else { + continuation.resume(emptyList()) } } } - private fun parseMessageCacheClear(eventType: ApiEvent, event: List) { + private suspend fun parseMessageCacheClear( + eventType: ApiEvent, + event: List + ): List = suspendCancellableCoroutine { continuation -> Log.d("LongPollUpdatesParser", "$eventType $event") val messageId = event[1].asLong() coroutineScope.launch(Dispatchers.IO) { - loadMessage(messageId = messageId)?.let { message -> - listenersMap[LongPollEvent.MESSAGE_CACHE_CLEAR]?.let { - it.map { vkEventCallback -> - (vkEventCallback as VkEventCallback) - .onEvent(LongPollParsedEvent.MessageCacheClear(message)) - } - } + val message = loadMessage(messageId = messageId) + if (message != null) { + val event = LongPollParsedEvent.MessageCacheClear(message) + listenersMap[LongPollEvent.MESSAGE_CACHE_CLEAR]?.forEach { it.onEvent(event) } + continuation.resume(listOf(event)) + } else { + continuation.resume(emptyList()) } } } @@ -643,7 +625,7 @@ class LongPollUpdatesParser( peerId: Long? = null, cmId: Long? = null, messageId: Long? = null - ): VkMessage? = suspendCoroutine { continuation -> + ): VkMessage? = suspendCancellableCoroutine { continuation -> require((peerId != null && cmId != null) || messageId != null) coroutineScope.launch(Dispatchers.IO) { @@ -677,7 +659,7 @@ class LongPollUpdatesParser( peerId: Long, extended: Boolean = false, fields: String? = null - ): VkConvo? = suspendCoroutine { continuation -> + ): VkConvo? = suspendCancellableCoroutine { continuation -> coroutineScope.launch(Dispatchers.IO) { convoUseCase.getById( peerIds = listOf(peerId), diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkMessageData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkMessageData.kt index 1aa0fbb5..a9f04d2f 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkMessageData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkMessageData.kt @@ -117,5 +117,6 @@ fun VkMessageData.asDomain(): VkMessage = VkMessage( pinnedAt = pinnedAt, isPinned = isPinned == true, formatData = formatData?.asDomain(), - isSpam = false + isSpam = false, + isDeleted = false ) diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkPinnedMessageData.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkPinnedMessageData.kt index 2c44f0fd..84f40b4c 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkPinnedMessageData.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/VkPinnedMessageData.kt @@ -56,5 +56,6 @@ data class VkPinnedMessageData( isPinned = true, isSpam = false, formatData = null, + isDeleted = false ) } diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkMessage.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkMessage.kt index 10c036bd..a12fb739 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkMessage.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkMessage.kt @@ -36,6 +36,8 @@ data class VkMessage( val group: VkGroupDomain?, val actionUser: VkUser?, val actionGroup: VkGroupDomain?, + + val isDeleted: Boolean ) { fun isPeerChat() = peerId > 2_000_000_000 @@ -111,7 +113,7 @@ fun VkMessage.asEntity(): VkMessageEntity = VkMessageEntity( actionCmId = actionCmId, actionMessage = actionMessage, updateTime = updateTime, - important = isImportant, + isImportant = isImportant, forwardIds = forwards.orEmpty().map(VkMessage::id), // TODO: 05/05/2024, Danil Nikolaev: save attachments attachments = emptyList(), @@ -119,4 +121,6 @@ fun VkMessage.asEntity(): VkMessageEntity = VkMessageEntity( geoType = geoType, pinnedAt = pinnedAt, isPinned = isPinned, + isDeleted = isDeleted, + isSpam = isSpam ) diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/database/VkMessageEntity.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/database/VkMessageEntity.kt index 8595fbb4..bf9aaa90 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/database/VkMessageEntity.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/database/VkMessageEntity.kt @@ -21,13 +21,15 @@ data class VkMessageEntity( val actionCmId: Long?, val actionMessage: String?, val updateTime: Int?, - val important: Boolean, + val isImportant: Boolean, val forwardIds: List?, val attachments: List?, // TODO: 01/05/2024, Danil Nikolaev: how to store??? val replyMessageId: Long?, val geoType: String?, val pinnedAt: Int?, - val isPinned: Boolean + val isPinned: Boolean, + val isDeleted: Boolean, + val isSpam: Boolean ) fun VkMessageEntity.asExternalModel(): VkMessage = VkMessage( @@ -45,7 +47,7 @@ fun VkMessageEntity.asExternalModel(): VkMessage = VkMessage( actionCmId = actionCmId, actionMessage = actionMessage, updateTime = updateTime, - isImportant = important, + isImportant = isImportant, forwards = emptyList(),//forwards.orEmpty().map(VkMessageEntity::asExternalModel), // TODO: 05/05/2024, Danil Nikolaev: restore attachments attachments = attachments.orEmpty().map { VkUnknownAttachment }, @@ -59,4 +61,5 @@ fun VkMessageEntity.asExternalModel(): VkMessage = VkMessage( isPinned = isPinned, isSpam = false, formatData = null, + isDeleted = isDeleted ) diff --git a/feature/convos/src/main/kotlin/dev/meloda/fast/convos/ConvosViewModel.kt b/feature/convos/src/main/kotlin/dev/meloda/fast/convos/ConvosViewModel.kt index 84fbcece..1fc02457 100644 --- a/feature/convos/src/main/kotlin/dev/meloda/fast/convos/ConvosViewModel.kt +++ b/feature/convos/src/main/kotlin/dev/meloda/fast/convos/ConvosViewModel.kt @@ -613,6 +613,7 @@ class ConvosViewModel( if (convoIndex == null) { // диалога нет в списке // pizdets } else { + // TODO: 30.05.2026, Danil Nikolaev: reimplement newConvos.removeAt(convoIndex) replaceConvos(newConvos.sorted()) diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModelImpl.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModelImpl.kt index 0293deee..3086c4a4 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModelImpl.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModelImpl.kt @@ -1037,6 +1037,7 @@ class MessagesHistoryViewModelImpl( isSpam = false, pinnedAt = null, formatData = formatData, + isDeleted = false ) formatData = formatData.copy(items = emptyList()) sendingMessages += newMessage