refactor: extract message loading

This commit is contained in:
Codex
2026-05-14 18:16:43 +03:00
parent 6961ac7240
commit 5dc000341b
2 changed files with 159 additions and 117 deletions
@@ -0,0 +1,140 @@
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.orDots
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
import dev.meloda.fast.common.paging.loadingFlags
import dev.meloda.fast.common.paging.mergePage
import dev.meloda.fast.common.provider.ResourceProvider
import dev.meloda.fast.data.State
import dev.meloda.fast.data.VkUtils
import dev.meloda.fast.data.processState
import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.domain.ConvoUseCase
import dev.meloda.fast.domain.MessagesUseCase
import dev.meloda.fast.domain.util.extractAvatar
import dev.meloda.fast.domain.util.extractTitle
import dev.meloda.fast.messageshistory.model.MessagesHistoryScreenState
import dev.meloda.fast.model.BaseError
import dev.meloda.fast.model.api.domain.VkMessage
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
internal class MessagesHistoryLoaders(
private val convoUseCase: ConvoUseCase,
private val messagesUseCase: MessagesUseCase,
private val resourceProvider: ResourceProvider,
private val screenState: MutableStateFlow<MessagesHistoryScreenState>,
private val messages: MutableStateFlow<List<VkMessage>>,
private val imagesToPreload: MutableStateFlow<List<String>>,
private val canPaginate: MutableStateFlow<Boolean>,
private val baseError: MutableStateFlow<BaseError?>,
private val scope: CoroutineScope,
private val syncUiMessages: () -> Unit,
private val onPinnedMessage: (VkMessage?) -> Unit,
) {
fun loadConvo() {
Log.d("MessagesHistoryViewModelImpl", "loadConvo()")
convoUseCase.getById(
peerIds = listOf(screenState.value.convoId),
extended = true,
fields = VkConstants.ALL_FIELDS
).listenValue(scope) { state ->
state.processState(
error = ::handleError,
success = { response ->
val convo = response.firstOrNull() ?: return@listenValue
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)
}
)
}
}
fun loadMessagesHistory(offset: Int) {
Log.d("MessagesHistoryViewModel", "loadMessagesHistory: $offset")
messagesUseCase.getMessagesHistory(
convoId = screenState.value.convoId,
count = MessagesHistoryViewModelImpl.MESSAGES_LOAD_COUNT,
offset = offset,
).listenValue(scope) { state ->
state.processState(
error = ::handleError,
success = { response ->
val messages = response.messages
val fullMessages = mergePage(this.messages.value, messages, offset)
.sorted()
imagesToPreload.setValue {
messages.mapNotNull { it.extractAvatar().extractUrl() }
}
messagesUseCase.storeMessages(messages)
convoUseCase.storeConvos(response.convos)
val itemsCountSufficient = canPaginatePage(
MessagesHistoryViewModelImpl.MESSAGES_LOAD_COUNT,
messages.size
)
val paginationExhausted = isPaginationExhaustedPage(
pageSize = MessagesHistoryViewModelImpl.MESSAGES_LOAD_COUNT,
loadedCount = messages.size,
hasExistingItems = this.messages.value.isNotEmpty()
)
screenState.setValue { old ->
old.copy(isPaginationExhausted = paginationExhausted)
}
this.messages.emit(fullMessages)
syncUiMessages()
canPaginate.setValue { itemsCountSufficient }
}
)
val flags = loadingFlags(offset, state.isLoading())
screenState.setValue { old ->
old.copy(
isLoading = flags.isLoading,
isPaginating = flags.isPaginating
)
}
}
}
private fun handleError(error: State.Error) {
VkUtils.parseError(error)?.let { newBaseError ->
baseError.setValue { newBaseError }
}
}
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()
}
}
}
}
@@ -2,35 +2,23 @@ package dev.meloda.fast.messageshistory
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.util.Log import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.conena.nanokt.collections.indexOfFirstOrNull import com.conena.nanokt.collections.indexOfFirstOrNull
import dev.meloda.fast.common.VkConstants
import dev.meloda.fast.common.extensions.getParcelableCompat 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.orDots import dev.meloda.fast.common.extensions.orDots
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.common.paging.canPaginate as canPaginatePage
import dev.meloda.fast.common.paging.isPaginationExhausted as isPaginationExhaustedPage
import dev.meloda.fast.common.paging.loadingFlags
import dev.meloda.fast.common.paging.mergePage
import dev.meloda.fast.data.State
import dev.meloda.fast.data.VkUtils
import dev.meloda.fast.data.VkMemoryCache import dev.meloda.fast.data.VkMemoryCache
import dev.meloda.fast.data.processState
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.LoadConvosByIdUseCase
import dev.meloda.fast.domain.LongPollUpdatesParser import dev.meloda.fast.domain.LongPollUpdatesParser
import dev.meloda.fast.domain.MessagesUseCase import dev.meloda.fast.domain.MessagesUseCase
import dev.meloda.fast.domain.util.extractAvatar
import dev.meloda.fast.domain.util.extractTitle
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
import dev.meloda.fast.messageshistory.model.MessageNavigation import dev.meloda.fast.messageshistory.model.MessageNavigation
@@ -52,7 +40,6 @@ class MessagesHistoryViewModelImpl(
private val messagesUseCase: MessagesUseCase, private val messagesUseCase: MessagesUseCase,
private val convoUseCase: ConvoUseCase, private val convoUseCase: ConvoUseCase,
private val resourceProvider: ResourceProvider, private val resourceProvider: ResourceProvider,
private val loadConvosByIdUseCase: LoadConvosByIdUseCase,
private val getMessageReadPeersUseCase: GetMessageReadPeersUseCase, private val getMessageReadPeersUseCase: GetMessageReadPeersUseCase,
updatesParser: LongPollUpdatesParser, updatesParser: LongPollUpdatesParser,
savedStateHandle: SavedStateHandle savedStateHandle: SavedStateHandle
@@ -91,6 +78,20 @@ class MessagesHistoryViewModelImpl(
onPinnedMessageChanged = ::handlePinnedMessage onPinnedMessageChanged = ::handlePinnedMessage
) )
private val loaders = MessagesHistoryLoaders(
convoUseCase = convoUseCase,
messagesUseCase = messagesUseCase,
resourceProvider = resourceProvider,
screenState = screenState,
messages = messages,
imagesToPreload = imagesToPreload,
canPaginate = canPaginate,
baseError = baseError,
scope = viewModelScope,
syncUiMessages = ::syncUiMessages,
onPinnedMessage = ::handlePinnedMessage
)
private val longPollEventHandler = MessagesHistoryLongPollEventHandler( private val longPollEventHandler = MessagesHistoryLongPollEventHandler(
screenState = screenState, screenState = screenState,
messages = messages messages = messages
@@ -103,8 +104,8 @@ class MessagesHistoryViewModelImpl(
screenState.setValue { old -> old.copy(convoId = arguments.convoId) } screenState.setValue { old -> old.copy(convoId = arguments.convoId) }
loadConvo() loaders.loadConvo()
loadMessagesHistory() loaders.loadMessagesHistory(currentOffset.value)
updatesParser.onNewMessage(longPollEventHandler::onNewMessage) updatesParser.onNewMessage(longPollEventHandler::onNewMessage)
updatesParser.onMessageEdited(longPollEventHandler::onMessageEdited) updatesParser.onMessageEdited(longPollEventHandler::onMessageEdited)
@@ -311,7 +312,7 @@ class MessagesHistoryViewModelImpl(
} }
override fun onRefresh() { override fun onRefresh() {
loadMessagesHistory(offset = 0) loaders.loadMessagesHistory(offset = 0)
} }
override fun onAttachmentButtonClicked() { override fun onAttachmentButtonClicked() {
@@ -370,7 +371,7 @@ class MessagesHistoryViewModelImpl(
override fun onPaginationConditionsMet() { override fun onPaginationConditionsMet() {
currentOffset.update { messages.value.size } currentOffset.update { messages.value.size }
loadMessagesHistory() loaders.loadMessagesHistory(currentOffset.value)
} }
override fun onMessageClicked(messageId: Long) { override fun onMessageClicked(messageId: Long) {
@@ -496,38 +497,6 @@ class MessagesHistoryViewModelImpl(
} }
} }
private fun loadConvo() {
Log.d("MessagesHistoryViewModelImpl", "loadConvo()")
loadConvosByIdUseCase(
peerIds = listOf(screenState.value.convoId),
extended = true,
fields = VkConstants.ALL_FIELDS
).listenValue(viewModelScope) { state ->
state.processState(
error = ::handleError,
success = { response ->
val convo = response.firstOrNull() ?: return@listenValue
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
)
}
convo.pinnedMessage?.let(::handlePinnedMessage)
}
)
}
}
private fun handlePinnedMessage(pinnedMessage: VkMessage?) { private fun handlePinnedMessage(pinnedMessage: VkMessage?) {
if (pinnedMessage == null) { if (pinnedMessage == null) {
screenState.setValue { old -> screenState.setValue { old ->
@@ -565,73 +534,6 @@ class MessagesHistoryViewModelImpl(
} }
} }
private fun loadMessagesHistory(offset: Int = currentOffset.value) {
Log.d("MessagesHistoryViewModel", "loadMessagesHistory: $offset")
messagesUseCase.getMessagesHistory(
convoId = screenState.value.convoId,
count = MESSAGES_LOAD_COUNT,
offset = offset,
).listenValue(viewModelScope) { state ->
state.processState(
error = ::handleError,
success = { response ->
val messages = response.messages
val fullMessages = mergePage(this.messages.value, messages, offset).sorted()
val convos = response.convos
imagesToPreload.setValue {
messages.mapNotNull { it.extractAvatar().extractUrl() }
}
messagesUseCase.storeMessages(messages)
convoUseCase.storeConvos(convos)
val itemsCountSufficient = canPaginatePage(MESSAGES_LOAD_COUNT, messages.size)
val paginationExhausted = isPaginationExhaustedPage(
pageSize = MESSAGES_LOAD_COUNT,
loadedCount = messages.size,
hasExistingItems = this.messages.value.isNotEmpty()
)
screenState.setValue { old ->
old.copy(isPaginationExhausted = paginationExhausted)
}
this.messages.emit(fullMessages)
syncUiMessages()
canPaginate.setValue { itemsCountSufficient }
}
)
val flags = loadingFlags(offset, state.isLoading())
screenState.setValue { old ->
old.copy(
isLoading = flags.isLoading,
isPaginating = flags.isPaginating
)
}
}
}
private fun handleError(error: State.Error) {
VkUtils.parseError(error)?.let { newBaseError ->
baseError.setValue { newBaseError }
}
}
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 syncUiMessages(): List<MessageUiItem> { private fun syncUiMessages(): List<MessageUiItem> {
val newUiMessages = buildMessagesHistoryUiMessages( val newUiMessages = buildMessagesHistoryUiMessages(
messages = messages.value, messages = messages.value,