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..d5380677 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,17 @@ 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.data.VkMemoryCache 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.domain.MessagesUseCase +import dev.meloda.fast.domain.StoreUsersUseCase +import dev.meloda.fast.model.api.data.VkGroupData +import dev.meloda.fast.model.api.data.VkUserData +import dev.meloda.fast.model.api.data.asDomain import dev.meloda.fast.model.api.data.LongPollUpdates import dev.meloda.fast.model.api.data.VkLongPollData import dev.meloda.fast.ui.R @@ -33,6 +39,7 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.flow.last import org.koin.android.ext.android.inject import kotlin.coroutines.CoroutineContext import kotlin.coroutines.resume @@ -55,6 +62,8 @@ class LongPollingService : Service() { private val coroutineScope = CoroutineScope(coroutineContext) private val longPollUseCase: LongPollUseCase by inject() + private val messagesUseCase: MessagesUseCase by inject() + private val storeUsersUseCase: StoreUsersUseCase by inject() private val updatesParser: LongPollUpdatesParser by inject() private var currentJob: Job? = null @@ -150,6 +159,8 @@ class LongPollingService : Service() { var serverInfo = getServerInfo() ?: throw LongPollException(message = "bad VK response (server info)") + syncLongPollHistory(serverInfo) + var lastUpdatesResponse: LongPollUpdates? = getUpdatesResponse(serverInfo) ?: throw LongPollException(message = "initiation error: bad VK response (last updates)") @@ -160,6 +171,7 @@ class LongPollingService : Service() { failCount++ serverInfo = getServerInfo() ?: throw LongPollException(message = "failed retrieving server info after error: bad VK response (server info #2)") + syncLongPollHistory(serverInfo) lastUpdatesResponse = getUpdatesResponse(serverInfo) continue } @@ -179,6 +191,7 @@ class LongPollingService : Service() { ?: throw LongPollException( message = "failed retrieving server info after error: bad VK response (server info #3)" ) + syncLongPollHistory(serverInfo) lastUpdatesResponse = getUpdatesResponse(serverInfo) } @@ -196,6 +209,9 @@ class LongPollingService : Service() { updates.forEach(updatesParser::parseNextUpdate) } + AppSettings.LongPoll.ts = lastUpdatesResponse.ts ?: serverInfo.ts + AppSettings.LongPoll.pts = lastUpdatesResponse.pts ?: AppSettings.LongPoll.pts + lastUpdatesResponse = getUpdatesResponse(serverInfo.copy(ts = newTs)) } } @@ -246,6 +262,73 @@ class LongPollingService : Service() { } } + private suspend fun syncLongPollHistory(serverInfo: VkLongPollData) { + val cursorTs = AppSettings.LongPoll.ts ?: serverInfo.ts + val cursorPts = AppSettings.LongPoll.pts ?: serverInfo.pts + val maxMsgId = messagesUseCase.getLocalMaxMessageId() + + var currentTs = cursorTs + var currentPts = cursorPts + var more: Int? + + do { + val historyResponse = getLongPollHistory( + ts = currentTs, + pts = currentPts, + maxMsgId = maxMsgId + ) ?: return + + historyResponse.history.orEmpty().forEach(updatesParser::parseNextUpdate) + + historyResponse.messages?.items.orEmpty().takeIf { it.isNotEmpty() }?.let { rawMessages -> + val messages = rawMessages.map { it.asDomain() } + messagesUseCase.storeMessages(messages) + } + + historyResponse.profiles.orEmpty().takeIf { it.isNotEmpty() }?.let { profiles -> + val users = profiles.map(VkUserData::mapToDomain) + VkMemoryCache.appendUsers(users) + storeUsersUseCase(users).last() + } + + historyResponse.groups.orEmpty().takeIf { it.isNotEmpty() }?.let { groups -> + VkMemoryCache.appendGroups(groups.map(VkGroupData::mapToDomain)) + } + + currentTs = historyResponse.ts ?: historyResponse.fromPts ?: currentTs + currentPts = historyResponse.newPts ?: historyResponse.pts ?: currentPts + more = historyResponse.more + + AppSettings.LongPoll.ts = currentTs + AppSettings.LongPoll.pts = currentPts + } while (more == 1) + } + + private suspend fun getLongPollHistory( + ts: Int, + pts: Int, + maxMsgId: Long? + ): dev.meloda.fast.model.api.data.LongPollHistoryResponse? = suspendCancellableCoroutine { + longPollUseCase.getLongPollHistory( + ts = ts, + pts = pts, + lpVersion = VkConstants.LP_VERSION, + maxMsgId = maxMsgId, + eventsLimit = 1000, + msgsLimit = 200 + ).listenValue(coroutineScope) { state -> + state.processState( + success = { response -> + it.resume(response) + }, + error = { error -> + Log.e(TAG, "getLongPollHistory: error: $error") + it.resume(null) + } + ) + } + } + 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..e291bc2d 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,6 +1,7 @@ package dev.meloda.fast.service.longpolling.di import dev.meloda.fast.domain.LongPollUpdatesParser +import dev.meloda.fast.domain.LongPollUpdatesReducer import dev.meloda.fast.domain.LongPollUseCase import dev.meloda.fast.domain.LongPollUseCaseImpl import org.koin.core.module.dsl.singleOf @@ -10,4 +11,5 @@ import org.koin.dsl.module val longPollModule = module { singleOf(::LongPollUseCaseImpl) bind LongPollUseCase::class singleOf(::LongPollUpdatesParser) + singleOf(::LongPollUpdatesReducer) } diff --git a/core/common/src/main/kotlin/dev/meloda/fast/common/extensions/Extensions.kt b/core/common/src/main/kotlin/dev/meloda/fast/common/extensions/Extensions.kt index 25d1dd1c..7c560945 100644 --- a/core/common/src/main/kotlin/dev/meloda/fast/common/extensions/Extensions.kt +++ b/core/common/src/main/kotlin/dev/meloda/fast/common/extensions/Extensions.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import kotlin.reflect.KClass import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds @@ -45,6 +46,14 @@ fun Flow.listenValue( action: suspend (T) -> Unit ): Job = onEach(action::invoke).launchIn(coroutineScope) +fun CoroutineScope.launchDbRefresh( + load: suspend () -> Unit, + after: suspend () -> Unit +): Job = launch { + load() + after() +} + fun createTimerFlow( time: Int, onStartAction: (suspend () -> Unit)? = null, diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/UserConfig.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/UserConfig.kt index 2ebc4f88..611e0ac5 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/UserConfig.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/UserConfig.kt @@ -26,6 +26,7 @@ object UserConfig { userId = -1 trustedHash = null exchangeToken = null + AppSettings.LongPoll.clear() } fun isLoggedIn(): Boolean { diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/api/convos/ConvosRepository.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/api/convos/ConvosRepository.kt index 53d73767..a987bad3 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/api/convos/ConvosRepository.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/api/convos/ConvosRepository.kt @@ -8,6 +8,9 @@ import dev.meloda.fast.network.RestApiErrorDomain interface ConvosRepository { suspend fun storeConvos(convos: List) + suspend fun getLocalConvos(): List + suspend fun getLocalConvoById(peerId: Long): VkConvo? + suspend fun deleteLocalConvo(peerId: Long) suspend fun getConvos( count: Int?, diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/api/convos/ConvosRepositoryImpl.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/api/convos/ConvosRepositoryImpl.kt index 970cbfd6..0b593afd 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/api/convos/ConvosRepositoryImpl.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/api/convos/ConvosRepositoryImpl.kt @@ -19,11 +19,13 @@ import dev.meloda.fast.model.api.domain.VkGroupDomain import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkUser import dev.meloda.fast.model.api.domain.asEntity +import dev.meloda.fast.model.database.VkConvoEntity import dev.meloda.fast.model.api.requests.ConvosGetRequest import dev.meloda.fast.network.RestApiErrorDomain import dev.meloda.fast.network.mapApiDefault import dev.meloda.fast.network.mapApiResult import dev.meloda.fast.network.service.convos.ConvosService +import dev.meloda.fast.model.database.asExternalModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -40,6 +42,28 @@ class ConvosRepositoryImpl( convoDao.insertAll(convos.map(VkConvo::asEntity)) } + override suspend fun getLocalConvos(): List = withContext(Dispatchers.IO) { + convoDao.getAllWithMessage().map { convoWithMessage -> + convoWithMessage.convo.asExternalModel().copy( + lastMessage = convoWithMessage.message?.asExternalModel() + ) + }.sorted() + } + + override suspend fun getLocalConvoById(peerId: Long): VkConvo? = withContext(Dispatchers.IO) { + convoDao.getByIdWithMessage(peerId)?.let { convoWithMessage -> + convoWithMessage.convo.asExternalModel().copy( + lastMessage = convoWithMessage.message?.asExternalModel() + ) + } + } + + override suspend fun deleteLocalConvo(peerId: Long) { + withContext(Dispatchers.IO) { + convoDao.deleteById(peerId) + } + } + override suspend fun getConvos( count: Int?, offset: Int?, @@ -198,4 +222,28 @@ class ConvosRepositoryImpl( ): ApiResult = withContext(Dispatchers.IO) { convosService.unarchive(mapOf("peer_id" to peerId.toString())).mapApiDefault() } + + private fun List.sorted(): List { + val newConvos = toMutableList() + + val pinnedConvos = newConvos + .filter(VkConvo::isPinned) + .sortedWith { c1, c2 -> + val diff = c2.majorId - c1.majorId + + if (diff == 0) { + c2.minorId - c1.minorId + } else { + diff + } + } + + newConvos.removeAll(pinnedConvos) + newConvos.sortWith { c1, c2 -> + (c2.lastMessage?.date ?: 0) - (c1.lastMessage?.date ?: 0) + } + + newConvos.addAll(0, pinnedConvos) + return newConvos + } } diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/api/longpoll/LongPollRepository.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/api/longpoll/LongPollRepository.kt index ceb2b0cb..d2225197 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/api/longpoll/LongPollRepository.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/api/longpoll/LongPollRepository.kt @@ -1,6 +1,7 @@ package dev.meloda.fast.data.api.longpoll import dev.meloda.fast.model.api.data.LongPollUpdates +import dev.meloda.fast.model.api.data.LongPollHistoryResponse import dev.meloda.fast.model.api.data.VkLongPollData import dev.meloda.fast.network.RestApiErrorDomain import com.slack.eithernet.ApiResult @@ -21,4 +22,14 @@ interface LongPollRepository { mode: Int, version: Int ): ApiResult + + suspend fun getLongPollHistory( + ts: Int, + pts: Int, + lpVersion: Int, + lastN: Int? = null, + maxMsgId: Long? = null, + eventsLimit: Int? = null, + msgsLimit: Int? = null + ): ApiResult } diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/api/longpoll/LongPollRepositoryImpl.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/api/longpoll/LongPollRepositoryImpl.kt index 3dbf7b9b..06d937db 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/api/longpoll/LongPollRepositoryImpl.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/api/longpoll/LongPollRepositoryImpl.kt @@ -1,7 +1,9 @@ package dev.meloda.fast.data.api.longpoll import dev.meloda.fast.model.api.data.LongPollUpdates +import dev.meloda.fast.model.api.data.LongPollHistoryResponse import dev.meloda.fast.model.api.data.VkLongPollData +import dev.meloda.fast.model.api.requests.LongPollGetHistoryRequest import dev.meloda.fast.model.api.requests.LongPollGetUpdatesRequest import dev.meloda.fast.model.api.requests.MessagesGetLongPollServerRequest import dev.meloda.fast.network.RestApiErrorDomain @@ -52,4 +54,29 @@ class LongPollRepositoryImpl( longPollService.getResponse(serverUrl, requestModel.map).mapDefault() } + + override suspend fun getLongPollHistory( + ts: Int, + pts: Int, + lpVersion: Int, + lastN: Int?, + maxMsgId: Long?, + eventsLimit: Int?, + msgsLimit: Int? + ): ApiResult = withContext(Dispatchers.IO) { + val requestModel = LongPollGetHistoryRequest( + ts = ts, + pts = pts, + lpVersion = lpVersion, + lastN = lastN, + maxMsgId = maxMsgId, + eventsLimit = eventsLimit, + msgsLimit = msgsLimit + ) + + messagesService.getLongPollHistory(requestModel.map).mapApiResult( + successMapper = { response -> response.requireResponse() }, + errorMapper = { error -> error?.toDomain() } + ) + } } diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/api/messages/MessagesRepository.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/api/messages/MessagesRepository.kt index a204c3e0..70ef7aa7 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/api/messages/MessagesRepository.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/api/messages/MessagesRepository.kt @@ -14,6 +14,12 @@ interface MessagesRepository { suspend fun storeMessages(messages: List) + suspend fun getLocalMessages(convoId: Long): List + suspend fun getLocalMessageById(messageId: Long): VkMessage? + suspend fun getLocalMessageByConvoMessageId(convoId: Long, cmId: Long): VkMessage? + suspend fun getLocalMaxMessageId(): Long? + suspend fun deleteLocalMessages(messageIds: List) + suspend fun getHistory( convoId: Long, offset: Int?, diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/api/messages/MessagesRepositoryImpl.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/api/messages/MessagesRepositoryImpl.kt index 3634deb8..6739aced 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/api/messages/MessagesRepositoryImpl.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/api/messages/MessagesRepositoryImpl.kt @@ -22,6 +22,7 @@ import dev.meloda.fast.model.api.domain.VkGroupDomain import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkUser import dev.meloda.fast.model.api.domain.asEntity +import dev.meloda.fast.model.database.VkMessageEntity import dev.meloda.fast.model.api.requests.MessagesCreateChatRequest import dev.meloda.fast.model.api.requests.MessagesDeleteRequest import dev.meloda.fast.model.api.requests.MessagesEditRequest @@ -39,6 +40,7 @@ import dev.meloda.fast.model.api.requests.MessagesUnpinMessageRequest import dev.meloda.fast.model.api.responses.MessagesGetConvoMembersResponse import dev.meloda.fast.model.api.responses.MessagesGetReadPeersResponse import dev.meloda.fast.model.api.responses.MessagesSendResponse +import dev.meloda.fast.model.database.asExternalModel import dev.meloda.fast.network.RestApiErrorDomain import dev.meloda.fast.network.mapApiDefault import dev.meloda.fast.network.mapApiResult @@ -354,6 +356,28 @@ class MessagesRepositoryImpl( messageDao.insertAll(messages.map(VkMessage::asEntity)) } + override suspend fun getLocalMessages(convoId: Long): List = withContext(Dispatchers.IO) { + messageDao.getAll(convoId).map(VkMessageEntity::asExternalModel) + } + + override suspend fun getLocalMessageById(messageId: Long): VkMessage? = withContext(Dispatchers.IO) { + messageDao.getById(messageId)?.asExternalModel() + } + + override suspend fun getLocalMessageByConvoMessageId(convoId: Long, cmId: Long): VkMessage? = withContext(Dispatchers.IO) { + messageDao.getByConvoMessageId(convoId, cmId)?.asExternalModel() + } + + override suspend fun getLocalMaxMessageId(): Long? = withContext(Dispatchers.IO) { + messageDao.getMaxId() + } + + override suspend fun deleteLocalMessages(messageIds: List) { + withContext(Dispatchers.IO) { + messageDao.deleteByIds(messageIds) + } + } + override suspend fun edit( peerId: Long, messageId: Long?, 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..efcffd64 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 @@ -9,9 +9,13 @@ import dev.meloda.fast.model.database.VkConvoEntity @Dao abstract class ConvoDao : EntityDao { - @Query("SELECT * FROM convos") + @Query("SELECT * FROM convos ORDER BY majorId DESC, minorId DESC") abstract suspend fun getAll(): List + @Transaction + @Query("SELECT * FROM convos ORDER BY majorId DESC, minorId DESC") + abstract suspend fun getAllWithMessage(): List + @Query("SELECT * FROM convos WHERE id IN (:ids)") abstract suspend fun getAllByIds(ids: List): List @@ -22,6 +26,9 @@ abstract class ConvoDao : EntityDao { @Query("SELECT * FROM convos WHERE id IS (:id)") abstract suspend fun getByIdWithMessage(id: Long): ConvoWithMessage? + @Query("DELETE FROM convos WHERE id IS (:id)") + abstract suspend fun deleteById(id: Long): Int + @Query("DELETE FROM convos WHERE rowid IN (:ids)") abstract suspend fun deleteByIds(ids: List): 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..c9c8de29 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 @@ -10,15 +10,21 @@ abstract class MessageDao : EntityDao { @Query("SELECT * FROM messages") abstract suspend fun getAll(): List - @Query("SELECT * FROM messages WHERE peerId IS (:convoId)") + @Query("SELECT * FROM messages WHERE peerId IS (:convoId) ORDER BY date DESC, id DESC") abstract suspend fun getAll(convoId: Long): List @Query("SELECT * FROM messages WHERE id IN (:ids)") - abstract suspend fun getAllByIds(ids: List): List + abstract suspend fun getAllByIds(ids: List): List @Query("SELECT * FROM messages WHERE id IS (:messageId)") abstract suspend fun getById(messageId: Long): VkMessageEntity? + @Query("SELECT * FROM messages WHERE peerId IS (:convoId) AND cmId IS (:cmId)") + abstract suspend fun getByConvoMessageId(convoId: Long, cmId: Long): VkMessageEntity? + + @Query("SELECT MAX(id) FROM messages") + abstract suspend fun getMaxId(): Long? + @Query("DELETE FROM messages WHERE id IN (:ids)") - abstract suspend fun deleteByIds(ids: List): Int + abstract suspend fun deleteByIds(ids: List): Int } diff --git a/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/AppSettings.kt b/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/AppSettings.kt index 9eb39856..b34a9e9a 100644 --- a/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/AppSettings.kt +++ b/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/AppSettings.kt @@ -230,6 +230,21 @@ object AppSettings { set(value) = put(SettingsKeys.KEY_MORE_ANIMATIONS, value) } + object LongPoll { + var ts: Int? + get() = get(SettingsKeys.KEY_LONG_POLL_TS, 0).takeIf { it > 0 } + set(value) = put(SettingsKeys.KEY_LONG_POLL_TS, value ?: 0) + + var pts: Int? + get() = get(SettingsKeys.KEY_LONG_POLL_PTS, 0).takeIf { it > 0 } + set(value) = put(SettingsKeys.KEY_LONG_POLL_PTS, value ?: 0) + + fun clear() { + ts = null + pts = null + } + } + object Debug { var showAlertAfterCrash: Boolean get() = get( diff --git a/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/SettingsKeys.kt b/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/SettingsKeys.kt index 259e6f76..97a8b665 100644 --- a/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/SettingsKeys.kt +++ b/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/SettingsKeys.kt @@ -39,6 +39,8 @@ object SettingsKeys { const val DEFAULT_VALUE_FEATURES_FAST_TEXT = "¯\\_(ツ)_/¯" const val KEY_LONG_POLL_IN_BACKGROUND = "lp_background" const val DEFAULT_LONG_POLL_IN_BACKGROUND = false + const val KEY_LONG_POLL_TS = "lp_ts" + const val KEY_LONG_POLL_PTS = "lp_pts" const val KEY_ACTIVITY_SEND_ONLINE_STATUS = "activity_send_online_status" const val DEFAULT_VALUE_KEY_ACTIVITY_SEND_ONLINE_STATUS = false diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/ConvoUseCase.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/ConvoUseCase.kt index d5217e8f..d98c693d 100644 --- a/core/domain/src/main/kotlin/dev/meloda/fast/domain/ConvoUseCase.kt +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/ConvoUseCase.kt @@ -8,6 +8,9 @@ import kotlinx.coroutines.flow.Flow interface ConvoUseCase : BaseUseCase { suspend fun storeConvos(convos: List) + suspend fun getLocalConvos(): List + suspend fun getLocalConvoById(peerId: Long): VkConvo? + suspend fun deleteLocalConvo(peerId: Long) fun getConvos( count: Int? = null, diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/ConvoUseCaseImpl.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/ConvoUseCaseImpl.kt index 094fc675..ab7cabe5 100644 --- a/core/domain/src/main/kotlin/dev/meloda/fast/domain/ConvoUseCaseImpl.kt +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/ConvoUseCaseImpl.kt @@ -19,6 +19,18 @@ class ConvoUseCaseImpl( repository.storeConvos(convos) } + override suspend fun getLocalConvos(): List = withContext(Dispatchers.IO) { + repository.getLocalConvos() + } + + override suspend fun getLocalConvoById(peerId: Long): VkConvo? = withContext(Dispatchers.IO) { + repository.getLocalConvoById(peerId) + } + + override suspend fun deleteLocalConvo(peerId: Long) = withContext(Dispatchers.IO) { + repository.deleteLocalConvo(peerId) + } + override fun getConvos( count: Int?, offset: Int?, 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 042cbf75..169c55f3 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 @@ -66,6 +66,14 @@ class LongPollUpdatesParser( eventDispatcher.registerListener(LongPollEvent.MESSAGE_RESTORED, assembleEventCallback(block)) } + fun onMessageUpdated(block: (LongPollParsedEvent.MessageUpdated) -> Unit) { + eventDispatcher.registerListener(LongPollEvent.MESSAGE_UPDATED, assembleEventCallback(block)) + } + + fun onMessageCacheClear(block: (LongPollParsedEvent.MessageCacheClear) -> Unit) { + eventDispatcher.registerListener(LongPollEvent.MESSAGE_CACHE_CLEAR, assembleEventCallback(block)) + } + fun onNewMessage(block: (LongPollParsedEvent.NewMessage) -> Unit) { eventDispatcher.registerListener(LongPollEvent.MESSAGE_NEW, assembleEventCallback(block)) } diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/LongPollUpdatesReducer.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/LongPollUpdatesReducer.kt new file mode 100644 index 00000000..607c2d48 --- /dev/null +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/LongPollUpdatesReducer.kt @@ -0,0 +1,213 @@ +package dev.meloda.fast.domain + +import android.util.Log +import dev.meloda.fast.model.LongPollParsedEvent +import dev.meloda.fast.model.api.domain.VkMessage +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.launch + +class LongPollUpdatesReducer( + updatesParser: LongPollUpdatesParser, + private val messagesUseCase: MessagesUseCase, + private val convoUseCase: ConvoUseCase +) { + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + + private val _events = MutableSharedFlow(extraBufferCapacity = 256) + val events: SharedFlow = _events.asSharedFlow() + + val newMessages = events.filterIsInstance() + val messageEdited = events.filterIsInstance() + val messageIncomingRead = events.filterIsInstance() + val messageOutgoingRead = events.filterIsInstance() + val messageDeleted = events.filterIsInstance() + val messageRestored = events.filterIsInstance() + val messageMarkedAsImportant = events.filterIsInstance() + val messageMarkedAsSpam = events.filterIsInstance() + val messageMarkedAsNotSpam = events.filterIsInstance() + val interactions = events.filterIsInstance() + val chatMajorChanged = events.filterIsInstance() + val chatMinorChanged = events.filterIsInstance() + val chatCleared = events.filterIsInstance() + val chatArchived = events.filterIsInstance() + val messageUpdated = events.filterIsInstance() + val messageCacheClear = events.filterIsInstance() + + init { + updatesParser.onNewMessage { publish(it) } + updatesParser.onMessageEdited { publish(it) } + updatesParser.onMessageIncomingRead { publish(it) } + updatesParser.onMessageOutgoingRead { publish(it) } + updatesParser.onMessageDeleted { publish(it) } + updatesParser.onMessageRestored { publish(it) } + updatesParser.onMessageUpdated { publish(it) } + updatesParser.onMessageCacheClear { publish(it) } + updatesParser.onMessageMarkedAsImportant { publish(it) } + updatesParser.onMessageMarkedAsSpam { publish(it) } + updatesParser.onMessageMarkedAsNotSpam { publish(it) } + updatesParser.onInteractions { publish(it) } + updatesParser.onChatMajorChanged { publish(it) } + updatesParser.onChatMinorChanged { publish(it) } + updatesParser.onChatCleared { publish(it) } + updatesParser.onChatArchived { publish(it) } + } + + private fun publish(event: LongPollParsedEvent) { + scope.launch { + runCatching { applyCommon(event) } + .onFailure { throwable -> + Log.e("LongPollUpdatesReducer", "applyCommon failed: $event", throwable) + } + _events.emit(event) + } + } + + private suspend fun applyCommon(event: LongPollParsedEvent) { + when (event) { + is LongPollParsedEvent.NewMessage -> { + messagesUseCase.storeMessages(listOf(event.message)) + updateConvoForMessage(event.message, unreadIncrement = if (event.message.isOut) 0 else 1) + } + + is LongPollParsedEvent.MessageEdited -> { + messagesUseCase.storeMessage(event.message) + } + + is LongPollParsedEvent.MessageUpdated -> { + messagesUseCase.storeMessage(event.message) + } + + is LongPollParsedEvent.MessageCacheClear -> { + messagesUseCase.storeMessage(event.message) + } + + is LongPollParsedEvent.IncomingMessageRead -> { + updateConvoReadState( + peerId = event.peerId, + inReadCmId = event.cmId, + unreadCount = event.unreadCount + ) + } + + is LongPollParsedEvent.OutgoingMessageRead -> { + updateConvoReadState( + peerId = event.peerId, + outReadCmId = event.cmId, + unreadCount = event.unreadCount + ) + } + + is LongPollParsedEvent.MessageDeleted -> { + val message = messagesUseCase.getLocalMessageByConvoMessageId( + convoId = event.peerId, + cmId = event.cmId + ) + if (message != null) { + messagesUseCase.deleteLocalMessages(listOf(message.id)) + } + } + + is LongPollParsedEvent.MessageRestored -> { + messagesUseCase.storeMessage(event.message) + } + + is LongPollParsedEvent.MessageMarkedAsImportant -> { + val message = messagesUseCase.getLocalMessageByConvoMessageId( + convoId = event.peerId, + cmId = event.cmId + ) ?: return + + messagesUseCase.storeMessage(message.copy(isImportant = event.marked)) + } + + is LongPollParsedEvent.MessageMarkedAsSpam -> { + val message = messagesUseCase.getLocalMessageByConvoMessageId( + convoId = event.peerId, + cmId = event.cmId + ) + if (message != null) { + messagesUseCase.deleteLocalMessages(listOf(message.id)) + } + } + + is LongPollParsedEvent.MessageMarkedAsNotSpam -> { + messagesUseCase.storeMessage(event.message) + } + + is LongPollParsedEvent.ChatMajorChanged -> { + updateConvoSortState(event.peerId, majorId = event.majorId) + } + + is LongPollParsedEvent.ChatMinorChanged -> { + updateConvoSortState(event.peerId, minorId = event.minorId) + } + + is LongPollParsedEvent.ChatCleared -> { + convoUseCase.deleteLocalConvo(event.peerId) + } + + is LongPollParsedEvent.ChatArchived -> { + event.convo.lastMessage?.let(messagesUseCase::storeMessage) + convoUseCase.storeConvos(listOf(event.convo.copy(isArchived = event.archived))) + } + + is LongPollParsedEvent.Interaction -> Unit + } + } + + private suspend fun updateConvoReadState( + peerId: Long, + inReadCmId: Long? = null, + outReadCmId: Long? = null, + unreadCount: Int + ) { + val convo = convoUseCase.getLocalConvoById(peerId) ?: return + convoUseCase.storeConvos( + listOf( + convo.copy( + inReadCmId = inReadCmId ?: convo.inReadCmId, + outReadCmId = outReadCmId ?: convo.outReadCmId, + unreadCount = unreadCount + ) + ) + ) + } + + private suspend fun updateConvoSortState( + peerId: Long, + majorId: Int? = null, + minorId: Int? = null + ) { + val convo = convoUseCase.getLocalConvoById(peerId) ?: return + convoUseCase.storeConvos( + listOf( + convo.copy( + majorId = majorId ?: convo.majorId, + minorId = minorId ?: convo.minorId + ) + ) + ) + } + + private suspend fun updateConvoForMessage( + message: VkMessage, + unreadIncrement: Int + ) { + val convo = convoUseCase.getLocalConvoById(message.peerId) ?: return + convoUseCase.storeConvos( + listOf( + convo.copy( + lastMessageId = message.id, + lastCmId = message.cmId, + unreadCount = convo.unreadCount + unreadIncrement + ) + ) + ) + } +} diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/LongPollUseCase.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/LongPollUseCase.kt index 40e73c22..f2b6dc11 100644 --- a/core/domain/src/main/kotlin/dev/meloda/fast/domain/LongPollUseCase.kt +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/LongPollUseCase.kt @@ -1,6 +1,7 @@ package dev.meloda.fast.domain import dev.meloda.fast.data.State +import dev.meloda.fast.model.api.data.LongPollHistoryResponse import dev.meloda.fast.model.api.data.LongPollUpdates import dev.meloda.fast.model.api.data.VkLongPollData import kotlinx.coroutines.flow.Flow @@ -21,4 +22,14 @@ interface LongPollUseCase { mode: Int, version: Int ): Flow> + + fun getLongPollHistory( + ts: Int, + pts: Int, + lpVersion: Int, + lastN: Int? = null, + maxMsgId: Long? = null, + eventsLimit: Int? = null, + msgsLimit: Int? = null + ): Flow> } diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/LongPollUseCaseImpl.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/LongPollUseCaseImpl.kt index b4ea2a79..042a4752 100644 --- a/core/domain/src/main/kotlin/dev/meloda/fast/domain/LongPollUseCaseImpl.kt +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/LongPollUseCaseImpl.kt @@ -3,6 +3,7 @@ package dev.meloda.fast.domain import dev.meloda.fast.data.State import dev.meloda.fast.data.api.longpoll.LongPollRepository import dev.meloda.fast.data.mapToState +import dev.meloda.fast.model.api.data.LongPollHistoryResponse import dev.meloda.fast.model.api.data.LongPollUpdates import dev.meloda.fast.model.api.data.VkLongPollData import kotlinx.coroutines.flow.Flow @@ -48,4 +49,27 @@ class LongPollUseCaseImpl( ).mapToState() emit(newState) } + + override fun getLongPollHistory( + ts: Int, + pts: Int, + lpVersion: Int, + lastN: Int?, + maxMsgId: Long?, + eventsLimit: Int?, + msgsLimit: Int? + ): Flow> = flow { + emit(State.Loading) + + val newState = repository.getLongPollHistory( + ts = ts, + pts = pts, + lpVersion = lpVersion, + lastN = lastN, + maxMsgId = maxMsgId, + eventsLimit = eventsLimit, + msgsLimit = msgsLimit + ).mapToState() + emit(newState) + } } diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/MessagesUseCase.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/MessagesUseCase.kt index e5d880ef..5bf58382 100644 --- a/core/domain/src/main/kotlin/dev/meloda/fast/domain/MessagesUseCase.kt +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/MessagesUseCase.kt @@ -12,6 +12,11 @@ interface MessagesUseCase : BaseUseCase { suspend fun storeMessage(message: VkMessage) suspend fun storeMessages(messages: List) + suspend fun getLocalMessages(convoId: Long): List + suspend fun getLocalMessageById(messageId: Long): VkMessage? + suspend fun getLocalMessageByConvoMessageId(convoId: Long, cmId: Long): VkMessage? + suspend fun getLocalMaxMessageId(): Long? + suspend fun deleteLocalMessages(messageIds: List) fun getMessagesHistory( convoId: Long, diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/MessagesUseCaseImpl.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/MessagesUseCaseImpl.kt index 3bbd40d7..255119c7 100644 --- a/core/domain/src/main/kotlin/dev/meloda/fast/domain/MessagesUseCaseImpl.kt +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/MessagesUseCaseImpl.kt @@ -22,6 +22,26 @@ class MessagesUseCaseImpl( repository.storeMessages(messages) } + override suspend fun getLocalMessages(convoId: Long): List { + return repository.getLocalMessages(convoId) + } + + override suspend fun getLocalMessageById(messageId: Long): VkMessage? { + return repository.getLocalMessageById(messageId) + } + + override suspend fun getLocalMessageByConvoMessageId(convoId: Long, cmId: Long): VkMessage? { + return repository.getLocalMessageByConvoMessageId(convoId, cmId) + } + + override suspend fun getLocalMaxMessageId(): Long? { + return repository.getLocalMaxMessageId() + } + + override suspend fun deleteLocalMessages(messageIds: List) { + repository.deleteLocalMessages(messageIds) + } + override fun getMessagesHistory( convoId: Long, count: Int?, diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/LongPollHistoryResponse.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/LongPollHistoryResponse.kt new file mode 100644 index 00000000..02177320 --- /dev/null +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/data/LongPollHistoryResponse.kt @@ -0,0 +1,24 @@ +package dev.meloda.fast.model.api.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class LongPollHistoryResponse( + @Json(name = "history") val history: List>? = null, + @Json(name = "messages") val messages: Messages? = null, + @Json(name = "profiles") val profiles: List? = null, + @Json(name = "groups") val groups: List? = null, + @Json(name = "new_pts") val newPts: Int? = null, + @Json(name = "from_pts") val fromPts: Int? = null, + @Json(name = "ts") val ts: Int? = null, + @Json(name = "pts") val pts: Int? = null, + @Json(name = "more") val more: Int? = null, + @Json(name = "conversations") val conversations: List? = null +) { + @JsonClass(generateAdapter = true) + data class Messages( + @Json(name = "count") val count: Int? = null, + @Json(name = "items") val items: List? = null + ) +} diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/requests/LongPollRequests.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/requests/LongPollRequests.kt index f3458ca6..16c27611 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/requests/LongPollRequests.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/requests/LongPollRequests.kt @@ -19,3 +19,27 @@ data class LongPollGetUpdatesRequest( "version" to version.toString() ) } + +data class LongPollGetHistoryRequest( + val ts: Int, + val pts: Int, + val lpVersion: Int, + val lastN: Int? = null, + val maxMsgId: Long? = null, + val eventsLimit: Int? = null, + val msgsLimit: Int? = null, + val extended: Boolean = true, +) { + val map: Map + get() = mutableMapOf( + "ts" to ts.toString(), + "pts" to pts.toString(), + "lp_version" to lpVersion.toString(), + "extended" to if (extended) "1" else "0", + ).apply { + lastN?.let { this["last_n"] = it.toString() } + maxMsgId?.let { this["max_msg_id"] = it.toString() } + eventsLimit?.let { this["events_limit"] = it.toString() } + msgsLimit?.let { this["msgs_limit"] = it.toString() } + } +} diff --git a/core/network/src/main/kotlin/dev/meloda/fast/network/service/messages/MessagesService.kt b/core/network/src/main/kotlin/dev/meloda/fast/network/service/messages/MessagesService.kt index e14cbe2b..45ebefd8 100644 --- a/core/network/src/main/kotlin/dev/meloda/fast/network/service/messages/MessagesService.kt +++ b/core/network/src/main/kotlin/dev/meloda/fast/network/service/messages/MessagesService.kt @@ -2,6 +2,7 @@ package dev.meloda.fast.network.service.messages import com.slack.eithernet.ApiResult import dev.meloda.fast.model.api.data.VkChatData +import dev.meloda.fast.model.api.data.LongPollHistoryResponse import dev.meloda.fast.model.api.data.VkLongPollData import dev.meloda.fast.model.api.data.VkMessageData import dev.meloda.fast.model.api.responses.MessagesCreateChatResponse @@ -44,6 +45,12 @@ interface MessagesService { @FieldMap params: Map ): ApiResult, RestApiError> + @FormUrlEncoded + @POST(MessagesUrls.GET_LONG_POLL_HISTORY) + suspend fun getLongPollHistory( + @FieldMap params: Map + ): ApiResult, RestApiError> + @FormUrlEncoded @POST(MessagesUrls.MARK_AS_READ) suspend fun markAsRead( 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 1606c756..753fbeb1 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 @@ -8,11 +8,11 @@ import androidx.lifecycle.viewModelScope import coil.ImageLoader import coil.request.ImageRequest import com.conena.nanokt.collections.indexOfFirstOrNull -import dev.meloda.fast.common.VkConstants import dev.meloda.fast.common.extensions.createTimerFlow import dev.meloda.fast.common.extensions.findWithIndex import dev.meloda.fast.common.extensions.listenValue import dev.meloda.fast.common.extensions.setValue +import dev.meloda.fast.common.extensions.launchDbRefresh import dev.meloda.fast.common.extensions.updateValue import dev.meloda.fast.common.paging.canPaginate as canPaginatePage import dev.meloda.fast.common.paging.isPaginationExhausted as isPaginationExhaustedPage @@ -27,8 +27,7 @@ import dev.meloda.fast.data.VkUtils import dev.meloda.fast.data.processState import dev.meloda.fast.datastore.UserSettings import dev.meloda.fast.domain.ConvoUseCase -import dev.meloda.fast.domain.LoadConvosByIdUseCase -import dev.meloda.fast.domain.LongPollUpdatesParser +import dev.meloda.fast.domain.LongPollUpdatesReducer import dev.meloda.fast.domain.MessagesUseCase import dev.meloda.fast.domain.util.asPresentation import dev.meloda.fast.domain.util.extractAvatar @@ -44,20 +43,21 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch class ConvosViewModel( - updatesParser: LongPollUpdatesParser, + updatesReducer: LongPollUpdatesReducer, private val filter: ConvosFilter, private val convoUseCase: ConvoUseCase, private val messagesUseCase: MessagesUseCase, private val resources: Resources, private val userSettings: UserSettings, private val imageLoader: ImageLoader, - private val applicationContext: Context, - private val loadConvosByIdUseCase: LoadConvosByIdUseCase + private val applicationContext: Context ) : ViewModel() { private val _screenState = MutableStateFlow(ConvosScreenState.EMPTY) val screenState = _screenState.asStateFlow() @@ -98,15 +98,15 @@ class ConvosViewModel( loadConvos() - updatesParser.onNewMessage(::handleNewMessage) - updatesParser.onMessageEdited(::handleEditedMessage) - updatesParser.onMessageIncomingRead(::handleReadIncomingMessage) - updatesParser.onMessageOutgoingRead(::handleReadOutgoingMessage) - updatesParser.onInteractions(::handleInteraction) - updatesParser.onChatMajorChanged(::handleChatMajorChanged) - updatesParser.onChatMinorChanged(::handleChatMinorChanged) - updatesParser.onChatCleared(::handleChatClearing) - updatesParser.onChatArchived(::handleChatArchived) + updatesReducer.newMessages.onEach(::handleNewMessage).launchIn(viewModelScope) + updatesReducer.messageEdited.onEach(::handleEditedMessage).launchIn(viewModelScope) + updatesReducer.messageIncomingRead.onEach(::handleReadIncomingMessage).launchIn(viewModelScope) + updatesReducer.messageOutgoingRead.onEach(::handleReadOutgoingMessage).launchIn(viewModelScope) + updatesReducer.interactions.onEach(::handleInteraction).launchIn(viewModelScope) + updatesReducer.chatMajorChanged.onEach(::handleChatMajorChanged).launchIn(viewModelScope) + updatesReducer.chatMinorChanged.onEach(::handleChatMinorChanged).launchIn(viewModelScope) + updatesReducer.chatCleared.onEach(::handleChatClearing).launchIn(viewModelScope) + updatesReducer.chatArchived.onEach(::handleChatArchived).launchIn(viewModelScope) userSettings.useContactNames.listenValue(viewModelScope) { syncUiConvos() @@ -257,6 +257,10 @@ class ConvosViewModel( private fun loadConvos( offset: Int = currentOffset.value ) { + if (offset == 0) { + refreshConvosFromDb() + } + convoUseCase.getConvos( count = LOAD_COUNT, offset = offset, @@ -313,14 +317,10 @@ class ConvosViewModel( state.processState( error = {}, success = { - val newConvos = convos.value.toMutableList() - val convoIndex = - newConvos.indexOfFirstOrNull { it.id == peerId } - ?: return@processState - - newConvos.removeAt(convoIndex) - _convos.update { newConvos.sorted() } - syncUiConvos() + viewModelScope.launch { + convoUseCase.deleteLocalConvo(peerId) + refreshConvosFromDb() + } } ) _screenState.emit(screenState.value.copy(isLoading = state.isLoading())) @@ -333,16 +333,22 @@ class ConvosViewModel( state.processState( error = {}, success = { - handleChatMajorChanged( - LongPollParsedEvent.ChatMajorChanged( - peerId = peerId, - majorId = if (pin) { - pinnedConvosCount.value.plus(1) * 16 - } else { - 0 - } - ) - ) + viewModelScope.launch { + convoUseCase.getLocalConvoById(peerId)?.let { convo -> + convoUseCase.storeConvos( + listOf( + convo.copy( + majorId = if (pin) { + pinnedConvosCount.value.plus(1) * 16 + } else { + 0 + } + ) + ) + ) + } + refreshConvosFromDb() + } } ) @@ -356,145 +362,35 @@ class ConvosViewModel( state.processState( error = {}, success = { - convos.value.find { it.id == peerId }?.let { convo -> - handleChatArchived( - LongPollParsedEvent.ChatArchived( - convo = convo, - archived = archive + viewModelScope.launch { + convoUseCase.getLocalConvoById(peerId)?.let { convo -> + convoUseCase.storeConvos( + listOf( + convo.copy(isArchived = archive) + ) ) - ) + } + refreshConvosFromDb() } } ) } } - // TODO: 03-Apr-25, Danil Nikolaev: handle business messages private fun handleNewMessage(event: LongPollParsedEvent.NewMessage) { - val message = event.message - - val newConvos = convos.value.toMutableList() - val convoIndex = - newConvos.indexOfFirstOrNull { it.id == message.peerId } - - if (convoIndex == null) { - if (event.inArchive != (filter == ConvosFilter.ARCHIVE)) return - - loadConvosByIdUseCase( - peerIds = listOf(message.peerId), - extended = true, - fields = VkConstants.ALL_FIELDS - ).listenValue(viewModelScope) { state -> - state.processState( - error = {}, - success = { response -> - val convo = (response.firstOrNull() ?: return@listenValue) - .copy(lastMessage = message) - - newConvos.add(pinnedConvosCount.value, convo) - _convos.update { newConvos.sorted() } - syncUiConvos() - } - ) - } - } else { - val convo = newConvos[convoIndex] - var newConvo = convo.copy( - lastMessage = message, - lastMessageId = message.id, - lastCmId = message.cmId, - unreadCount = if (message.isOut) convo.unreadCount - else convo.unreadCount + 1 - ) - - interactionsTimers[convo.id]?.let { job -> - if (job.interactionType == InteractionType.Typing - && message.fromId in convo.interactionIds - ) { - val newInteractionIds = newConvo.interactionIds.filter { id -> - id != message.fromId - } - - newConvo = newConvo.copy( - interactionType = if (newInteractionIds.isEmpty()) -1 else { - newConvo.interactionType - }, - interactionIds = newInteractionIds - ) - } - } - - if (convo.isPinned()) { - newConvos[convoIndex] = newConvo - } else { - newConvos.removeAt(convoIndex) - - val toPosition = pinnedConvosCount.value - newConvos.add(toPosition, newConvo) - } - - _convos.update { newConvos.sorted() } - syncUiConvos() - } + refreshConvosFromDb() } private fun handleEditedMessage(event: LongPollParsedEvent.MessageEdited) { - val message = event.message - val newConvos = convos.value.toMutableList() - - val convoIndex = newConvos.indexOfFirstOrNull { it.id == message.peerId } - if (convoIndex == null) { // диалога нет в списке - // pizdets - } else { - val convo = newConvos[convoIndex] - newConvos[convoIndex] = convo.copy( - lastMessage = message, - lastMessageId = message.id, - lastCmId = message.cmId - ) - _convos.update { newConvos } - syncUiConvos() - } + refreshConvosFromDb() } private fun handleReadIncomingMessage(event: LongPollParsedEvent.IncomingMessageRead) { - val newConvos = convos.value.toMutableList() - - val convoIndex = - newConvos.indexOfFirstOrNull { it.id == event.peerId } - - if (convoIndex == null) { // диалога нет в списке - // pizdets - } else { - newConvos[convoIndex] = - newConvos[convoIndex].copy( - inReadCmId = event.cmId, - unreadCount = event.unreadCount - ) - - _convos.update { newConvos } - syncUiConvos() - } + refreshConvosFromDb() } private fun handleReadOutgoingMessage(event: LongPollParsedEvent.OutgoingMessageRead) { - val newConvos = convos.value.toMutableList() - - val convoIndex = - newConvos.indexOfFirstOrNull { it.id == event.peerId } - - if (convoIndex == null) { // диалога нет в списке - // pizdets - } else { - newConvos[convoIndex] = - newConvos[convoIndex].copy( - outReadCmId = event.cmId, - unreadCount = event.unreadCount - ) - - _convos.update { newConvos } - syncUiConvos() - } + refreshConvosFromDb() } private fun handleInteraction(event: LongPollParsedEvent.Interaction) { @@ -563,88 +459,19 @@ class ConvosViewModel( } private fun handleChatMajorChanged(event: LongPollParsedEvent.ChatMajorChanged) { - val newConvos = convos.value.toMutableList() - val convoIndex = - newConvos.indexOfFirstOrNull { it.id == event.peerId } - - if (convoIndex == null) { // диалога нет в списке - // pizdets - } else { - newConvos[convoIndex] = - newConvos[convoIndex].copy(majorId = event.majorId) - - _convos.setValue { newConvos.sorted() } - syncUiConvos() - } + refreshConvosFromDb() } private fun handleChatMinorChanged(event: LongPollParsedEvent.ChatMinorChanged) { - val newConvos = convos.value.toMutableList() - val convoIndex = - newConvos.indexOfFirstOrNull { it.id == event.peerId } - - if (convoIndex == null) { // диалога нет в списке - // pizdets - } else { - newConvos[convoIndex] = - newConvos[convoIndex].copy(minorId = event.minorId) - - _convos.setValue { newConvos.sorted() } - syncUiConvos() - } + refreshConvosFromDb() } private fun handleChatClearing(event: LongPollParsedEvent.ChatCleared) { - val newConvos = convos.value.toMutableList() - - val convoIndex = newConvos.indexOfFirstOrNull { it.id == event.peerId } - - if (convoIndex == null) { // диалога нет в списке - // pizdets - } else { - newConvos.removeAt(convoIndex) - - _convos.setValue { newConvos.sorted() } - syncUiConvos() - } + refreshConvosFromDb() } private fun handleChatArchived(event: LongPollParsedEvent.ChatArchived) { - val convo = event.convo - - val newConvos = convos.value.toMutableList() - - when (filter) { - ConvosFilter.BUSINESS_NOTIFY -> Unit - - ConvosFilter.ARCHIVE -> { - if (event.archived) { - newConvos.add(0, convo) - } else { - val index = newConvos.indexOfFirstOrNull { it.id == convo.id } - if (index == null) return - - newConvos.removeAt(index) - } - - _convos.update { newConvos } - syncUiConvos() - } - - else -> { - if (event.archived) { - val index = newConvos.indexOfFirstOrNull { it.id == convo.id } - if (index == null) return - - newConvos.removeAt(index) - } else { - newConvos.add(pinnedConvosCount.value, convo) - } - - _convos.update { newConvos.sorted() } - syncUiConvos() - } - } + refreshConvosFromDb() } private fun readConvo(peerId: Long, startMessageId: Long) { @@ -655,21 +482,42 @@ class ConvosViewModel( state.processState( error = {}, success = { - val newConvos = convos.value.toMutableList() - val convoIndex = - newConvos.indexOfFirstOrNull { it.id == peerId } - ?: return@listenValue - - newConvos[convoIndex] = - newConvos[convoIndex].copy(inRead = startMessageId) - - _convos.update { newConvos } - syncUiConvos() + viewModelScope.launch { + convoUseCase.getLocalConvoById(peerId)?.let { convo -> + convoUseCase.storeConvos( + listOf( + convo.copy( + inRead = startMessageId, + unreadCount = 0 + ) + ) + ) + } + refreshConvosFromDb() + } } ) } } + private fun refreshConvosFromDb() { + viewModelScope.launchDbRefresh( + load = { + val localConvos = convoUseCase.getLocalConvos() + + val filteredConvos = when (filter) { + ConvosFilter.ARCHIVE -> localConvos.filter(VkConvo::isArchived) + ConvosFilter.UNREAD -> localConvos.filter { !it.isArchived && it.unreadCount > 0 } + ConvosFilter.ALL -> localConvos.filterNot(VkConvo::isArchived) + ConvosFilter.BUSINESS_NOTIFY -> localConvos + } + + _convos.emit(filteredConvos) + }, + after = ::syncUiConvos + ) + } + private fun List.sorted(): List { val newConvos = toMutableList() diff --git a/feature/convos/src/main/kotlin/dev/meloda/fast/convos/di/ConvosModule.kt b/feature/convos/src/main/kotlin/dev/meloda/fast/convos/di/ConvosModule.kt index b54191ef..aaf8cc91 100644 --- a/feature/convos/src/main/kotlin/dev/meloda/fast/convos/di/ConvosModule.kt +++ b/feature/convos/src/main/kotlin/dev/meloda/fast/convos/di/ConvosModule.kt @@ -31,7 +31,6 @@ private fun Scope.createConvosViewModel(filter: ConvosFilter): ConvosViewModel { resources = get(), userSettings = get(), imageLoader = get(), - applicationContext = get(), - loadConvosByIdUseCase = get() + applicationContext = get() ) } diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryLoaders.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryLoaders.kt index 7f2da1c7..23bd31fc 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryLoaders.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryLoaders.kt @@ -3,6 +3,7 @@ package dev.meloda.fast.messageshistory import android.util.Log import dev.meloda.fast.common.VkConstants import dev.meloda.fast.common.extensions.listenValue +import dev.meloda.fast.common.extensions.launchDbRefresh import dev.meloda.fast.common.extensions.setValue import dev.meloda.fast.common.paging.canPaginate as canPaginatePage import dev.meloda.fast.common.paging.isPaginationExhausted as isPaginationExhaustedPage @@ -39,6 +40,29 @@ internal class MessagesHistoryLoaders( fun loadConvo() { Log.d("MessagesHistoryViewModelImpl", "loadConvo()") + scope.launchDbRefresh( + load = { + convoUseCase.getLocalConvoById(screenState.value.convoId)?.let { convo -> + val title = convo.extractTitle( + useContactName = AppSettings.General.useContactNames, + resources = resourceProvider.resources + ) + val avatar = convo.extractAvatar() + + screenState.setValue { old -> + old.copy( + convo = convo, + title = title, + avatar = avatar + ) + } + + onPinnedMessage(convo.pinnedMessage) + } + }, + after = {} + ) + convoUseCase.getById( peerIds = listOf(screenState.value.convoId), extended = true, @@ -71,6 +95,22 @@ internal class MessagesHistoryLoaders( fun loadMessagesHistory(offset: Int) { Log.d("MessagesHistoryViewModel", "loadMessagesHistory: $offset") + if (offset == 0) { + scope.launchDbRefresh( + load = { + val cachedMessages = messagesUseCase.getLocalMessages(screenState.value.convoId) + if (cachedMessages.isNotEmpty()) { + messages.emit(cachedMessages.sorted()) + } + }, + after = { + if (messages.value.isNotEmpty()) { + syncUiMessages() + } + } + ) + } + messagesUseCase.getMessagesHistory( convoId = screenState.value.convoId, count = MessagesHistoryViewModelImpl.MESSAGES_LOAD_COUNT, diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryLongPollEventHandler.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryLongPollEventHandler.kt index b8020390..2c819a11 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryLongPollEventHandler.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryLongPollEventHandler.kt @@ -1,18 +1,24 @@ package dev.meloda.fast.messageshistory import android.util.Log -import com.conena.nanokt.collections.indexOfFirstOrNull +import dev.meloda.fast.common.extensions.launchDbRefresh import dev.meloda.fast.common.extensions.setValue +import dev.meloda.fast.domain.ConvoUseCase +import dev.meloda.fast.domain.MessagesUseCase import dev.meloda.fast.messageshistory.model.MessagesHistoryScreenState import dev.meloda.fast.model.LongPollParsedEvent import dev.meloda.fast.model.api.domain.VkMessage +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow -import kotlin.math.abs internal class MessagesHistoryLongPollEventHandler( + private val scope: CoroutineScope, + private val convoUseCase: ConvoUseCase, + private val messagesUseCase: MessagesUseCase, private val screenState: MutableStateFlow, private val messages: MutableStateFlow>, - private val syncUiMessages: () -> Unit + private val syncUiMessages: () -> Unit, + private val onPinnedMessageChanged: (VkMessage?) -> Unit ) { fun onNewMessage(event: LongPollParsedEvent.NewMessage) { val message = event.message @@ -20,139 +26,74 @@ internal class MessagesHistoryLongPollEventHandler( Log.d("MessagesHistoryViewModel", "handleNewMessage: $message") if (message.peerId != screenState.value.convoId) return - if (messages.value.indexOfFirstOrNull { it.id == message.id } != null) return - - val randomIds = messages.value.map(VkMessage::randomId) - if (message.randomId != 0L && message.randomId in randomIds) return - - val newMessages = messages.value.toMutableList() - newMessages.add(0, message) - - messages.setValue { newMessages } - syncUiMessages() + refreshFromDb(refreshMessages = true) } fun onMessageEdited(event: LongPollParsedEvent.MessageEdited) { val message = event.message if (message.peerId != screenState.value.convoId) return - val newMessages = messages.value.toMutableList() - val index = newMessages.indexOfFirstOrNull { it.id == message.id } - if (index == null) { - return - } - - newMessages[index] = message - messages.setValue { newMessages } - syncUiMessages() + refreshFromDb(refreshMessages = true) } fun onReadIncoming(event: LongPollParsedEvent.IncomingMessageRead) { if (event.peerId != screenState.value.convoId) return - val index = messages.value.indexOfFirstOrNull { it.cmId == event.cmId } - if (index == null) return - - val newConvo = screenState.value.convo.copy( - inReadCmId = event.cmId - ) - - screenState.setValue { old -> - old.copy(convo = newConvo) - } - - syncUiMessages() + refreshFromDb(refreshMessages = false) } fun onReadOutgoing(event: LongPollParsedEvent.OutgoingMessageRead) { if (event.peerId != screenState.value.convoId) return - val index = messages.value.indexOfFirstOrNull { it.cmId == event.cmId } - if (index == null) return - - val newConvo = screenState.value.convo.copy( - outReadCmId = event.cmId - ) - - screenState.setValue { old -> - old.copy(convo = newConvo) - } - - syncUiMessages() + refreshFromDb(refreshMessages = false) } fun onMessageDeleted(event: LongPollParsedEvent.MessageDeleted) { if (event.peerId != screenState.value.convoId) return - val newMessages = messages.value.toMutableList() - val index = newMessages.indexOfFirstOrNull { it.cmId == event.cmId } - if (index == null) return - - newMessages.removeAt(index) - messages.setValue { newMessages } - syncUiMessages() + refreshFromDb(refreshMessages = true) } fun onMessageRestored(event: LongPollParsedEvent.MessageRestored) { if (event.message.peerId != screenState.value.convoId) return - val newMessages = messages.value.toMutableList() - val minDate = newMessages.minOf(VkMessage::date) - - if (event.message.date < minDate) return - - newMessages.add(event.message) - messages.setValue { newMessages.sorted() } - syncUiMessages() + refreshFromDb(refreshMessages = true) } fun onMessageMarkedAsImportant(event: LongPollParsedEvent.MessageMarkedAsImportant) { if (event.peerId != screenState.value.convoId) return - val newMessages = messages.value.toMutableList() - val index = newMessages.indexOfFirstOrNull { it.cmId == event.cmId } - if (index == null) return - - newMessages[index] = newMessages[index].copy(isImportant = event.marked) - messages.setValue { newMessages } - syncUiMessages() + refreshFromDb(refreshMessages = true) } fun onMessageMarkedAsSpam(event: LongPollParsedEvent.MessageMarkedAsSpam) { if (event.peerId != screenState.value.convoId) return - val newMessages = messages.value.toMutableList() - val index = newMessages.indexOfFirstOrNull { it.cmId == event.cmId } - if (index == null) return - - newMessages.removeAt(index) - messages.setValue { newMessages } - syncUiMessages() + refreshFromDb(refreshMessages = true) } fun onMessageMarkedAsNotSpam(event: LongPollParsedEvent.MessageMarkedAsNotSpam) { if (event.message.peerId != screenState.value.convoId) return - val newMessages = messages.value.toMutableList() - val maxDate = newMessages.maxOf(VkMessage::date) - val minDate = newMessages.minOf(VkMessage::date) - - if (event.message.date !in minDate..maxDate) return - - newMessages.add(event.message) - messages.setValue { newMessages.sorted() } - syncUiMessages() + refreshFromDb(refreshMessages = true) } - private fun List.sorted(): List { - return sortedWith { m1, m2 -> - val dateDiff = m2.date - m1.date - if (dateDiff != 0) { - dateDiff - } else { - val idDiff = m2.id - m1.id - idDiff.toInt() - } - } + private fun refreshFromDb(refreshMessages: Boolean) { + scope.launchDbRefresh( + load = { + convoUseCase.getLocalConvoById(screenState.value.convoId)?.let { convo -> + screenState.setValue { old -> + old.copy(convo = convo) + } + onPinnedMessageChanged(convo.pinnedMessage) + } + + if (refreshMessages) { + val localMessages = messagesUseCase.getLocalMessages(screenState.value.convoId) + messages.setValue { localMessages } + } + }, + after = ::syncUiMessages + ) } } diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryMessageActions.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryMessageActions.kt index f5d3151f..8030e0dc 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryMessageActions.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryMessageActions.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextDecoration import dev.meloda.fast.common.extensions.listenValue +import dev.meloda.fast.common.extensions.launchDbRefresh import dev.meloda.fast.common.extensions.removeIfCompat import dev.meloda.fast.common.extensions.setValue import dev.meloda.fast.common.provider.ResourceProvider @@ -214,13 +215,15 @@ internal class MessagesHistoryMessageActions( syncUiMessages() }, success = { response -> - val newMessages = messages.value.toMutableList() - newMessages[newMessages.indexOf(newMessage)] = newMessage.copy( - id = response.messageId, - cmId = response.cmId - ) - messages.setValue { newMessages } - syncUiMessages() + viewModelScope.launch { + messagesUseCase.storeMessage( + newMessage.copy( + id = response.messageId, + cmId = response.cmId + ) + ) + refreshMessagesFromDb() + } } ) } @@ -232,10 +235,9 @@ internal class MessagesHistoryMessageActions( } fun editCurrentEditMessage() { - replyToCmId = null - val newText = screenState.value.message.text val lastText = lastMessageText.orEmpty().trim() + val currentReplyToCmId = replyToCmId screenState.setValue { old -> old.copy( @@ -253,11 +255,33 @@ internal class MessagesHistoryMessageActions( syncUiMessages() val newMessage = editMessage?.copy( - replyMessage = if (replyToCmId == null) null else editMessage?.replyMessage, + replyMessage = if (currentReplyToCmId == null) null else editMessage?.replyMessage, text = newText ) ?: return Log.d("MessagesHistoryViewModelImpl", "editMessage: $newMessage") + + messagesUseCase.edit( + peerId = screenState.value.convoId, + cmId = newMessage.cmId, + message = newMessage.text, + attachments = null, + formatData = newMessage.formatData + ).listenValue(viewModelScope) { state -> + state.processState( + error = { error -> + Log.d("MessagesHistoryViewModelImpl", "editMessage: ERROR: $error") + }, + success = { + viewModelScope.launch { + messagesUseCase.storeMessage(newMessage) + refreshMessagesFromDb() + } + } + ) + } + + replyToCmId = null } private fun updateFormatting(type: FormatDataType) { @@ -313,4 +337,14 @@ internal class MessagesHistoryMessageActions( } } + private fun refreshMessagesFromDb() { + viewModelScope.launchDbRefresh( + load = { + val localMessages = messagesUseCase.getLocalMessages(screenState.value.convoId) + messages.setValue { localMessages } + }, + after = ::syncUiMessages + ) + } + } diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryMessageTransportActions.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryMessageTransportActions.kt index a7333248..92ed270b 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryMessageTransportActions.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryMessageTransportActions.kt @@ -10,12 +10,13 @@ import androidx.core.content.FileProvider import androidx.core.graphics.drawable.toBitmapOrNull import coil.imageLoader import coil.request.ImageRequest -import com.conena.nanokt.collections.indexOfFirstOrNull +import dev.meloda.fast.common.extensions.launchDbRefresh import dev.meloda.fast.common.extensions.listenValue import dev.meloda.fast.common.extensions.setValue import dev.meloda.fast.data.State import dev.meloda.fast.data.VkUtils import dev.meloda.fast.data.processState +import dev.meloda.fast.domain.ConvoUseCase import dev.meloda.fast.domain.MessagesUseCase import dev.meloda.fast.messageshistory.model.MessagesHistoryScreenState import dev.meloda.fast.model.BaseError @@ -33,6 +34,7 @@ import java.io.FileOutputStream internal class MessagesHistoryMessageTransportActions( private val applicationContext: Context, private val viewModelScope: CoroutineScope, + private val convoUseCase: ConvoUseCase, private val messagesUseCase: MessagesUseCase, private val screenState: MutableStateFlow, private val messages: MutableStateFlow>, @@ -49,17 +51,14 @@ internal class MessagesHistoryMessageTransportActions( state.processState( error = ::handleError, success = { - val newMessages = messages.value - .toMutableList() - .map { message -> - if (message.id in messageIds) { - message.copy(isImportant = important) - } else { - message + viewModelScope.launch { + messageIds.forEach { messageId -> + messagesUseCase.getLocalMessageById(messageId)?.let { localMessage -> + messagesUseCase.storeMessage(localMessage.copy(isImportant = important)) } } - messages.setValue { newMessages } - syncUiMessages() + refreshMessagesFromDb() + } } ) } @@ -80,12 +79,17 @@ internal class MessagesHistoryMessageTransportActions( state.processState( error = ::handleError, success = { - onSuccess() - val newMessages = messages.value.toMutableList() - val messagesToDelete = newMessages.filter { it.id in messageIds } - newMessages.removeAll(messagesToDelete) - messages.setValue { newMessages } - syncUiMessages() + viewModelScope.launch { + onSuccess() + val localMessageIds = mutableListOf() + messageIds.forEach { messageId -> + messagesUseCase.getLocalMessageById(messageId)?.let { localMessage -> + localMessageIds += localMessage.id + } + } + messagesUseCase.deleteLocalMessages(localMessageIds) + refreshMessagesFromDb() + } } ) } @@ -100,14 +104,10 @@ internal class MessagesHistoryMessageTransportActions( state.processState( error = ::handleError, success = { pinnedMessage -> - onPinnedMessageChanged(pinnedMessage) - - val newMessages = messages.value.toMutableList() - val index = newMessages.indexOfFirstOrNull { it.id == messageId } - if (index != null) { - newMessages[index] = pinnedMessage - messages.setValue { newMessages } - syncUiMessages() + viewModelScope.launch { + onPinnedMessageChanged(pinnedMessage) + messagesUseCase.storeMessage(pinnedMessage) + refreshMessagesFromDb() } } ) @@ -118,20 +118,18 @@ internal class MessagesHistoryMessageTransportActions( messagesUseCase.unpin(screenState.value.convoId) .listenValue(viewModelScope) { state -> state.processState( - error = ::handleError, - success = { - val newMessages = messages.value.toMutableList() - val index = newMessages.indexOfFirstOrNull { it.id == messageId } - if (index != null) { - newMessages[index] = newMessages[index].copy(isPinned = false) - messages.setValue { newMessages } - syncUiMessages() + error = ::handleError, + success = { + viewModelScope.launch { + messagesUseCase.getLocalMessageById(messageId)?.let { localMessage -> + messagesUseCase.storeMessage(localMessage.copy(isPinned = false)) } - onPinnedMessageChanged(null) + refreshMessagesFromDb() } - ) - } + } + ) + } } fun readMessage(message: VkMessage) { @@ -142,17 +140,19 @@ internal class MessagesHistoryMessageTransportActions( state.processState( error = ::handleError, success = { - val oldConvo = screenState.value.convo - val newConvo = oldConvo.copy( - inRead = if (!message.isOut) message.id else oldConvo.inRead, - outRead = if (message.isOut) message.id else oldConvo.outRead - ) - - screenState.setValue { old -> - old.copy(convo = newConvo) + viewModelScope.launch { + convoUseCase.getLocalConvoById(screenState.value.convoId)?.let { localConvo -> + convoUseCase.storeConvos( + listOf( + localConvo.copy( + inRead = if (!message.isOut) message.id else localConvo.inRead, + outRead = if (message.isOut) message.id else localConvo.outRead + ) + ) + ) + } + refreshMessagesFromDb() } - - syncUiMessages() } ) } @@ -219,4 +219,14 @@ internal class MessagesHistoryMessageTransportActions( baseError.setValue { newBaseError } } } + + private fun refreshMessagesFromDb() { + viewModelScope.launchDbRefresh( + load = { + val localMessages = messagesUseCase.getLocalMessages(screenState.value.convoId) + messages.setValue { localMessages } + }, + after = ::syncUiMessages + ) + } } 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 72a87cc4..457e27d7 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 @@ -12,10 +12,11 @@ import dev.meloda.fast.common.extensions.getParcelableCompat import dev.meloda.fast.common.extensions.listenValue import dev.meloda.fast.common.extensions.setValue import dev.meloda.fast.common.provider.ResourceProvider +import dev.meloda.fast.data.processState import dev.meloda.fast.datastore.AppSettings import dev.meloda.fast.domain.ConvoUseCase import dev.meloda.fast.domain.GetMessageReadPeersUseCase -import dev.meloda.fast.domain.LongPollUpdatesParser +import dev.meloda.fast.domain.LongPollUpdatesReducer import dev.meloda.fast.domain.MessagesUseCase import dev.meloda.fast.messageshistory.model.ActionMode import dev.meloda.fast.messageshistory.model.MessageDialog @@ -27,6 +28,8 @@ import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.ui.model.vk.MessageUiItem import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine @@ -38,7 +41,7 @@ class MessagesHistoryViewModelImpl( private val convoUseCase: ConvoUseCase, private val resourceProvider: ResourceProvider, private val getMessageReadPeersUseCase: GetMessageReadPeersUseCase, - updatesParser: LongPollUpdatesParser, + updatesReducer: LongPollUpdatesReducer, savedStateHandle: SavedStateHandle ) : MessagesHistoryViewModel, ViewModel() { @@ -80,6 +83,7 @@ class MessagesHistoryViewModelImpl( private val messageTransportActions = MessagesHistoryMessageTransportActions( applicationContext = applicationContext, viewModelScope = viewModelScope, + convoUseCase = convoUseCase, messagesUseCase = messagesUseCase, screenState = screenState, messages = messages, @@ -103,11 +107,14 @@ class MessagesHistoryViewModelImpl( ) private val longPollEventHandler = MessagesHistoryLongPollEventHandler( + scope = viewModelScope, + convoUseCase = convoUseCase, + messagesUseCase = messagesUseCase, screenState = screenState, - messages = messages - ) { - syncUiMessages() - } + messages = messages, + syncUiMessages = ::syncUiMessages, + onPinnedMessageChanged = pinnedMessageHandler::update + ) init { val arguments = MessagesHistory.from(savedStateHandle).arguments @@ -117,15 +124,15 @@ class MessagesHistoryViewModelImpl( loaders.loadConvo() loaders.loadMessagesHistory(currentOffset.value) - updatesParser.onNewMessage(longPollEventHandler::onNewMessage) - updatesParser.onMessageEdited(longPollEventHandler::onMessageEdited) - updatesParser.onMessageIncomingRead(longPollEventHandler::onReadIncoming) - updatesParser.onMessageOutgoingRead(longPollEventHandler::onReadOutgoing) - updatesParser.onMessageDeleted(longPollEventHandler::onMessageDeleted) - updatesParser.onMessageRestored(longPollEventHandler::onMessageRestored) - updatesParser.onMessageMarkedAsImportant(longPollEventHandler::onMessageMarkedAsImportant) - updatesParser.onMessageMarkedAsSpam(longPollEventHandler::onMessageMarkedAsSpam) - updatesParser.onMessageMarkedAsNotSpam(longPollEventHandler::onMessageMarkedAsNotSpam) + updatesReducer.newMessages.onEach(longPollEventHandler::onNewMessage).launchIn(viewModelScope) + updatesReducer.messageEdited.onEach(longPollEventHandler::onMessageEdited).launchIn(viewModelScope) + updatesReducer.messageIncomingRead.onEach(longPollEventHandler::onReadIncoming).launchIn(viewModelScope) + updatesReducer.messageOutgoingRead.onEach(longPollEventHandler::onReadOutgoing).launchIn(viewModelScope) + updatesReducer.messageDeleted.onEach(longPollEventHandler::onMessageDeleted).launchIn(viewModelScope) + updatesReducer.messageRestored.onEach(longPollEventHandler::onMessageRestored).launchIn(viewModelScope) + updatesReducer.messageMarkedAsImportant.onEach(longPollEventHandler::onMessageMarkedAsImportant).launchIn(viewModelScope) + updatesReducer.messageMarkedAsSpam.onEach(longPollEventHandler::onMessageMarkedAsSpam).launchIn(viewModelScope) + updatesReducer.messageMarkedAsNotSpam.onEach(longPollEventHandler::onMessageMarkedAsNotSpam).launchIn(viewModelScope) } override fun onNavigationConsumed() {