forked from melod1n/fast-messenger
refactor: unify db refresh flows
This commit is contained in:
@@ -16,11 +16,17 @@ import dev.meloda.fast.common.LongPollController
|
|||||||
import dev.meloda.fast.common.VkConstants
|
import dev.meloda.fast.common.VkConstants
|
||||||
import dev.meloda.fast.common.extensions.listenValue
|
import dev.meloda.fast.common.extensions.listenValue
|
||||||
import dev.meloda.fast.common.model.LongPollState
|
import dev.meloda.fast.common.model.LongPollState
|
||||||
|
import dev.meloda.fast.data.VkMemoryCache
|
||||||
import dev.meloda.fast.data.UserConfig
|
import dev.meloda.fast.data.UserConfig
|
||||||
import dev.meloda.fast.data.processState
|
import dev.meloda.fast.data.processState
|
||||||
import dev.meloda.fast.datastore.AppSettings
|
import dev.meloda.fast.datastore.AppSettings
|
||||||
import dev.meloda.fast.domain.LongPollUpdatesParser
|
import dev.meloda.fast.domain.LongPollUpdatesParser
|
||||||
import dev.meloda.fast.domain.LongPollUseCase
|
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.LongPollUpdates
|
||||||
import dev.meloda.fast.model.api.data.VkLongPollData
|
import dev.meloda.fast.model.api.data.VkLongPollData
|
||||||
import dev.meloda.fast.ui.R
|
import dev.meloda.fast.ui.R
|
||||||
@@ -33,6 +39,7 @@ import kotlinx.coroutines.SupervisorJob
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import kotlinx.coroutines.flow.last
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
@@ -55,6 +62,8 @@ class LongPollingService : Service() {
|
|||||||
private val coroutineScope = CoroutineScope(coroutineContext)
|
private val coroutineScope = CoroutineScope(coroutineContext)
|
||||||
|
|
||||||
private val longPollUseCase: LongPollUseCase by inject()
|
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 val updatesParser: LongPollUpdatesParser by inject()
|
||||||
|
|
||||||
private var currentJob: Job? = null
|
private var currentJob: Job? = null
|
||||||
@@ -150,6 +159,8 @@ class LongPollingService : Service() {
|
|||||||
var serverInfo = getServerInfo()
|
var serverInfo = getServerInfo()
|
||||||
?: throw LongPollException(message = "bad VK response (server info)")
|
?: throw LongPollException(message = "bad VK response (server info)")
|
||||||
|
|
||||||
|
syncLongPollHistory(serverInfo)
|
||||||
|
|
||||||
var lastUpdatesResponse: LongPollUpdates? = getUpdatesResponse(serverInfo)
|
var lastUpdatesResponse: LongPollUpdates? = getUpdatesResponse(serverInfo)
|
||||||
?: throw LongPollException(message = "initiation error: bad VK response (last updates)")
|
?: throw LongPollException(message = "initiation error: bad VK response (last updates)")
|
||||||
|
|
||||||
@@ -160,6 +171,7 @@ class LongPollingService : Service() {
|
|||||||
failCount++
|
failCount++
|
||||||
serverInfo = getServerInfo()
|
serverInfo = getServerInfo()
|
||||||
?: throw LongPollException(message = "failed retrieving server info after error: bad VK response (server info #2)")
|
?: throw LongPollException(message = "failed retrieving server info after error: bad VK response (server info #2)")
|
||||||
|
syncLongPollHistory(serverInfo)
|
||||||
lastUpdatesResponse = getUpdatesResponse(serverInfo)
|
lastUpdatesResponse = getUpdatesResponse(serverInfo)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -179,6 +191,7 @@ class LongPollingService : Service() {
|
|||||||
?: throw LongPollException(
|
?: throw LongPollException(
|
||||||
message = "failed retrieving server info after error: bad VK response (server info #3)"
|
message = "failed retrieving server info after error: bad VK response (server info #3)"
|
||||||
)
|
)
|
||||||
|
syncLongPollHistory(serverInfo)
|
||||||
lastUpdatesResponse = getUpdatesResponse(serverInfo)
|
lastUpdatesResponse = getUpdatesResponse(serverInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,6 +209,9 @@ class LongPollingService : Service() {
|
|||||||
updates.forEach(updatesParser::parseNextUpdate)
|
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))
|
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) {
|
private fun handleError(throwable: Throwable) {
|
||||||
Log.e(TAG, "error: $throwable")
|
Log.e(TAG, "error: $throwable")
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package dev.meloda.fast.service.longpolling.di
|
package dev.meloda.fast.service.longpolling.di
|
||||||
|
|
||||||
import dev.meloda.fast.domain.LongPollUpdatesParser
|
import dev.meloda.fast.domain.LongPollUpdatesParser
|
||||||
|
import dev.meloda.fast.domain.LongPollUpdatesReducer
|
||||||
import dev.meloda.fast.domain.LongPollUseCase
|
import dev.meloda.fast.domain.LongPollUseCase
|
||||||
import dev.meloda.fast.domain.LongPollUseCaseImpl
|
import dev.meloda.fast.domain.LongPollUseCaseImpl
|
||||||
import org.koin.core.module.dsl.singleOf
|
import org.koin.core.module.dsl.singleOf
|
||||||
@@ -10,4 +11,5 @@ import org.koin.dsl.module
|
|||||||
val longPollModule = module {
|
val longPollModule = module {
|
||||||
singleOf(::LongPollUseCaseImpl) bind LongPollUseCase::class
|
singleOf(::LongPollUseCaseImpl) bind LongPollUseCase::class
|
||||||
singleOf(::LongPollUpdatesParser)
|
singleOf(::LongPollUpdatesParser)
|
||||||
|
singleOf(::LongPollUpdatesReducer)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.launchIn
|
|||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
@@ -45,6 +46,14 @@ fun <T> Flow<T>.listenValue(
|
|||||||
action: suspend (T) -> Unit
|
action: suspend (T) -> Unit
|
||||||
): Job = onEach(action::invoke).launchIn(coroutineScope)
|
): Job = onEach(action::invoke).launchIn(coroutineScope)
|
||||||
|
|
||||||
|
fun CoroutineScope.launchDbRefresh(
|
||||||
|
load: suspend () -> Unit,
|
||||||
|
after: suspend () -> Unit
|
||||||
|
): Job = launch {
|
||||||
|
load()
|
||||||
|
after()
|
||||||
|
}
|
||||||
|
|
||||||
fun createTimerFlow(
|
fun createTimerFlow(
|
||||||
time: Int,
|
time: Int,
|
||||||
onStartAction: (suspend () -> Unit)? = null,
|
onStartAction: (suspend () -> Unit)? = null,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ object UserConfig {
|
|||||||
userId = -1
|
userId = -1
|
||||||
trustedHash = null
|
trustedHash = null
|
||||||
exchangeToken = null
|
exchangeToken = null
|
||||||
|
AppSettings.LongPoll.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isLoggedIn(): Boolean {
|
fun isLoggedIn(): Boolean {
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import dev.meloda.fast.network.RestApiErrorDomain
|
|||||||
interface ConvosRepository {
|
interface ConvosRepository {
|
||||||
|
|
||||||
suspend fun storeConvos(convos: List<VkConvo>)
|
suspend fun storeConvos(convos: List<VkConvo>)
|
||||||
|
suspend fun getLocalConvos(): List<VkConvo>
|
||||||
|
suspend fun getLocalConvoById(peerId: Long): VkConvo?
|
||||||
|
suspend fun deleteLocalConvo(peerId: Long)
|
||||||
|
|
||||||
suspend fun getConvos(
|
suspend fun getConvos(
|
||||||
count: Int?,
|
count: Int?,
|
||||||
|
|||||||
@@ -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.VkMessage
|
||||||
import dev.meloda.fast.model.api.domain.VkUser
|
import dev.meloda.fast.model.api.domain.VkUser
|
||||||
import dev.meloda.fast.model.api.domain.asEntity
|
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.model.api.requests.ConvosGetRequest
|
||||||
import dev.meloda.fast.network.RestApiErrorDomain
|
import dev.meloda.fast.network.RestApiErrorDomain
|
||||||
import dev.meloda.fast.network.mapApiDefault
|
import dev.meloda.fast.network.mapApiDefault
|
||||||
import dev.meloda.fast.network.mapApiResult
|
import dev.meloda.fast.network.mapApiResult
|
||||||
import dev.meloda.fast.network.service.convos.ConvosService
|
import dev.meloda.fast.network.service.convos.ConvosService
|
||||||
|
import dev.meloda.fast.model.database.asExternalModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -40,6 +42,28 @@ class ConvosRepositoryImpl(
|
|||||||
convoDao.insertAll(convos.map(VkConvo::asEntity))
|
convoDao.insertAll(convos.map(VkConvo::asEntity))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getLocalConvos(): List<VkConvo> = 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(
|
override suspend fun getConvos(
|
||||||
count: Int?,
|
count: Int?,
|
||||||
offset: Int?,
|
offset: Int?,
|
||||||
@@ -198,4 +222,28 @@ class ConvosRepositoryImpl(
|
|||||||
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||||
convosService.unarchive(mapOf("peer_id" to peerId.toString())).mapApiDefault()
|
convosService.unarchive(mapOf("peer_id" to peerId.toString())).mapApiDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun List<VkConvo>.sorted(): List<VkConvo> {
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package dev.meloda.fast.data.api.longpoll
|
package dev.meloda.fast.data.api.longpoll
|
||||||
|
|
||||||
import dev.meloda.fast.model.api.data.LongPollUpdates
|
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.data.VkLongPollData
|
||||||
import dev.meloda.fast.network.RestApiErrorDomain
|
import dev.meloda.fast.network.RestApiErrorDomain
|
||||||
import com.slack.eithernet.ApiResult
|
import com.slack.eithernet.ApiResult
|
||||||
@@ -21,4 +22,14 @@ interface LongPollRepository {
|
|||||||
mode: Int,
|
mode: Int,
|
||||||
version: Int
|
version: Int
|
||||||
): ApiResult<LongPollUpdates, RestApiErrorDomain>
|
): ApiResult<LongPollUpdates, RestApiErrorDomain>
|
||||||
|
|
||||||
|
suspend fun getLongPollHistory(
|
||||||
|
ts: Int,
|
||||||
|
pts: Int,
|
||||||
|
lpVersion: Int,
|
||||||
|
lastN: Int? = null,
|
||||||
|
maxMsgId: Long? = null,
|
||||||
|
eventsLimit: Int? = null,
|
||||||
|
msgsLimit: Int? = null
|
||||||
|
): ApiResult<LongPollHistoryResponse, RestApiErrorDomain>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package dev.meloda.fast.data.api.longpoll
|
package dev.meloda.fast.data.api.longpoll
|
||||||
|
|
||||||
import dev.meloda.fast.model.api.data.LongPollUpdates
|
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.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.LongPollGetUpdatesRequest
|
||||||
import dev.meloda.fast.model.api.requests.MessagesGetLongPollServerRequest
|
import dev.meloda.fast.model.api.requests.MessagesGetLongPollServerRequest
|
||||||
import dev.meloda.fast.network.RestApiErrorDomain
|
import dev.meloda.fast.network.RestApiErrorDomain
|
||||||
@@ -52,4 +54,29 @@ class LongPollRepositoryImpl(
|
|||||||
|
|
||||||
longPollService.getResponse(serverUrl, requestModel.map).mapDefault()
|
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<LongPollHistoryResponse, RestApiErrorDomain> = 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() }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ interface MessagesRepository {
|
|||||||
|
|
||||||
suspend fun storeMessages(messages: List<VkMessage>)
|
suspend fun storeMessages(messages: List<VkMessage>)
|
||||||
|
|
||||||
|
suspend fun getLocalMessages(convoId: Long): List<VkMessage>
|
||||||
|
suspend fun getLocalMessageById(messageId: Long): VkMessage?
|
||||||
|
suspend fun getLocalMessageByConvoMessageId(convoId: Long, cmId: Long): VkMessage?
|
||||||
|
suspend fun getLocalMaxMessageId(): Long?
|
||||||
|
suspend fun deleteLocalMessages(messageIds: List<Long>)
|
||||||
|
|
||||||
suspend fun getHistory(
|
suspend fun getHistory(
|
||||||
convoId: Long,
|
convoId: Long,
|
||||||
offset: Int?,
|
offset: Int?,
|
||||||
|
|||||||
@@ -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.VkMessage
|
||||||
import dev.meloda.fast.model.api.domain.VkUser
|
import dev.meloda.fast.model.api.domain.VkUser
|
||||||
import dev.meloda.fast.model.api.domain.asEntity
|
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.MessagesCreateChatRequest
|
||||||
import dev.meloda.fast.model.api.requests.MessagesDeleteRequest
|
import dev.meloda.fast.model.api.requests.MessagesDeleteRequest
|
||||||
import dev.meloda.fast.model.api.requests.MessagesEditRequest
|
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.MessagesGetConvoMembersResponse
|
||||||
import dev.meloda.fast.model.api.responses.MessagesGetReadPeersResponse
|
import dev.meloda.fast.model.api.responses.MessagesGetReadPeersResponse
|
||||||
import dev.meloda.fast.model.api.responses.MessagesSendResponse
|
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.RestApiErrorDomain
|
||||||
import dev.meloda.fast.network.mapApiDefault
|
import dev.meloda.fast.network.mapApiDefault
|
||||||
import dev.meloda.fast.network.mapApiResult
|
import dev.meloda.fast.network.mapApiResult
|
||||||
@@ -354,6 +356,28 @@ class MessagesRepositoryImpl(
|
|||||||
messageDao.insertAll(messages.map(VkMessage::asEntity))
|
messageDao.insertAll(messages.map(VkMessage::asEntity))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getLocalMessages(convoId: Long): List<VkMessage> = 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<Long>) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
messageDao.deleteByIds(messageIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun edit(
|
override suspend fun edit(
|
||||||
peerId: Long,
|
peerId: Long,
|
||||||
messageId: Long?,
|
messageId: Long?,
|
||||||
|
|||||||
@@ -9,9 +9,13 @@ import dev.meloda.fast.model.database.VkConvoEntity
|
|||||||
@Dao
|
@Dao
|
||||||
abstract class ConvoDao : EntityDao<VkConvoEntity> {
|
abstract class ConvoDao : EntityDao<VkConvoEntity> {
|
||||||
|
|
||||||
@Query("SELECT * FROM convos")
|
@Query("SELECT * FROM convos ORDER BY majorId DESC, minorId DESC")
|
||||||
abstract suspend fun getAll(): List<VkConvoEntity>
|
abstract suspend fun getAll(): List<VkConvoEntity>
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query("SELECT * FROM convos ORDER BY majorId DESC, minorId DESC")
|
||||||
|
abstract suspend fun getAllWithMessage(): List<ConvoWithMessage>
|
||||||
|
|
||||||
@Query("SELECT * FROM convos WHERE id IN (:ids)")
|
@Query("SELECT * FROM convos WHERE id IN (:ids)")
|
||||||
abstract suspend fun getAllByIds(ids: List<Int>): List<VkConvoEntity>
|
abstract suspend fun getAllByIds(ids: List<Int>): List<VkConvoEntity>
|
||||||
|
|
||||||
@@ -22,6 +26,9 @@ abstract class ConvoDao : EntityDao<VkConvoEntity> {
|
|||||||
@Query("SELECT * FROM convos WHERE id IS (:id)")
|
@Query("SELECT * FROM convos WHERE id IS (:id)")
|
||||||
abstract suspend fun getByIdWithMessage(id: Long): ConvoWithMessage?
|
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)")
|
@Query("DELETE FROM convos WHERE rowid IN (:ids)")
|
||||||
abstract suspend fun deleteByIds(ids: List<Int>): Int
|
abstract suspend fun deleteByIds(ids: List<Int>): Int
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,15 +10,21 @@ abstract class MessageDao : EntityDao<VkMessageEntity> {
|
|||||||
@Query("SELECT * FROM messages")
|
@Query("SELECT * FROM messages")
|
||||||
abstract suspend fun getAll(): List<VkMessageEntity>
|
abstract suspend fun getAll(): List<VkMessageEntity>
|
||||||
|
|
||||||
@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<VkMessageEntity>
|
abstract suspend fun getAll(convoId: Long): List<VkMessageEntity>
|
||||||
|
|
||||||
@Query("SELECT * FROM messages WHERE id IN (:ids)")
|
@Query("SELECT * FROM messages WHERE id IN (:ids)")
|
||||||
abstract suspend fun getAllByIds(ids: List<Int>): List<VkMessageEntity>
|
abstract suspend fun getAllByIds(ids: List<Long>): List<VkMessageEntity>
|
||||||
|
|
||||||
@Query("SELECT * FROM messages WHERE id IS (:messageId)")
|
@Query("SELECT * FROM messages WHERE id IS (:messageId)")
|
||||||
abstract suspend fun getById(messageId: Long): VkMessageEntity?
|
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)")
|
@Query("DELETE FROM messages WHERE id IN (:ids)")
|
||||||
abstract suspend fun deleteByIds(ids: List<Int>): Int
|
abstract suspend fun deleteByIds(ids: List<Long>): Int
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -230,6 +230,21 @@ object AppSettings {
|
|||||||
set(value) = put(SettingsKeys.KEY_MORE_ANIMATIONS, value)
|
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 {
|
object Debug {
|
||||||
var showAlertAfterCrash: Boolean
|
var showAlertAfterCrash: Boolean
|
||||||
get() = get(
|
get() = get(
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ object SettingsKeys {
|
|||||||
const val DEFAULT_VALUE_FEATURES_FAST_TEXT = "¯\\_(ツ)_/¯"
|
const val DEFAULT_VALUE_FEATURES_FAST_TEXT = "¯\\_(ツ)_/¯"
|
||||||
const val KEY_LONG_POLL_IN_BACKGROUND = "lp_background"
|
const val KEY_LONG_POLL_IN_BACKGROUND = "lp_background"
|
||||||
const val DEFAULT_LONG_POLL_IN_BACKGROUND = false
|
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 KEY_ACTIVITY_SEND_ONLINE_STATUS = "activity_send_online_status"
|
||||||
const val DEFAULT_VALUE_KEY_ACTIVITY_SEND_ONLINE_STATUS = false
|
const val DEFAULT_VALUE_KEY_ACTIVITY_SEND_ONLINE_STATUS = false
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
interface ConvoUseCase : BaseUseCase {
|
interface ConvoUseCase : BaseUseCase {
|
||||||
|
|
||||||
suspend fun storeConvos(convos: List<VkConvo>)
|
suspend fun storeConvos(convos: List<VkConvo>)
|
||||||
|
suspend fun getLocalConvos(): List<VkConvo>
|
||||||
|
suspend fun getLocalConvoById(peerId: Long): VkConvo?
|
||||||
|
suspend fun deleteLocalConvo(peerId: Long)
|
||||||
|
|
||||||
fun getConvos(
|
fun getConvos(
|
||||||
count: Int? = null,
|
count: Int? = null,
|
||||||
|
|||||||
@@ -19,6 +19,18 @@ class ConvoUseCaseImpl(
|
|||||||
repository.storeConvos(convos)
|
repository.storeConvos(convos)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getLocalConvos(): List<VkConvo> = 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(
|
override fun getConvos(
|
||||||
count: Int?,
|
count: Int?,
|
||||||
offset: Int?,
|
offset: Int?,
|
||||||
|
|||||||
@@ -66,6 +66,14 @@ class LongPollUpdatesParser(
|
|||||||
eventDispatcher.registerListener(LongPollEvent.MESSAGE_RESTORED, assembleEventCallback(block))
|
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) {
|
fun onNewMessage(block: (LongPollParsedEvent.NewMessage) -> Unit) {
|
||||||
eventDispatcher.registerListener(LongPollEvent.MESSAGE_NEW, assembleEventCallback(block))
|
eventDispatcher.registerListener(LongPollEvent.MESSAGE_NEW, assembleEventCallback(block))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<LongPollParsedEvent>(extraBufferCapacity = 256)
|
||||||
|
val events: SharedFlow<LongPollParsedEvent> = _events.asSharedFlow()
|
||||||
|
|
||||||
|
val newMessages = events.filterIsInstance<LongPollParsedEvent.NewMessage>()
|
||||||
|
val messageEdited = events.filterIsInstance<LongPollParsedEvent.MessageEdited>()
|
||||||
|
val messageIncomingRead = events.filterIsInstance<LongPollParsedEvent.IncomingMessageRead>()
|
||||||
|
val messageOutgoingRead = events.filterIsInstance<LongPollParsedEvent.OutgoingMessageRead>()
|
||||||
|
val messageDeleted = events.filterIsInstance<LongPollParsedEvent.MessageDeleted>()
|
||||||
|
val messageRestored = events.filterIsInstance<LongPollParsedEvent.MessageRestored>()
|
||||||
|
val messageMarkedAsImportant = events.filterIsInstance<LongPollParsedEvent.MessageMarkedAsImportant>()
|
||||||
|
val messageMarkedAsSpam = events.filterIsInstance<LongPollParsedEvent.MessageMarkedAsSpam>()
|
||||||
|
val messageMarkedAsNotSpam = events.filterIsInstance<LongPollParsedEvent.MessageMarkedAsNotSpam>()
|
||||||
|
val interactions = events.filterIsInstance<LongPollParsedEvent.Interaction>()
|
||||||
|
val chatMajorChanged = events.filterIsInstance<LongPollParsedEvent.ChatMajorChanged>()
|
||||||
|
val chatMinorChanged = events.filterIsInstance<LongPollParsedEvent.ChatMinorChanged>()
|
||||||
|
val chatCleared = events.filterIsInstance<LongPollParsedEvent.ChatCleared>()
|
||||||
|
val chatArchived = events.filterIsInstance<LongPollParsedEvent.ChatArchived>()
|
||||||
|
val messageUpdated = events.filterIsInstance<LongPollParsedEvent.MessageUpdated>()
|
||||||
|
val messageCacheClear = events.filterIsInstance<LongPollParsedEvent.MessageCacheClear>()
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package dev.meloda.fast.domain
|
package dev.meloda.fast.domain
|
||||||
|
|
||||||
import dev.meloda.fast.data.State
|
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.LongPollUpdates
|
||||||
import dev.meloda.fast.model.api.data.VkLongPollData
|
import dev.meloda.fast.model.api.data.VkLongPollData
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@@ -21,4 +22,14 @@ interface LongPollUseCase {
|
|||||||
mode: Int,
|
mode: Int,
|
||||||
version: Int
|
version: Int
|
||||||
): Flow<State<LongPollUpdates>>
|
): Flow<State<LongPollUpdates>>
|
||||||
|
|
||||||
|
fun getLongPollHistory(
|
||||||
|
ts: Int,
|
||||||
|
pts: Int,
|
||||||
|
lpVersion: Int,
|
||||||
|
lastN: Int? = null,
|
||||||
|
maxMsgId: Long? = null,
|
||||||
|
eventsLimit: Int? = null,
|
||||||
|
msgsLimit: Int? = null
|
||||||
|
): Flow<State<LongPollHistoryResponse>>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package dev.meloda.fast.domain
|
|||||||
import dev.meloda.fast.data.State
|
import dev.meloda.fast.data.State
|
||||||
import dev.meloda.fast.data.api.longpoll.LongPollRepository
|
import dev.meloda.fast.data.api.longpoll.LongPollRepository
|
||||||
import dev.meloda.fast.data.mapToState
|
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.LongPollUpdates
|
||||||
import dev.meloda.fast.model.api.data.VkLongPollData
|
import dev.meloda.fast.model.api.data.VkLongPollData
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@@ -48,4 +49,27 @@ class LongPollUseCaseImpl(
|
|||||||
).mapToState()
|
).mapToState()
|
||||||
emit(newState)
|
emit(newState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getLongPollHistory(
|
||||||
|
ts: Int,
|
||||||
|
pts: Int,
|
||||||
|
lpVersion: Int,
|
||||||
|
lastN: Int?,
|
||||||
|
maxMsgId: Long?,
|
||||||
|
eventsLimit: Int?,
|
||||||
|
msgsLimit: Int?
|
||||||
|
): Flow<State<LongPollHistoryResponse>> = 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ interface MessagesUseCase : BaseUseCase {
|
|||||||
|
|
||||||
suspend fun storeMessage(message: VkMessage)
|
suspend fun storeMessage(message: VkMessage)
|
||||||
suspend fun storeMessages(messages: List<VkMessage>)
|
suspend fun storeMessages(messages: List<VkMessage>)
|
||||||
|
suspend fun getLocalMessages(convoId: Long): List<VkMessage>
|
||||||
|
suspend fun getLocalMessageById(messageId: Long): VkMessage?
|
||||||
|
suspend fun getLocalMessageByConvoMessageId(convoId: Long, cmId: Long): VkMessage?
|
||||||
|
suspend fun getLocalMaxMessageId(): Long?
|
||||||
|
suspend fun deleteLocalMessages(messageIds: List<Long>)
|
||||||
|
|
||||||
fun getMessagesHistory(
|
fun getMessagesHistory(
|
||||||
convoId: Long,
|
convoId: Long,
|
||||||
|
|||||||
@@ -22,6 +22,26 @@ class MessagesUseCaseImpl(
|
|||||||
repository.storeMessages(messages)
|
repository.storeMessages(messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getLocalMessages(convoId: Long): List<VkMessage> {
|
||||||
|
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<Long>) {
|
||||||
|
repository.deleteLocalMessages(messageIds)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getMessagesHistory(
|
override fun getMessagesHistory(
|
||||||
convoId: Long,
|
convoId: Long,
|
||||||
count: Int?,
|
count: Int?,
|
||||||
|
|||||||
@@ -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<List<Any>>? = null,
|
||||||
|
@Json(name = "messages") val messages: Messages? = null,
|
||||||
|
@Json(name = "profiles") val profiles: List<VkUserData>? = null,
|
||||||
|
@Json(name = "groups") val groups: List<VkGroupData>? = 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<VkConvoData>? = null
|
||||||
|
) {
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class Messages(
|
||||||
|
@Json(name = "count") val count: Int? = null,
|
||||||
|
@Json(name = "items") val items: List<VkMessageData>? = null
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -19,3 +19,27 @@ data class LongPollGetUpdatesRequest(
|
|||||||
"version" to version.toString()
|
"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<String, String>
|
||||||
|
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() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+7
@@ -2,6 +2,7 @@ package dev.meloda.fast.network.service.messages
|
|||||||
|
|
||||||
import com.slack.eithernet.ApiResult
|
import com.slack.eithernet.ApiResult
|
||||||
import dev.meloda.fast.model.api.data.VkChatData
|
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.VkLongPollData
|
||||||
import dev.meloda.fast.model.api.data.VkMessageData
|
import dev.meloda.fast.model.api.data.VkMessageData
|
||||||
import dev.meloda.fast.model.api.responses.MessagesCreateChatResponse
|
import dev.meloda.fast.model.api.responses.MessagesCreateChatResponse
|
||||||
@@ -44,6 +45,12 @@ interface MessagesService {
|
|||||||
@FieldMap params: Map<String, String>
|
@FieldMap params: Map<String, String>
|
||||||
): ApiResult<ApiResponse<VkLongPollData>, RestApiError>
|
): ApiResult<ApiResponse<VkLongPollData>, RestApiError>
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST(MessagesUrls.GET_LONG_POLL_HISTORY)
|
||||||
|
suspend fun getLongPollHistory(
|
||||||
|
@FieldMap params: Map<String, String>
|
||||||
|
): ApiResult<ApiResponse<LongPollHistoryResponse>, RestApiError>
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@POST(MessagesUrls.MARK_AS_READ)
|
@POST(MessagesUrls.MARK_AS_READ)
|
||||||
suspend fun markAsRead(
|
suspend fun markAsRead(
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import com.conena.nanokt.collections.indexOfFirstOrNull
|
import com.conena.nanokt.collections.indexOfFirstOrNull
|
||||||
import dev.meloda.fast.common.VkConstants
|
|
||||||
import dev.meloda.fast.common.extensions.createTimerFlow
|
import dev.meloda.fast.common.extensions.createTimerFlow
|
||||||
import dev.meloda.fast.common.extensions.findWithIndex
|
import dev.meloda.fast.common.extensions.findWithIndex
|
||||||
import dev.meloda.fast.common.extensions.listenValue
|
import dev.meloda.fast.common.extensions.listenValue
|
||||||
import dev.meloda.fast.common.extensions.setValue
|
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.extensions.updateValue
|
||||||
import dev.meloda.fast.common.paging.canPaginate as canPaginatePage
|
import dev.meloda.fast.common.paging.canPaginate as canPaginatePage
|
||||||
import dev.meloda.fast.common.paging.isPaginationExhausted as isPaginationExhaustedPage
|
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.data.processState
|
||||||
import dev.meloda.fast.datastore.UserSettings
|
import dev.meloda.fast.datastore.UserSettings
|
||||||
import dev.meloda.fast.domain.ConvoUseCase
|
import dev.meloda.fast.domain.ConvoUseCase
|
||||||
import dev.meloda.fast.domain.LoadConvosByIdUseCase
|
import dev.meloda.fast.domain.LongPollUpdatesReducer
|
||||||
import dev.meloda.fast.domain.LongPollUpdatesParser
|
|
||||||
import dev.meloda.fast.domain.MessagesUseCase
|
import dev.meloda.fast.domain.MessagesUseCase
|
||||||
import dev.meloda.fast.domain.util.asPresentation
|
import dev.meloda.fast.domain.util.asPresentation
|
||||||
import dev.meloda.fast.domain.util.extractAvatar
|
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.SharingStarted
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class ConvosViewModel(
|
class ConvosViewModel(
|
||||||
updatesParser: LongPollUpdatesParser,
|
updatesReducer: LongPollUpdatesReducer,
|
||||||
private val filter: ConvosFilter,
|
private val filter: ConvosFilter,
|
||||||
private val convoUseCase: ConvoUseCase,
|
private val convoUseCase: ConvoUseCase,
|
||||||
private val messagesUseCase: MessagesUseCase,
|
private val messagesUseCase: MessagesUseCase,
|
||||||
private val resources: Resources,
|
private val resources: Resources,
|
||||||
private val userSettings: UserSettings,
|
private val userSettings: UserSettings,
|
||||||
private val imageLoader: ImageLoader,
|
private val imageLoader: ImageLoader,
|
||||||
private val applicationContext: Context,
|
private val applicationContext: Context
|
||||||
private val loadConvosByIdUseCase: LoadConvosByIdUseCase
|
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _screenState = MutableStateFlow(ConvosScreenState.EMPTY)
|
private val _screenState = MutableStateFlow(ConvosScreenState.EMPTY)
|
||||||
val screenState = _screenState.asStateFlow()
|
val screenState = _screenState.asStateFlow()
|
||||||
@@ -98,15 +98,15 @@ class ConvosViewModel(
|
|||||||
|
|
||||||
loadConvos()
|
loadConvos()
|
||||||
|
|
||||||
updatesParser.onNewMessage(::handleNewMessage)
|
updatesReducer.newMessages.onEach(::handleNewMessage).launchIn(viewModelScope)
|
||||||
updatesParser.onMessageEdited(::handleEditedMessage)
|
updatesReducer.messageEdited.onEach(::handleEditedMessage).launchIn(viewModelScope)
|
||||||
updatesParser.onMessageIncomingRead(::handleReadIncomingMessage)
|
updatesReducer.messageIncomingRead.onEach(::handleReadIncomingMessage).launchIn(viewModelScope)
|
||||||
updatesParser.onMessageOutgoingRead(::handleReadOutgoingMessage)
|
updatesReducer.messageOutgoingRead.onEach(::handleReadOutgoingMessage).launchIn(viewModelScope)
|
||||||
updatesParser.onInteractions(::handleInteraction)
|
updatesReducer.interactions.onEach(::handleInteraction).launchIn(viewModelScope)
|
||||||
updatesParser.onChatMajorChanged(::handleChatMajorChanged)
|
updatesReducer.chatMajorChanged.onEach(::handleChatMajorChanged).launchIn(viewModelScope)
|
||||||
updatesParser.onChatMinorChanged(::handleChatMinorChanged)
|
updatesReducer.chatMinorChanged.onEach(::handleChatMinorChanged).launchIn(viewModelScope)
|
||||||
updatesParser.onChatCleared(::handleChatClearing)
|
updatesReducer.chatCleared.onEach(::handleChatClearing).launchIn(viewModelScope)
|
||||||
updatesParser.onChatArchived(::handleChatArchived)
|
updatesReducer.chatArchived.onEach(::handleChatArchived).launchIn(viewModelScope)
|
||||||
|
|
||||||
userSettings.useContactNames.listenValue(viewModelScope) {
|
userSettings.useContactNames.listenValue(viewModelScope) {
|
||||||
syncUiConvos()
|
syncUiConvos()
|
||||||
@@ -257,6 +257,10 @@ class ConvosViewModel(
|
|||||||
private fun loadConvos(
|
private fun loadConvos(
|
||||||
offset: Int = currentOffset.value
|
offset: Int = currentOffset.value
|
||||||
) {
|
) {
|
||||||
|
if (offset == 0) {
|
||||||
|
refreshConvosFromDb()
|
||||||
|
}
|
||||||
|
|
||||||
convoUseCase.getConvos(
|
convoUseCase.getConvos(
|
||||||
count = LOAD_COUNT,
|
count = LOAD_COUNT,
|
||||||
offset = offset,
|
offset = offset,
|
||||||
@@ -313,14 +317,10 @@ class ConvosViewModel(
|
|||||||
state.processState(
|
state.processState(
|
||||||
error = {},
|
error = {},
|
||||||
success = {
|
success = {
|
||||||
val newConvos = convos.value.toMutableList()
|
viewModelScope.launch {
|
||||||
val convoIndex =
|
convoUseCase.deleteLocalConvo(peerId)
|
||||||
newConvos.indexOfFirstOrNull { it.id == peerId }
|
refreshConvosFromDb()
|
||||||
?: return@processState
|
}
|
||||||
|
|
||||||
newConvos.removeAt(convoIndex)
|
|
||||||
_convos.update { newConvos.sorted() }
|
|
||||||
syncUiConvos()
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
_screenState.emit(screenState.value.copy(isLoading = state.isLoading()))
|
_screenState.emit(screenState.value.copy(isLoading = state.isLoading()))
|
||||||
@@ -333,16 +333,22 @@ class ConvosViewModel(
|
|||||||
state.processState(
|
state.processState(
|
||||||
error = {},
|
error = {},
|
||||||
success = {
|
success = {
|
||||||
handleChatMajorChanged(
|
viewModelScope.launch {
|
||||||
LongPollParsedEvent.ChatMajorChanged(
|
convoUseCase.getLocalConvoById(peerId)?.let { convo ->
|
||||||
peerId = peerId,
|
convoUseCase.storeConvos(
|
||||||
majorId = if (pin) {
|
listOf(
|
||||||
pinnedConvosCount.value.plus(1) * 16
|
convo.copy(
|
||||||
} else {
|
majorId = if (pin) {
|
||||||
0
|
pinnedConvosCount.value.plus(1) * 16
|
||||||
}
|
} else {
|
||||||
)
|
0
|
||||||
)
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
refreshConvosFromDb()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -356,145 +362,35 @@ class ConvosViewModel(
|
|||||||
state.processState(
|
state.processState(
|
||||||
error = {},
|
error = {},
|
||||||
success = {
|
success = {
|
||||||
convos.value.find { it.id == peerId }?.let { convo ->
|
viewModelScope.launch {
|
||||||
handleChatArchived(
|
convoUseCase.getLocalConvoById(peerId)?.let { convo ->
|
||||||
LongPollParsedEvent.ChatArchived(
|
convoUseCase.storeConvos(
|
||||||
convo = convo,
|
listOf(
|
||||||
archived = archive
|
convo.copy(isArchived = archive)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
|
refreshConvosFromDb()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: 03-Apr-25, Danil Nikolaev: handle business messages
|
|
||||||
private fun handleNewMessage(event: LongPollParsedEvent.NewMessage) {
|
private fun handleNewMessage(event: LongPollParsedEvent.NewMessage) {
|
||||||
val message = event.message
|
refreshConvosFromDb()
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleEditedMessage(event: LongPollParsedEvent.MessageEdited) {
|
private fun handleEditedMessage(event: LongPollParsedEvent.MessageEdited) {
|
||||||
val message = event.message
|
refreshConvosFromDb()
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleReadIncomingMessage(event: LongPollParsedEvent.IncomingMessageRead) {
|
private fun handleReadIncomingMessage(event: LongPollParsedEvent.IncomingMessageRead) {
|
||||||
val newConvos = convos.value.toMutableList()
|
refreshConvosFromDb()
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleReadOutgoingMessage(event: LongPollParsedEvent.OutgoingMessageRead) {
|
private fun handleReadOutgoingMessage(event: LongPollParsedEvent.OutgoingMessageRead) {
|
||||||
val newConvos = convos.value.toMutableList()
|
refreshConvosFromDb()
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleInteraction(event: LongPollParsedEvent.Interaction) {
|
private fun handleInteraction(event: LongPollParsedEvent.Interaction) {
|
||||||
@@ -563,88 +459,19 @@ class ConvosViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleChatMajorChanged(event: LongPollParsedEvent.ChatMajorChanged) {
|
private fun handleChatMajorChanged(event: LongPollParsedEvent.ChatMajorChanged) {
|
||||||
val newConvos = convos.value.toMutableList()
|
refreshConvosFromDb()
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleChatMinorChanged(event: LongPollParsedEvent.ChatMinorChanged) {
|
private fun handleChatMinorChanged(event: LongPollParsedEvent.ChatMinorChanged) {
|
||||||
val newConvos = convos.value.toMutableList()
|
refreshConvosFromDb()
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleChatClearing(event: LongPollParsedEvent.ChatCleared) {
|
private fun handleChatClearing(event: LongPollParsedEvent.ChatCleared) {
|
||||||
val newConvos = convos.value.toMutableList()
|
refreshConvosFromDb()
|
||||||
|
|
||||||
val convoIndex = newConvos.indexOfFirstOrNull { it.id == event.peerId }
|
|
||||||
|
|
||||||
if (convoIndex == null) { // диалога нет в списке
|
|
||||||
// pizdets
|
|
||||||
} else {
|
|
||||||
newConvos.removeAt(convoIndex)
|
|
||||||
|
|
||||||
_convos.setValue { newConvos.sorted() }
|
|
||||||
syncUiConvos()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleChatArchived(event: LongPollParsedEvent.ChatArchived) {
|
private fun handleChatArchived(event: LongPollParsedEvent.ChatArchived) {
|
||||||
val convo = event.convo
|
refreshConvosFromDb()
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun readConvo(peerId: Long, startMessageId: Long) {
|
private fun readConvo(peerId: Long, startMessageId: Long) {
|
||||||
@@ -655,21 +482,42 @@ class ConvosViewModel(
|
|||||||
state.processState(
|
state.processState(
|
||||||
error = {},
|
error = {},
|
||||||
success = {
|
success = {
|
||||||
val newConvos = convos.value.toMutableList()
|
viewModelScope.launch {
|
||||||
val convoIndex =
|
convoUseCase.getLocalConvoById(peerId)?.let { convo ->
|
||||||
newConvos.indexOfFirstOrNull { it.id == peerId }
|
convoUseCase.storeConvos(
|
||||||
?: return@listenValue
|
listOf(
|
||||||
|
convo.copy(
|
||||||
newConvos[convoIndex] =
|
inRead = startMessageId,
|
||||||
newConvos[convoIndex].copy(inRead = startMessageId)
|
unreadCount = 0
|
||||||
|
)
|
||||||
_convos.update { newConvos }
|
)
|
||||||
syncUiConvos()
|
)
|
||||||
|
}
|
||||||
|
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<VkConvo>.sorted(): List<VkConvo> {
|
private fun List<VkConvo>.sorted(): List<VkConvo> {
|
||||||
val newConvos = toMutableList()
|
val newConvos = toMutableList()
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ private fun Scope.createConvosViewModel(filter: ConvosFilter): ConvosViewModel {
|
|||||||
resources = get(),
|
resources = get(),
|
||||||
userSettings = get(),
|
userSettings = get(),
|
||||||
imageLoader = get(),
|
imageLoader = get(),
|
||||||
applicationContext = get(),
|
applicationContext = get()
|
||||||
loadConvosByIdUseCase = get()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+40
@@ -3,6 +3,7 @@ package dev.meloda.fast.messageshistory
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import dev.meloda.fast.common.VkConstants
|
import dev.meloda.fast.common.VkConstants
|
||||||
import dev.meloda.fast.common.extensions.listenValue
|
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.extensions.setValue
|
||||||
import dev.meloda.fast.common.paging.canPaginate as canPaginatePage
|
import dev.meloda.fast.common.paging.canPaginate as canPaginatePage
|
||||||
import dev.meloda.fast.common.paging.isPaginationExhausted as isPaginationExhaustedPage
|
import dev.meloda.fast.common.paging.isPaginationExhausted as isPaginationExhaustedPage
|
||||||
@@ -39,6 +40,29 @@ internal class MessagesHistoryLoaders(
|
|||||||
fun loadConvo() {
|
fun loadConvo() {
|
||||||
Log.d("MessagesHistoryViewModelImpl", "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(
|
convoUseCase.getById(
|
||||||
peerIds = listOf(screenState.value.convoId),
|
peerIds = listOf(screenState.value.convoId),
|
||||||
extended = true,
|
extended = true,
|
||||||
@@ -71,6 +95,22 @@ internal class MessagesHistoryLoaders(
|
|||||||
fun loadMessagesHistory(offset: Int) {
|
fun loadMessagesHistory(offset: Int) {
|
||||||
Log.d("MessagesHistoryViewModel", "loadMessagesHistory: $offset")
|
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(
|
messagesUseCase.getMessagesHistory(
|
||||||
convoId = screenState.value.convoId,
|
convoId = screenState.value.convoId,
|
||||||
count = MessagesHistoryViewModelImpl.MESSAGES_LOAD_COUNT,
|
count = MessagesHistoryViewModelImpl.MESSAGES_LOAD_COUNT,
|
||||||
|
|||||||
+35
-94
@@ -1,18 +1,24 @@
|
|||||||
package dev.meloda.fast.messageshistory
|
package dev.meloda.fast.messageshistory
|
||||||
|
|
||||||
import android.util.Log
|
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.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.messageshistory.model.MessagesHistoryScreenState
|
||||||
import dev.meloda.fast.model.LongPollParsedEvent
|
import dev.meloda.fast.model.LongPollParsedEvent
|
||||||
import dev.meloda.fast.model.api.domain.VkMessage
|
import dev.meloda.fast.model.api.domain.VkMessage
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlin.math.abs
|
|
||||||
|
|
||||||
internal class MessagesHistoryLongPollEventHandler(
|
internal class MessagesHistoryLongPollEventHandler(
|
||||||
|
private val scope: CoroutineScope,
|
||||||
|
private val convoUseCase: ConvoUseCase,
|
||||||
|
private val messagesUseCase: MessagesUseCase,
|
||||||
private val screenState: MutableStateFlow<MessagesHistoryScreenState>,
|
private val screenState: MutableStateFlow<MessagesHistoryScreenState>,
|
||||||
private val messages: MutableStateFlow<List<VkMessage>>,
|
private val messages: MutableStateFlow<List<VkMessage>>,
|
||||||
private val syncUiMessages: () -> Unit
|
private val syncUiMessages: () -> Unit,
|
||||||
|
private val onPinnedMessageChanged: (VkMessage?) -> Unit
|
||||||
) {
|
) {
|
||||||
fun onNewMessage(event: LongPollParsedEvent.NewMessage) {
|
fun onNewMessage(event: LongPollParsedEvent.NewMessage) {
|
||||||
val message = event.message
|
val message = event.message
|
||||||
@@ -20,139 +26,74 @@ internal class MessagesHistoryLongPollEventHandler(
|
|||||||
Log.d("MessagesHistoryViewModel", "handleNewMessage: $message")
|
Log.d("MessagesHistoryViewModel", "handleNewMessage: $message")
|
||||||
|
|
||||||
if (message.peerId != screenState.value.convoId) return
|
if (message.peerId != screenState.value.convoId) return
|
||||||
if (messages.value.indexOfFirstOrNull { it.id == message.id } != null) return
|
refreshFromDb(refreshMessages = true)
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onMessageEdited(event: LongPollParsedEvent.MessageEdited) {
|
fun onMessageEdited(event: LongPollParsedEvent.MessageEdited) {
|
||||||
val message = event.message
|
val message = event.message
|
||||||
if (message.peerId != screenState.value.convoId) return
|
if (message.peerId != screenState.value.convoId) return
|
||||||
|
|
||||||
val newMessages = messages.value.toMutableList()
|
refreshFromDb(refreshMessages = true)
|
||||||
val index = newMessages.indexOfFirstOrNull { it.id == message.id }
|
|
||||||
if (index == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
newMessages[index] = message
|
|
||||||
messages.setValue { newMessages }
|
|
||||||
syncUiMessages()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onReadIncoming(event: LongPollParsedEvent.IncomingMessageRead) {
|
fun onReadIncoming(event: LongPollParsedEvent.IncomingMessageRead) {
|
||||||
if (event.peerId != screenState.value.convoId) return
|
if (event.peerId != screenState.value.convoId) return
|
||||||
|
|
||||||
val index = messages.value.indexOfFirstOrNull { it.cmId == event.cmId }
|
refreshFromDb(refreshMessages = false)
|
||||||
if (index == null) return
|
|
||||||
|
|
||||||
val newConvo = screenState.value.convo.copy(
|
|
||||||
inReadCmId = event.cmId
|
|
||||||
)
|
|
||||||
|
|
||||||
screenState.setValue { old ->
|
|
||||||
old.copy(convo = newConvo)
|
|
||||||
}
|
|
||||||
|
|
||||||
syncUiMessages()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onReadOutgoing(event: LongPollParsedEvent.OutgoingMessageRead) {
|
fun onReadOutgoing(event: LongPollParsedEvent.OutgoingMessageRead) {
|
||||||
if (event.peerId != screenState.value.convoId) return
|
if (event.peerId != screenState.value.convoId) return
|
||||||
|
|
||||||
val index = messages.value.indexOfFirstOrNull { it.cmId == event.cmId }
|
refreshFromDb(refreshMessages = false)
|
||||||
if (index == null) return
|
|
||||||
|
|
||||||
val newConvo = screenState.value.convo.copy(
|
|
||||||
outReadCmId = event.cmId
|
|
||||||
)
|
|
||||||
|
|
||||||
screenState.setValue { old ->
|
|
||||||
old.copy(convo = newConvo)
|
|
||||||
}
|
|
||||||
|
|
||||||
syncUiMessages()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onMessageDeleted(event: LongPollParsedEvent.MessageDeleted) {
|
fun onMessageDeleted(event: LongPollParsedEvent.MessageDeleted) {
|
||||||
if (event.peerId != screenState.value.convoId) return
|
if (event.peerId != screenState.value.convoId) return
|
||||||
|
|
||||||
val newMessages = messages.value.toMutableList()
|
refreshFromDb(refreshMessages = true)
|
||||||
val index = newMessages.indexOfFirstOrNull { it.cmId == event.cmId }
|
|
||||||
if (index == null) return
|
|
||||||
|
|
||||||
newMessages.removeAt(index)
|
|
||||||
messages.setValue { newMessages }
|
|
||||||
syncUiMessages()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onMessageRestored(event: LongPollParsedEvent.MessageRestored) {
|
fun onMessageRestored(event: LongPollParsedEvent.MessageRestored) {
|
||||||
if (event.message.peerId != screenState.value.convoId) return
|
if (event.message.peerId != screenState.value.convoId) return
|
||||||
|
|
||||||
val newMessages = messages.value.toMutableList()
|
refreshFromDb(refreshMessages = true)
|
||||||
val minDate = newMessages.minOf(VkMessage::date)
|
|
||||||
|
|
||||||
if (event.message.date < minDate) return
|
|
||||||
|
|
||||||
newMessages.add(event.message)
|
|
||||||
messages.setValue { newMessages.sorted() }
|
|
||||||
syncUiMessages()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onMessageMarkedAsImportant(event: LongPollParsedEvent.MessageMarkedAsImportant) {
|
fun onMessageMarkedAsImportant(event: LongPollParsedEvent.MessageMarkedAsImportant) {
|
||||||
if (event.peerId != screenState.value.convoId) return
|
if (event.peerId != screenState.value.convoId) return
|
||||||
|
|
||||||
val newMessages = messages.value.toMutableList()
|
refreshFromDb(refreshMessages = true)
|
||||||
val index = newMessages.indexOfFirstOrNull { it.cmId == event.cmId }
|
|
||||||
if (index == null) return
|
|
||||||
|
|
||||||
newMessages[index] = newMessages[index].copy(isImportant = event.marked)
|
|
||||||
messages.setValue { newMessages }
|
|
||||||
syncUiMessages()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onMessageMarkedAsSpam(event: LongPollParsedEvent.MessageMarkedAsSpam) {
|
fun onMessageMarkedAsSpam(event: LongPollParsedEvent.MessageMarkedAsSpam) {
|
||||||
if (event.peerId != screenState.value.convoId) return
|
if (event.peerId != screenState.value.convoId) return
|
||||||
|
|
||||||
val newMessages = messages.value.toMutableList()
|
refreshFromDb(refreshMessages = true)
|
||||||
val index = newMessages.indexOfFirstOrNull { it.cmId == event.cmId }
|
|
||||||
if (index == null) return
|
|
||||||
|
|
||||||
newMessages.removeAt(index)
|
|
||||||
messages.setValue { newMessages }
|
|
||||||
syncUiMessages()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onMessageMarkedAsNotSpam(event: LongPollParsedEvent.MessageMarkedAsNotSpam) {
|
fun onMessageMarkedAsNotSpam(event: LongPollParsedEvent.MessageMarkedAsNotSpam) {
|
||||||
if (event.message.peerId != screenState.value.convoId) return
|
if (event.message.peerId != screenState.value.convoId) return
|
||||||
|
|
||||||
val newMessages = messages.value.toMutableList()
|
refreshFromDb(refreshMessages = true)
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun List<VkMessage>.sorted(): List<VkMessage> {
|
private fun refreshFromDb(refreshMessages: Boolean) {
|
||||||
return sortedWith { m1, m2 ->
|
scope.launchDbRefresh(
|
||||||
val dateDiff = m2.date - m1.date
|
load = {
|
||||||
if (dateDiff != 0) {
|
convoUseCase.getLocalConvoById(screenState.value.convoId)?.let { convo ->
|
||||||
dateDiff
|
screenState.setValue { old ->
|
||||||
} else {
|
old.copy(convo = convo)
|
||||||
val idDiff = m2.id - m1.id
|
}
|
||||||
idDiff.toInt()
|
onPinnedMessageChanged(convo.pinnedMessage)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
if (refreshMessages) {
|
||||||
|
val localMessages = messagesUseCase.getLocalMessages(screenState.value.convoId)
|
||||||
|
messages.setValue { localMessages }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
after = ::syncUiMessages
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+44
-10
@@ -10,6 +10,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import dev.meloda.fast.common.extensions.listenValue
|
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.removeIfCompat
|
||||||
import dev.meloda.fast.common.extensions.setValue
|
import dev.meloda.fast.common.extensions.setValue
|
||||||
import dev.meloda.fast.common.provider.ResourceProvider
|
import dev.meloda.fast.common.provider.ResourceProvider
|
||||||
@@ -214,13 +215,15 @@ internal class MessagesHistoryMessageActions(
|
|||||||
syncUiMessages()
|
syncUiMessages()
|
||||||
},
|
},
|
||||||
success = { response ->
|
success = { response ->
|
||||||
val newMessages = messages.value.toMutableList()
|
viewModelScope.launch {
|
||||||
newMessages[newMessages.indexOf(newMessage)] = newMessage.copy(
|
messagesUseCase.storeMessage(
|
||||||
id = response.messageId,
|
newMessage.copy(
|
||||||
cmId = response.cmId
|
id = response.messageId,
|
||||||
)
|
cmId = response.cmId
|
||||||
messages.setValue { newMessages }
|
)
|
||||||
syncUiMessages()
|
)
|
||||||
|
refreshMessagesFromDb()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -232,10 +235,9 @@ internal class MessagesHistoryMessageActions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun editCurrentEditMessage() {
|
fun editCurrentEditMessage() {
|
||||||
replyToCmId = null
|
|
||||||
|
|
||||||
val newText = screenState.value.message.text
|
val newText = screenState.value.message.text
|
||||||
val lastText = lastMessageText.orEmpty().trim()
|
val lastText = lastMessageText.orEmpty().trim()
|
||||||
|
val currentReplyToCmId = replyToCmId
|
||||||
|
|
||||||
screenState.setValue { old ->
|
screenState.setValue { old ->
|
||||||
old.copy(
|
old.copy(
|
||||||
@@ -253,11 +255,33 @@ internal class MessagesHistoryMessageActions(
|
|||||||
syncUiMessages()
|
syncUiMessages()
|
||||||
|
|
||||||
val newMessage = editMessage?.copy(
|
val newMessage = editMessage?.copy(
|
||||||
replyMessage = if (replyToCmId == null) null else editMessage?.replyMessage,
|
replyMessage = if (currentReplyToCmId == null) null else editMessage?.replyMessage,
|
||||||
text = newText
|
text = newText
|
||||||
) ?: return
|
) ?: return
|
||||||
|
|
||||||
Log.d("MessagesHistoryViewModelImpl", "editMessage: $newMessage")
|
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) {
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+55
-45
@@ -10,12 +10,13 @@ import androidx.core.content.FileProvider
|
|||||||
import androidx.core.graphics.drawable.toBitmapOrNull
|
import androidx.core.graphics.drawable.toBitmapOrNull
|
||||||
import coil.imageLoader
|
import coil.imageLoader
|
||||||
import coil.request.ImageRequest
|
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.listenValue
|
||||||
import dev.meloda.fast.common.extensions.setValue
|
import dev.meloda.fast.common.extensions.setValue
|
||||||
import dev.meloda.fast.data.State
|
import dev.meloda.fast.data.State
|
||||||
import dev.meloda.fast.data.VkUtils
|
import dev.meloda.fast.data.VkUtils
|
||||||
import dev.meloda.fast.data.processState
|
import dev.meloda.fast.data.processState
|
||||||
|
import dev.meloda.fast.domain.ConvoUseCase
|
||||||
import dev.meloda.fast.domain.MessagesUseCase
|
import dev.meloda.fast.domain.MessagesUseCase
|
||||||
import dev.meloda.fast.messageshistory.model.MessagesHistoryScreenState
|
import dev.meloda.fast.messageshistory.model.MessagesHistoryScreenState
|
||||||
import dev.meloda.fast.model.BaseError
|
import dev.meloda.fast.model.BaseError
|
||||||
@@ -33,6 +34,7 @@ import java.io.FileOutputStream
|
|||||||
internal class MessagesHistoryMessageTransportActions(
|
internal class MessagesHistoryMessageTransportActions(
|
||||||
private val applicationContext: Context,
|
private val applicationContext: Context,
|
||||||
private val viewModelScope: CoroutineScope,
|
private val viewModelScope: CoroutineScope,
|
||||||
|
private val convoUseCase: ConvoUseCase,
|
||||||
private val messagesUseCase: MessagesUseCase,
|
private val messagesUseCase: MessagesUseCase,
|
||||||
private val screenState: MutableStateFlow<MessagesHistoryScreenState>,
|
private val screenState: MutableStateFlow<MessagesHistoryScreenState>,
|
||||||
private val messages: MutableStateFlow<List<VkMessage>>,
|
private val messages: MutableStateFlow<List<VkMessage>>,
|
||||||
@@ -49,17 +51,14 @@ internal class MessagesHistoryMessageTransportActions(
|
|||||||
state.processState(
|
state.processState(
|
||||||
error = ::handleError,
|
error = ::handleError,
|
||||||
success = {
|
success = {
|
||||||
val newMessages = messages.value
|
viewModelScope.launch {
|
||||||
.toMutableList()
|
messageIds.forEach { messageId ->
|
||||||
.map { message ->
|
messagesUseCase.getLocalMessageById(messageId)?.let { localMessage ->
|
||||||
if (message.id in messageIds) {
|
messagesUseCase.storeMessage(localMessage.copy(isImportant = important))
|
||||||
message.copy(isImportant = important)
|
|
||||||
} else {
|
|
||||||
message
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
messages.setValue { newMessages }
|
refreshMessagesFromDb()
|
||||||
syncUiMessages()
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -80,12 +79,17 @@ internal class MessagesHistoryMessageTransportActions(
|
|||||||
state.processState(
|
state.processState(
|
||||||
error = ::handleError,
|
error = ::handleError,
|
||||||
success = {
|
success = {
|
||||||
onSuccess()
|
viewModelScope.launch {
|
||||||
val newMessages = messages.value.toMutableList()
|
onSuccess()
|
||||||
val messagesToDelete = newMessages.filter { it.id in messageIds }
|
val localMessageIds = mutableListOf<Long>()
|
||||||
newMessages.removeAll(messagesToDelete)
|
messageIds.forEach { messageId ->
|
||||||
messages.setValue { newMessages }
|
messagesUseCase.getLocalMessageById(messageId)?.let { localMessage ->
|
||||||
syncUiMessages()
|
localMessageIds += localMessage.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
messagesUseCase.deleteLocalMessages(localMessageIds)
|
||||||
|
refreshMessagesFromDb()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -100,14 +104,10 @@ internal class MessagesHistoryMessageTransportActions(
|
|||||||
state.processState(
|
state.processState(
|
||||||
error = ::handleError,
|
error = ::handleError,
|
||||||
success = { pinnedMessage ->
|
success = { pinnedMessage ->
|
||||||
onPinnedMessageChanged(pinnedMessage)
|
viewModelScope.launch {
|
||||||
|
onPinnedMessageChanged(pinnedMessage)
|
||||||
val newMessages = messages.value.toMutableList()
|
messagesUseCase.storeMessage(pinnedMessage)
|
||||||
val index = newMessages.indexOfFirstOrNull { it.id == messageId }
|
refreshMessagesFromDb()
|
||||||
if (index != null) {
|
|
||||||
newMessages[index] = pinnedMessage
|
|
||||||
messages.setValue { newMessages }
|
|
||||||
syncUiMessages()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -118,20 +118,18 @@ internal class MessagesHistoryMessageTransportActions(
|
|||||||
messagesUseCase.unpin(screenState.value.convoId)
|
messagesUseCase.unpin(screenState.value.convoId)
|
||||||
.listenValue(viewModelScope) { state ->
|
.listenValue(viewModelScope) { state ->
|
||||||
state.processState(
|
state.processState(
|
||||||
error = ::handleError,
|
error = ::handleError,
|
||||||
success = {
|
success = {
|
||||||
val newMessages = messages.value.toMutableList()
|
viewModelScope.launch {
|
||||||
val index = newMessages.indexOfFirstOrNull { it.id == messageId }
|
messagesUseCase.getLocalMessageById(messageId)?.let { localMessage ->
|
||||||
if (index != null) {
|
messagesUseCase.storeMessage(localMessage.copy(isPinned = false))
|
||||||
newMessages[index] = newMessages[index].copy(isPinned = false)
|
|
||||||
messages.setValue { newMessages }
|
|
||||||
syncUiMessages()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onPinnedMessageChanged(null)
|
onPinnedMessageChanged(null)
|
||||||
|
refreshMessagesFromDb()
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
}
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun readMessage(message: VkMessage) {
|
fun readMessage(message: VkMessage) {
|
||||||
@@ -142,17 +140,19 @@ internal class MessagesHistoryMessageTransportActions(
|
|||||||
state.processState(
|
state.processState(
|
||||||
error = ::handleError,
|
error = ::handleError,
|
||||||
success = {
|
success = {
|
||||||
val oldConvo = screenState.value.convo
|
viewModelScope.launch {
|
||||||
val newConvo = oldConvo.copy(
|
convoUseCase.getLocalConvoById(screenState.value.convoId)?.let { localConvo ->
|
||||||
inRead = if (!message.isOut) message.id else oldConvo.inRead,
|
convoUseCase.storeConvos(
|
||||||
outRead = if (message.isOut) message.id else oldConvo.outRead
|
listOf(
|
||||||
)
|
localConvo.copy(
|
||||||
|
inRead = if (!message.isOut) message.id else localConvo.inRead,
|
||||||
screenState.setValue { old ->
|
outRead = if (message.isOut) message.id else localConvo.outRead
|
||||||
old.copy(convo = newConvo)
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
refreshMessagesFromDb()
|
||||||
}
|
}
|
||||||
|
|
||||||
syncUiMessages()
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -219,4 +219,14 @@ internal class MessagesHistoryMessageTransportActions(
|
|||||||
baseError.setValue { newBaseError }
|
baseError.setValue { newBaseError }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun refreshMessagesFromDb() {
|
||||||
|
viewModelScope.launchDbRefresh(
|
||||||
|
load = {
|
||||||
|
val localMessages = messagesUseCase.getLocalMessages(screenState.value.convoId)
|
||||||
|
messages.setValue { localMessages }
|
||||||
|
},
|
||||||
|
after = ::syncUiMessages
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-15
@@ -12,10 +12,11 @@ import dev.meloda.fast.common.extensions.getParcelableCompat
|
|||||||
import dev.meloda.fast.common.extensions.listenValue
|
import dev.meloda.fast.common.extensions.listenValue
|
||||||
import dev.meloda.fast.common.extensions.setValue
|
import dev.meloda.fast.common.extensions.setValue
|
||||||
import dev.meloda.fast.common.provider.ResourceProvider
|
import dev.meloda.fast.common.provider.ResourceProvider
|
||||||
|
import dev.meloda.fast.data.processState
|
||||||
import dev.meloda.fast.datastore.AppSettings
|
import dev.meloda.fast.datastore.AppSettings
|
||||||
import dev.meloda.fast.domain.ConvoUseCase
|
import dev.meloda.fast.domain.ConvoUseCase
|
||||||
import dev.meloda.fast.domain.GetMessageReadPeersUseCase
|
import dev.meloda.fast.domain.GetMessageReadPeersUseCase
|
||||||
import dev.meloda.fast.domain.LongPollUpdatesParser
|
import dev.meloda.fast.domain.LongPollUpdatesReducer
|
||||||
import dev.meloda.fast.domain.MessagesUseCase
|
import dev.meloda.fast.domain.MessagesUseCase
|
||||||
import dev.meloda.fast.messageshistory.model.ActionMode
|
import dev.meloda.fast.messageshistory.model.ActionMode
|
||||||
import dev.meloda.fast.messageshistory.model.MessageDialog
|
import dev.meloda.fast.messageshistory.model.MessageDialog
|
||||||
@@ -27,6 +28,8 @@ import dev.meloda.fast.model.BaseError
|
|||||||
import dev.meloda.fast.model.api.domain.VkMessage
|
import dev.meloda.fast.model.api.domain.VkMessage
|
||||||
import dev.meloda.fast.ui.model.vk.MessageUiItem
|
import dev.meloda.fast.ui.model.vk.MessageUiItem
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
@@ -38,7 +41,7 @@ class MessagesHistoryViewModelImpl(
|
|||||||
private val convoUseCase: ConvoUseCase,
|
private val convoUseCase: ConvoUseCase,
|
||||||
private val resourceProvider: ResourceProvider,
|
private val resourceProvider: ResourceProvider,
|
||||||
private val getMessageReadPeersUseCase: GetMessageReadPeersUseCase,
|
private val getMessageReadPeersUseCase: GetMessageReadPeersUseCase,
|
||||||
updatesParser: LongPollUpdatesParser,
|
updatesReducer: LongPollUpdatesReducer,
|
||||||
savedStateHandle: SavedStateHandle
|
savedStateHandle: SavedStateHandle
|
||||||
) : MessagesHistoryViewModel, ViewModel() {
|
) : MessagesHistoryViewModel, ViewModel() {
|
||||||
|
|
||||||
@@ -80,6 +83,7 @@ class MessagesHistoryViewModelImpl(
|
|||||||
private val messageTransportActions = MessagesHistoryMessageTransportActions(
|
private val messageTransportActions = MessagesHistoryMessageTransportActions(
|
||||||
applicationContext = applicationContext,
|
applicationContext = applicationContext,
|
||||||
viewModelScope = viewModelScope,
|
viewModelScope = viewModelScope,
|
||||||
|
convoUseCase = convoUseCase,
|
||||||
messagesUseCase = messagesUseCase,
|
messagesUseCase = messagesUseCase,
|
||||||
screenState = screenState,
|
screenState = screenState,
|
||||||
messages = messages,
|
messages = messages,
|
||||||
@@ -103,11 +107,14 @@ class MessagesHistoryViewModelImpl(
|
|||||||
)
|
)
|
||||||
|
|
||||||
private val longPollEventHandler = MessagesHistoryLongPollEventHandler(
|
private val longPollEventHandler = MessagesHistoryLongPollEventHandler(
|
||||||
|
scope = viewModelScope,
|
||||||
|
convoUseCase = convoUseCase,
|
||||||
|
messagesUseCase = messagesUseCase,
|
||||||
screenState = screenState,
|
screenState = screenState,
|
||||||
messages = messages
|
messages = messages,
|
||||||
) {
|
syncUiMessages = ::syncUiMessages,
|
||||||
syncUiMessages()
|
onPinnedMessageChanged = pinnedMessageHandler::update
|
||||||
}
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val arguments = MessagesHistory.from(savedStateHandle).arguments
|
val arguments = MessagesHistory.from(savedStateHandle).arguments
|
||||||
@@ -117,15 +124,15 @@ class MessagesHistoryViewModelImpl(
|
|||||||
loaders.loadConvo()
|
loaders.loadConvo()
|
||||||
loaders.loadMessagesHistory(currentOffset.value)
|
loaders.loadMessagesHistory(currentOffset.value)
|
||||||
|
|
||||||
updatesParser.onNewMessage(longPollEventHandler::onNewMessage)
|
updatesReducer.newMessages.onEach(longPollEventHandler::onNewMessage).launchIn(viewModelScope)
|
||||||
updatesParser.onMessageEdited(longPollEventHandler::onMessageEdited)
|
updatesReducer.messageEdited.onEach(longPollEventHandler::onMessageEdited).launchIn(viewModelScope)
|
||||||
updatesParser.onMessageIncomingRead(longPollEventHandler::onReadIncoming)
|
updatesReducer.messageIncomingRead.onEach(longPollEventHandler::onReadIncoming).launchIn(viewModelScope)
|
||||||
updatesParser.onMessageOutgoingRead(longPollEventHandler::onReadOutgoing)
|
updatesReducer.messageOutgoingRead.onEach(longPollEventHandler::onReadOutgoing).launchIn(viewModelScope)
|
||||||
updatesParser.onMessageDeleted(longPollEventHandler::onMessageDeleted)
|
updatesReducer.messageDeleted.onEach(longPollEventHandler::onMessageDeleted).launchIn(viewModelScope)
|
||||||
updatesParser.onMessageRestored(longPollEventHandler::onMessageRestored)
|
updatesReducer.messageRestored.onEach(longPollEventHandler::onMessageRestored).launchIn(viewModelScope)
|
||||||
updatesParser.onMessageMarkedAsImportant(longPollEventHandler::onMessageMarkedAsImportant)
|
updatesReducer.messageMarkedAsImportant.onEach(longPollEventHandler::onMessageMarkedAsImportant).launchIn(viewModelScope)
|
||||||
updatesParser.onMessageMarkedAsSpam(longPollEventHandler::onMessageMarkedAsSpam)
|
updatesReducer.messageMarkedAsSpam.onEach(longPollEventHandler::onMessageMarkedAsSpam).launchIn(viewModelScope)
|
||||||
updatesParser.onMessageMarkedAsNotSpam(longPollEventHandler::onMessageMarkedAsNotSpam)
|
updatesReducer.messageMarkedAsNotSpam.onEach(longPollEventHandler::onMessageMarkedAsNotSpam).launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNavigationConsumed() {
|
override fun onNavigationConsumed() {
|
||||||
|
|||||||
Reference in New Issue
Block a user