refactor: unify db refresh flows

This commit is contained in:
Codex
2026-05-14 20:45:24 +03:00
parent f24eae8209
commit f6c6ed59f3
32 changed files with 882 additions and 408 deletions
@@ -3,6 +3,7 @@ package dev.meloda.fast.messageshistory
import android.util.Log
import dev.meloda.fast.common.VkConstants
import dev.meloda.fast.common.extensions.listenValue
import dev.meloda.fast.common.extensions.launchDbRefresh
import dev.meloda.fast.common.extensions.setValue
import dev.meloda.fast.common.paging.canPaginate as canPaginatePage
import dev.meloda.fast.common.paging.isPaginationExhausted as isPaginationExhaustedPage
@@ -39,6 +40,29 @@ internal class MessagesHistoryLoaders(
fun loadConvo() {
Log.d("MessagesHistoryViewModelImpl", "loadConvo()")
scope.launchDbRefresh(
load = {
convoUseCase.getLocalConvoById(screenState.value.convoId)?.let { convo ->
val title = convo.extractTitle(
useContactName = AppSettings.General.useContactNames,
resources = resourceProvider.resources
)
val avatar = convo.extractAvatar()
screenState.setValue { old ->
old.copy(
convo = convo,
title = title,
avatar = avatar
)
}
onPinnedMessage(convo.pinnedMessage)
}
},
after = {}
)
convoUseCase.getById(
peerIds = listOf(screenState.value.convoId),
extended = true,
@@ -71,6 +95,22 @@ internal class MessagesHistoryLoaders(
fun loadMessagesHistory(offset: Int) {
Log.d("MessagesHistoryViewModel", "loadMessagesHistory: $offset")
if (offset == 0) {
scope.launchDbRefresh(
load = {
val cachedMessages = messagesUseCase.getLocalMessages(screenState.value.convoId)
if (cachedMessages.isNotEmpty()) {
messages.emit(cachedMessages.sorted())
}
},
after = {
if (messages.value.isNotEmpty()) {
syncUiMessages()
}
}
)
}
messagesUseCase.getMessagesHistory(
convoId = screenState.value.convoId,
count = MessagesHistoryViewModelImpl.MESSAGES_LOAD_COUNT,
@@ -1,18 +1,24 @@
package dev.meloda.fast.messageshistory
import android.util.Log
import com.conena.nanokt.collections.indexOfFirstOrNull
import dev.meloda.fast.common.extensions.launchDbRefresh
import dev.meloda.fast.common.extensions.setValue
import dev.meloda.fast.domain.ConvoUseCase
import dev.meloda.fast.domain.MessagesUseCase
import dev.meloda.fast.messageshistory.model.MessagesHistoryScreenState
import dev.meloda.fast.model.LongPollParsedEvent
import dev.meloda.fast.model.api.domain.VkMessage
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlin.math.abs
internal class MessagesHistoryLongPollEventHandler(
private val scope: CoroutineScope,
private val convoUseCase: ConvoUseCase,
private val messagesUseCase: MessagesUseCase,
private val screenState: MutableStateFlow<MessagesHistoryScreenState>,
private val messages: MutableStateFlow<List<VkMessage>>,
private val syncUiMessages: () -> Unit
private val syncUiMessages: () -> Unit,
private val onPinnedMessageChanged: (VkMessage?) -> Unit
) {
fun onNewMessage(event: LongPollParsedEvent.NewMessage) {
val message = event.message
@@ -20,139 +26,74 @@ internal class MessagesHistoryLongPollEventHandler(
Log.d("MessagesHistoryViewModel", "handleNewMessage: $message")
if (message.peerId != screenState.value.convoId) return
if (messages.value.indexOfFirstOrNull { it.id == message.id } != null) return
val randomIds = messages.value.map(VkMessage::randomId)
if (message.randomId != 0L && message.randomId in randomIds) return
val newMessages = messages.value.toMutableList()
newMessages.add(0, message)
messages.setValue { newMessages }
syncUiMessages()
refreshFromDb(refreshMessages = true)
}
fun onMessageEdited(event: LongPollParsedEvent.MessageEdited) {
val message = event.message
if (message.peerId != screenState.value.convoId) return
val newMessages = messages.value.toMutableList()
val index = newMessages.indexOfFirstOrNull { it.id == message.id }
if (index == null) {
return
}
newMessages[index] = message
messages.setValue { newMessages }
syncUiMessages()
refreshFromDb(refreshMessages = true)
}
fun onReadIncoming(event: LongPollParsedEvent.IncomingMessageRead) {
if (event.peerId != screenState.value.convoId) return
val index = messages.value.indexOfFirstOrNull { it.cmId == event.cmId }
if (index == null) return
val newConvo = screenState.value.convo.copy(
inReadCmId = event.cmId
)
screenState.setValue { old ->
old.copy(convo = newConvo)
}
syncUiMessages()
refreshFromDb(refreshMessages = false)
}
fun onReadOutgoing(event: LongPollParsedEvent.OutgoingMessageRead) {
if (event.peerId != screenState.value.convoId) return
val index = messages.value.indexOfFirstOrNull { it.cmId == event.cmId }
if (index == null) return
val newConvo = screenState.value.convo.copy(
outReadCmId = event.cmId
)
screenState.setValue { old ->
old.copy(convo = newConvo)
}
syncUiMessages()
refreshFromDb(refreshMessages = false)
}
fun onMessageDeleted(event: LongPollParsedEvent.MessageDeleted) {
if (event.peerId != screenState.value.convoId) return
val newMessages = messages.value.toMutableList()
val index = newMessages.indexOfFirstOrNull { it.cmId == event.cmId }
if (index == null) return
newMessages.removeAt(index)
messages.setValue { newMessages }
syncUiMessages()
refreshFromDb(refreshMessages = true)
}
fun onMessageRestored(event: LongPollParsedEvent.MessageRestored) {
if (event.message.peerId != screenState.value.convoId) return
val newMessages = messages.value.toMutableList()
val minDate = newMessages.minOf(VkMessage::date)
if (event.message.date < minDate) return
newMessages.add(event.message)
messages.setValue { newMessages.sorted() }
syncUiMessages()
refreshFromDb(refreshMessages = true)
}
fun onMessageMarkedAsImportant(event: LongPollParsedEvent.MessageMarkedAsImportant) {
if (event.peerId != screenState.value.convoId) return
val newMessages = messages.value.toMutableList()
val index = newMessages.indexOfFirstOrNull { it.cmId == event.cmId }
if (index == null) return
newMessages[index] = newMessages[index].copy(isImportant = event.marked)
messages.setValue { newMessages }
syncUiMessages()
refreshFromDb(refreshMessages = true)
}
fun onMessageMarkedAsSpam(event: LongPollParsedEvent.MessageMarkedAsSpam) {
if (event.peerId != screenState.value.convoId) return
val newMessages = messages.value.toMutableList()
val index = newMessages.indexOfFirstOrNull { it.cmId == event.cmId }
if (index == null) return
newMessages.removeAt(index)
messages.setValue { newMessages }
syncUiMessages()
refreshFromDb(refreshMessages = true)
}
fun onMessageMarkedAsNotSpam(event: LongPollParsedEvent.MessageMarkedAsNotSpam) {
if (event.message.peerId != screenState.value.convoId) return
val newMessages = messages.value.toMutableList()
val maxDate = newMessages.maxOf(VkMessage::date)
val minDate = newMessages.minOf(VkMessage::date)
if (event.message.date !in minDate..maxDate) return
newMessages.add(event.message)
messages.setValue { newMessages.sorted() }
syncUiMessages()
refreshFromDb(refreshMessages = true)
}
private fun List<VkMessage>.sorted(): List<VkMessage> {
return sortedWith { m1, m2 ->
val dateDiff = m2.date - m1.date
if (dateDiff != 0) {
dateDiff
} else {
val idDiff = m2.id - m1.id
idDiff.toInt()
}
}
private fun refreshFromDb(refreshMessages: Boolean) {
scope.launchDbRefresh(
load = {
convoUseCase.getLocalConvoById(screenState.value.convoId)?.let { convo ->
screenState.setValue { old ->
old.copy(convo = convo)
}
onPinnedMessageChanged(convo.pinnedMessage)
}
if (refreshMessages) {
val localMessages = messagesUseCase.getLocalMessages(screenState.value.convoId)
messages.setValue { localMessages }
}
},
after = ::syncUiMessages
)
}
}
@@ -10,6 +10,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextDecoration
import dev.meloda.fast.common.extensions.listenValue
import dev.meloda.fast.common.extensions.launchDbRefresh
import dev.meloda.fast.common.extensions.removeIfCompat
import dev.meloda.fast.common.extensions.setValue
import dev.meloda.fast.common.provider.ResourceProvider
@@ -214,13 +215,15 @@ internal class MessagesHistoryMessageActions(
syncUiMessages()
},
success = { response ->
val newMessages = messages.value.toMutableList()
newMessages[newMessages.indexOf(newMessage)] = newMessage.copy(
id = response.messageId,
cmId = response.cmId
)
messages.setValue { newMessages }
syncUiMessages()
viewModelScope.launch {
messagesUseCase.storeMessage(
newMessage.copy(
id = response.messageId,
cmId = response.cmId
)
)
refreshMessagesFromDb()
}
}
)
}
@@ -232,10 +235,9 @@ internal class MessagesHistoryMessageActions(
}
fun editCurrentEditMessage() {
replyToCmId = null
val newText = screenState.value.message.text
val lastText = lastMessageText.orEmpty().trim()
val currentReplyToCmId = replyToCmId
screenState.setValue { old ->
old.copy(
@@ -253,11 +255,33 @@ internal class MessagesHistoryMessageActions(
syncUiMessages()
val newMessage = editMessage?.copy(
replyMessage = if (replyToCmId == null) null else editMessage?.replyMessage,
replyMessage = if (currentReplyToCmId == null) null else editMessage?.replyMessage,
text = newText
) ?: return
Log.d("MessagesHistoryViewModelImpl", "editMessage: $newMessage")
messagesUseCase.edit(
peerId = screenState.value.convoId,
cmId = newMessage.cmId,
message = newMessage.text,
attachments = null,
formatData = newMessage.formatData
).listenValue(viewModelScope) { state ->
state.processState(
error = { error ->
Log.d("MessagesHistoryViewModelImpl", "editMessage: ERROR: $error")
},
success = {
viewModelScope.launch {
messagesUseCase.storeMessage(newMessage)
refreshMessagesFromDb()
}
}
)
}
replyToCmId = null
}
private fun updateFormatting(type: FormatDataType) {
@@ -313,4 +337,14 @@ internal class MessagesHistoryMessageActions(
}
}
private fun refreshMessagesFromDb() {
viewModelScope.launchDbRefresh(
load = {
val localMessages = messagesUseCase.getLocalMessages(screenState.value.convoId)
messages.setValue { localMessages }
},
after = ::syncUiMessages
)
}
}
@@ -10,12 +10,13 @@ import androidx.core.content.FileProvider
import androidx.core.graphics.drawable.toBitmapOrNull
import coil.imageLoader
import coil.request.ImageRequest
import com.conena.nanokt.collections.indexOfFirstOrNull
import dev.meloda.fast.common.extensions.launchDbRefresh
import dev.meloda.fast.common.extensions.listenValue
import dev.meloda.fast.common.extensions.setValue
import dev.meloda.fast.data.State
import dev.meloda.fast.data.VkUtils
import dev.meloda.fast.data.processState
import dev.meloda.fast.domain.ConvoUseCase
import dev.meloda.fast.domain.MessagesUseCase
import dev.meloda.fast.messageshistory.model.MessagesHistoryScreenState
import dev.meloda.fast.model.BaseError
@@ -33,6 +34,7 @@ import java.io.FileOutputStream
internal class MessagesHistoryMessageTransportActions(
private val applicationContext: Context,
private val viewModelScope: CoroutineScope,
private val convoUseCase: ConvoUseCase,
private val messagesUseCase: MessagesUseCase,
private val screenState: MutableStateFlow<MessagesHistoryScreenState>,
private val messages: MutableStateFlow<List<VkMessage>>,
@@ -49,17 +51,14 @@ internal class MessagesHistoryMessageTransportActions(
state.processState(
error = ::handleError,
success = {
val newMessages = messages.value
.toMutableList()
.map { message ->
if (message.id in messageIds) {
message.copy(isImportant = important)
} else {
message
viewModelScope.launch {
messageIds.forEach { messageId ->
messagesUseCase.getLocalMessageById(messageId)?.let { localMessage ->
messagesUseCase.storeMessage(localMessage.copy(isImportant = important))
}
}
messages.setValue { newMessages }
syncUiMessages()
refreshMessagesFromDb()
}
}
)
}
@@ -80,12 +79,17 @@ internal class MessagesHistoryMessageTransportActions(
state.processState(
error = ::handleError,
success = {
onSuccess()
val newMessages = messages.value.toMutableList()
val messagesToDelete = newMessages.filter { it.id in messageIds }
newMessages.removeAll(messagesToDelete)
messages.setValue { newMessages }
syncUiMessages()
viewModelScope.launch {
onSuccess()
val localMessageIds = mutableListOf<Long>()
messageIds.forEach { messageId ->
messagesUseCase.getLocalMessageById(messageId)?.let { localMessage ->
localMessageIds += localMessage.id
}
}
messagesUseCase.deleteLocalMessages(localMessageIds)
refreshMessagesFromDb()
}
}
)
}
@@ -100,14 +104,10 @@ internal class MessagesHistoryMessageTransportActions(
state.processState(
error = ::handleError,
success = { pinnedMessage ->
onPinnedMessageChanged(pinnedMessage)
val newMessages = messages.value.toMutableList()
val index = newMessages.indexOfFirstOrNull { it.id == messageId }
if (index != null) {
newMessages[index] = pinnedMessage
messages.setValue { newMessages }
syncUiMessages()
viewModelScope.launch {
onPinnedMessageChanged(pinnedMessage)
messagesUseCase.storeMessage(pinnedMessage)
refreshMessagesFromDb()
}
}
)
@@ -118,20 +118,18 @@ internal class MessagesHistoryMessageTransportActions(
messagesUseCase.unpin(screenState.value.convoId)
.listenValue(viewModelScope) { state ->
state.processState(
error = ::handleError,
success = {
val newMessages = messages.value.toMutableList()
val index = newMessages.indexOfFirstOrNull { it.id == messageId }
if (index != null) {
newMessages[index] = newMessages[index].copy(isPinned = false)
messages.setValue { newMessages }
syncUiMessages()
error = ::handleError,
success = {
viewModelScope.launch {
messagesUseCase.getLocalMessageById(messageId)?.let { localMessage ->
messagesUseCase.storeMessage(localMessage.copy(isPinned = false))
}
onPinnedMessageChanged(null)
refreshMessagesFromDb()
}
)
}
}
)
}
}
fun readMessage(message: VkMessage) {
@@ -142,17 +140,19 @@ internal class MessagesHistoryMessageTransportActions(
state.processState(
error = ::handleError,
success = {
val oldConvo = screenState.value.convo
val newConvo = oldConvo.copy(
inRead = if (!message.isOut) message.id else oldConvo.inRead,
outRead = if (message.isOut) message.id else oldConvo.outRead
)
screenState.setValue { old ->
old.copy(convo = newConvo)
viewModelScope.launch {
convoUseCase.getLocalConvoById(screenState.value.convoId)?.let { localConvo ->
convoUseCase.storeConvos(
listOf(
localConvo.copy(
inRead = if (!message.isOut) message.id else localConvo.inRead,
outRead = if (message.isOut) message.id else localConvo.outRead
)
)
)
}
refreshMessagesFromDb()
}
syncUiMessages()
}
)
}
@@ -219,4 +219,14 @@ internal class MessagesHistoryMessageTransportActions(
baseError.setValue { newBaseError }
}
}
private fun refreshMessagesFromDb() {
viewModelScope.launchDbRefresh(
load = {
val localMessages = messagesUseCase.getLocalMessages(screenState.value.convoId)
messages.setValue { localMessages }
},
after = ::syncUiMessages
)
}
}
@@ -12,10 +12,11 @@ import dev.meloda.fast.common.extensions.getParcelableCompat
import dev.meloda.fast.common.extensions.listenValue
import dev.meloda.fast.common.extensions.setValue
import dev.meloda.fast.common.provider.ResourceProvider
import dev.meloda.fast.data.processState
import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.domain.ConvoUseCase
import dev.meloda.fast.domain.GetMessageReadPeersUseCase
import dev.meloda.fast.domain.LongPollUpdatesParser
import dev.meloda.fast.domain.LongPollUpdatesReducer
import dev.meloda.fast.domain.MessagesUseCase
import dev.meloda.fast.messageshistory.model.ActionMode
import dev.meloda.fast.messageshistory.model.MessageDialog
@@ -27,6 +28,8 @@ import dev.meloda.fast.model.BaseError
import dev.meloda.fast.model.api.domain.VkMessage
import dev.meloda.fast.ui.model.vk.MessageUiItem
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
@@ -38,7 +41,7 @@ class MessagesHistoryViewModelImpl(
private val convoUseCase: ConvoUseCase,
private val resourceProvider: ResourceProvider,
private val getMessageReadPeersUseCase: GetMessageReadPeersUseCase,
updatesParser: LongPollUpdatesParser,
updatesReducer: LongPollUpdatesReducer,
savedStateHandle: SavedStateHandle
) : MessagesHistoryViewModel, ViewModel() {
@@ -80,6 +83,7 @@ class MessagesHistoryViewModelImpl(
private val messageTransportActions = MessagesHistoryMessageTransportActions(
applicationContext = applicationContext,
viewModelScope = viewModelScope,
convoUseCase = convoUseCase,
messagesUseCase = messagesUseCase,
screenState = screenState,
messages = messages,
@@ -103,11 +107,14 @@ class MessagesHistoryViewModelImpl(
)
private val longPollEventHandler = MessagesHistoryLongPollEventHandler(
scope = viewModelScope,
convoUseCase = convoUseCase,
messagesUseCase = messagesUseCase,
screenState = screenState,
messages = messages
) {
syncUiMessages()
}
messages = messages,
syncUiMessages = ::syncUiMessages,
onPinnedMessageChanged = pinnedMessageHandler::update
)
init {
val arguments = MessagesHistory.from(savedStateHandle).arguments
@@ -117,15 +124,15 @@ class MessagesHistoryViewModelImpl(
loaders.loadConvo()
loaders.loadMessagesHistory(currentOffset.value)
updatesParser.onNewMessage(longPollEventHandler::onNewMessage)
updatesParser.onMessageEdited(longPollEventHandler::onMessageEdited)
updatesParser.onMessageIncomingRead(longPollEventHandler::onReadIncoming)
updatesParser.onMessageOutgoingRead(longPollEventHandler::onReadOutgoing)
updatesParser.onMessageDeleted(longPollEventHandler::onMessageDeleted)
updatesParser.onMessageRestored(longPollEventHandler::onMessageRestored)
updatesParser.onMessageMarkedAsImportant(longPollEventHandler::onMessageMarkedAsImportant)
updatesParser.onMessageMarkedAsSpam(longPollEventHandler::onMessageMarkedAsSpam)
updatesParser.onMessageMarkedAsNotSpam(longPollEventHandler::onMessageMarkedAsNotSpam)
updatesReducer.newMessages.onEach(longPollEventHandler::onNewMessage).launchIn(viewModelScope)
updatesReducer.messageEdited.onEach(longPollEventHandler::onMessageEdited).launchIn(viewModelScope)
updatesReducer.messageIncomingRead.onEach(longPollEventHandler::onReadIncoming).launchIn(viewModelScope)
updatesReducer.messageOutgoingRead.onEach(longPollEventHandler::onReadOutgoing).launchIn(viewModelScope)
updatesReducer.messageDeleted.onEach(longPollEventHandler::onMessageDeleted).launchIn(viewModelScope)
updatesReducer.messageRestored.onEach(longPollEventHandler::onMessageRestored).launchIn(viewModelScope)
updatesReducer.messageMarkedAsImportant.onEach(longPollEventHandler::onMessageMarkedAsImportant).launchIn(viewModelScope)
updatesReducer.messageMarkedAsSpam.onEach(longPollEventHandler::onMessageMarkedAsSpam).launchIn(viewModelScope)
updatesReducer.messageMarkedAsNotSpam.onEach(longPollEventHandler::onMessageMarkedAsNotSpam).launchIn(viewModelScope)
}
override fun onNavigationConsumed() {