diff --git a/app/src/main/kotlin/dev/meloda/fast/presentation/MainScreen.kt b/app/src/main/kotlin/dev/meloda/fast/presentation/MainScreen.kt index 208308cc..120c4fe4 100644 --- a/app/src/main/kotlin/dev/meloda/fast/presentation/MainScreen.kt +++ b/app/src/main/kotlin/dev/meloda/fast/presentation/MainScreen.kt @@ -38,7 +38,7 @@ import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.hazeEffect import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.HazeMaterials -import dev.meloda.fast.convos.navigation.ConvoGraph +import dev.meloda.fast.convos.model.ConvoNavigationIntent import dev.meloda.fast.convos.navigation.convosGraph import dev.meloda.fast.friends.navigation.Friends import dev.meloda.fast.friends.navigation.friendsScreen @@ -198,15 +198,17 @@ fun MainScreen( }, ) convosGraph( - activity = activity, - onError = onError, - onNavigateToMessagesHistory = onNavigateToMessagesHistory, - onNavigateToCreateChat = onNavigateToCreateChat, - onScrolledToTop = { - tabReselected = tabReselected.toMutableMap().also { - it[ConvoGraph] = false + handleNavigationIntent = { intent -> + when (intent) { + ConvoNavigationIntent.Back -> {} + ConvoNavigationIntent.Archive -> {} + ConvoNavigationIntent.CreateChat -> onNavigateToCreateChat() + is ConvoNavigationIntent.MessagesHistory -> { + onNavigateToMessagesHistory(intent.convoId) + } } - } + }, + activity = activity, ) profileScreen( activity = activity, diff --git a/feature/convos/src/main/kotlin/dev/meloda/fast/convos/ConvosViewModel.kt b/feature/convos/src/main/kotlin/dev/meloda/fast/convos/ConvosViewModel.kt index a3498cd1..84fbcece 100644 --- a/feature/convos/src/main/kotlin/dev/meloda/fast/convos/ConvosViewModel.kt +++ b/feature/convos/src/main/kotlin/dev/meloda/fast/convos/ConvosViewModel.kt @@ -3,6 +3,7 @@ package dev.meloda.fast.convos import android.content.Context import android.content.res.Resources import android.os.Bundle +import androidx.compose.runtime.Immutable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import coil.ImageLoader @@ -15,10 +16,12 @@ import dev.meloda.fast.common.extensions.listenValue import dev.meloda.fast.common.extensions.setValue import dev.meloda.fast.common.extensions.updateValue import dev.meloda.fast.convos.model.ConvoDialog -import dev.meloda.fast.convos.model.ConvoNavigation +import dev.meloda.fast.convos.model.ConvoIntent +import dev.meloda.fast.convos.model.ConvoNavigationIntent import dev.meloda.fast.convos.model.ConvosScreenState import dev.meloda.fast.convos.model.InteractionJob import dev.meloda.fast.convos.model.NewInteractionException +import dev.meloda.fast.data.UserConfig import dev.meloda.fast.data.VkUtils import dev.meloda.fast.data.processState import dev.meloda.fast.datastore.UserSettings @@ -28,25 +31,23 @@ import dev.meloda.fast.domain.LongPollUpdatesParser import dev.meloda.fast.domain.MessagesUseCase import dev.meloda.fast.domain.util.asPresentation import dev.meloda.fast.domain.util.extractAvatar -import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.ConvosFilter import dev.meloda.fast.model.InteractionType import dev.meloda.fast.model.LongPollParsedEvent import dev.meloda.fast.model.api.domain.VkConvo import dev.meloda.fast.ui.model.vk.ConvoOption import dev.meloda.fast.ui.model.vk.UiConvo +import dev.meloda.fast.ui.util.ImmutableList import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList +import dev.meloda.fast.ui.util.buildImmutableList import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update +@Immutable class ConvosViewModel( updatesParser: LongPollUpdatesParser, - private val filter: ConvosFilter, + val filter: ConvosFilter, private val convoUseCase: ConvoUseCase, private val messagesUseCase: MessagesUseCase, private val resources: Resources, @@ -59,40 +60,18 @@ class ConvosViewModel( private val screenState = MutableStateFlow(ConvosScreenState.EMPTY) val screenStateFlow get() = screenState.asStateFlow() - private val navigation = MutableStateFlow(null) - val navigationFlow get() = navigation.asStateFlow() + private val navigationIntent = MutableStateFlow(null) + val navigationIntentFlow get() = navigationIntent.asStateFlow() - private val dialog = MutableStateFlow(null) - val dialogFlow get() = dialog.asStateFlow() + private val convos: MutableList = mutableListOf() - private val convos = MutableStateFlow>(emptyList()) - val convosFlow get() = convos.asStateFlow() + private val pinnedConvosCount get() = convos.count(VkConvo::isPinned) - private val uiConvos = MutableStateFlow>(emptyList()) - val uiConvosFlow get() = uiConvos.asStateFlow() - - private val pinnedConvosCount = convosFlow.map { convos -> - convos.count(VkConvo::isPinned) - }.stateIn(viewModelScope, SharingStarted.Eagerly, 0) - - private val baseError = MutableStateFlow(null) - val baseErrorFlow get() = baseError.asStateFlow() - - private val currentOffset = MutableStateFlow(0) - val currentOffsetFlow get() = currentOffset.asStateFlow() - - private val canPaginate = MutableStateFlow(false) - val canPaginateFlow get() = canPaginate.asStateFlow() - - private val expandedConvoId = MutableStateFlow(0L) - - private val useContactNames: Boolean get() = userSettings.useContactNames.value + private var currentOffset = 0 private val interactionsTimers = hashMapOf() init { - screenState.updateValue { copy(isArchive = filter == ConvosFilter.ARCHIVE) } - loadConvos() updatesParser.onNewMessage(::handleNewMessage) @@ -110,100 +89,143 @@ class ConvosViewModel( } } - fun onNavigationConsumed() { - navigation.setValue { null } + fun handleIntent(intent: ConvoIntent) { + when (intent) { + ConvoIntent.ArchiveClick -> { + navigationIntent.setValue { ConvoNavigationIntent.Archive } + } + + ConvoIntent.Back -> { + navigationIntent.setValue { ConvoNavigationIntent.Back } + } + + ConvoIntent.ConsumeScrollToTop -> Unit + ConvoIntent.CreateChatClick -> { + navigationIntent.setValue { ConvoNavigationIntent.CreateChat } + } + + ConvoIntent.ErrorActionButtonClick -> { + onRefresh() + } + + is ConvoIntent.ItemClick -> { + onConvoItemClick(intent.convoId) + } + + is ConvoIntent.ItemLongClick -> { + onConvoItemLongClick(intent.convoId) + } + + is ConvoIntent.OptionItemClick -> { + onOptionClicked(intent.option) + } + + ConvoIntent.PaginationConditionsMet -> { + onPaginationConditionsMet() + } + + ConvoIntent.Refresh -> { + onRefresh() + } + + is ConvoIntent.SetScrollIndex -> { + setScrollIndex(intent.index) + } + + is ConvoIntent.SetScrollOffset -> { + setScrollOffset(intent.offset) + } + + is ConvoIntent.Dialog -> { + when (intent) { + is ConvoIntent.Dialog.Cancel -> Unit + is ConvoIntent.Dialog.Confirm -> onDialogConfirmed(intent.bundle) + ConvoIntent.Dialog.Dismiss -> onDialogDismissed() + } + } + } } - fun onDialogConfirmed(dialog: ConvoDialog, bundle: Bundle) { - onDialogDismissed(dialog) + fun onNavigationConsumed() { + navigationIntent.setValue { null } + } + + private fun onDialogConfirmed(bundle: Bundle?) { + val dialog = screenState.value.dialog ?: return + onDialogDismissed() + + val convo = with(screenState.value) { + convos.find { it.id == expandedConvoId } + } ?: return when (dialog) { - is ConvoDialog.ConvoDelete -> { - deleteConvo(dialog.convoId) + is ConvoDialog.Delete -> { + deleteConvo(convo.id) } - is ConvoDialog.ConvoPin -> { - pinConvo(dialog.convoId, true) + is ConvoDialog.Pin -> { + pinConvo(convo.id, true) } - is ConvoDialog.ConvoUnpin -> { - pinConvo(dialog.convoId, false) + is ConvoDialog.Unpin -> { + pinConvo(convo.id, false) } - is ConvoDialog.ConvoArchive -> { - archiveConvo(dialog.convoId, true) + is ConvoDialog.Archive -> { + archiveConvo(convo.id, true) } - is ConvoDialog.ConvoUnarchive -> { - archiveConvo(dialog.convoId, false) + is ConvoDialog.Unarchive -> { + archiveConvo(convo.id, false) } } - expandedConvoId.setValue { 0 } + collapseConvos(false) syncUiConvos() } - fun onDialogDismissed(dialog: ConvoDialog) { - this.dialog.setValue { null } + private fun onDialogDismissed() { + screenState.updateValue { copy(dialog = null) } } - fun onDialogItemPicked(dialog: ConvoDialog, bundle: Bundle) { - when (dialog) { - is ConvoDialog.ConvoDelete -> Unit - is ConvoDialog.ConvoPin -> Unit - is ConvoDialog.ConvoUnpin -> Unit - is ConvoDialog.ConvoArchive -> Unit - is ConvoDialog.ConvoUnarchive -> Unit - } - } - - fun onErrorButtonClicked() { - when (baseErrorFlow.value) { - null -> Unit - - is BaseError.ConnectionError, - is BaseError.InternalError, - is BaseError.SimpleError, - is BaseError.UnknownError -> onRefresh() - - else -> Unit - } - } - - fun onPaginationConditionsMet() { - currentOffset.update { convosFlow.value.size } + private fun onPaginationConditionsMet() { + currentOffset = convos.size loadConvos() } - fun onRefresh() { + private fun onErrorConsumed() { + screenState.updateValue { copy(error = null) } + } + + private fun onRefresh() { onErrorConsumed() loadConvos(offset = 0) } - fun onConvoItemClick(convo: UiConvo) { + private fun onConvoItemClick(convoId: Long) { collapseConvos() - navigation.setValue { ConvoNavigation.MessagesHistory(peerId = convo.id) } + navigationIntent.setValue { ConvoNavigationIntent.MessagesHistory(convoId) } } - fun onConvoItemLongClick(convo: UiConvo) { - expandedConvoId.setValue { - if (convo.isExpanded) 0 - else convo.id - } + private fun onConvoItemLongClick(convoId: Long) { + val isExpanded = screenState.value.convos.find { it.id == convoId }?.isExpanded == true + + screenState.updateValue { copy(expandedConvoId = if (isExpanded) 0L else convoId) } syncUiConvos() } - fun onOptionClicked( - convo: UiConvo, - option: ConvoOption - ) { + private fun onOptionClicked(option: ConvoOption) { + val convo = + screenState.value.convos.find { it.id == screenState.value.expandedConvoId } ?: return + when (option) { - ConvoOption.Delete -> { - dialog.setValue { ConvoDialog.ConvoDelete(convo.id) } - } + ConvoOption.Delete -> setDialog(ConvoDialog.Delete) ConvoOption.MarkAsRead -> { - convo.lastMessageId?.let { lastMessageId -> + val lastMessageId = + screenState.value.convos.find { it.id == screenState.value.expandedConvoId }?.lastMessageId + + if (lastMessageId != null) { readConvo( peerId = convo.id, startMessageId = lastMessageId @@ -212,48 +234,39 @@ class ConvosViewModel( } } - ConvoOption.Pin -> { - dialog.setValue { ConvoDialog.ConvoPin(convo.id) } - } - - ConvoOption.Unpin -> { - dialog.setValue { ConvoDialog.ConvoUnpin(convo.id) } - } - - ConvoOption.Archive -> { - dialog.setValue { ConvoDialog.ConvoArchive(convo.id) } - } - - ConvoOption.Unarchive -> { - dialog.setValue { ConvoDialog.ConvoUnarchive(convo.id) } - } + ConvoOption.Pin -> setDialog(ConvoDialog.Pin) + ConvoOption.Unpin -> setDialog(ConvoDialog.Unpin) + ConvoOption.Archive -> setDialog(ConvoDialog.Archive) + ConvoOption.Unarchive -> setDialog(ConvoDialog.Unarchive) } } - fun onErrorConsumed() { - baseError.setValue { null } - } - - fun setScrollIndex(index: Int) { + private fun setScrollIndex(index: Int) { screenState.setValue { old -> old.copy(scrollIndex = index) } } - fun setScrollOffset(offset: Int) { + private fun setScrollOffset(offset: Int) { screenState.setValue { old -> old.copy(scrollOffset = offset) } } - fun onCreateChatButtonClicked() { - navigation.setValue { ConvoNavigation.CreateChat } + private fun setDialog(dialog: ConvoDialog?) { + screenState.updateValue { copy(dialog = dialog) } } - private fun collapseConvos() { - expandedConvoId.setValue { 0 } - syncUiConvos() + private fun replaceConvos(newConvos: List) { + convos.clear() + convos.addAll(newConvos) } - private fun loadConvos( - offset: Int = currentOffsetFlow.value - ) { + private fun collapseConvos(sync: Boolean = true) { + screenState.updateValue { copy(expandedConvoId = null) } + + if (sync) { + syncUiConvos() + } + } + + private fun loadConvos(offset: Int = currentOffset) { convoUseCase.getConvos( count = LOAD_COUNT, offset = offset, @@ -261,21 +274,18 @@ class ConvosViewModel( ).listenValue(viewModelScope) { state -> state.processState( error = { error -> - val newBaseError = VkUtils.parseError(error) - baseError.update { newBaseError } + screenState.updateValue { copy(error = VkUtils.parseError(error)) } }, success = { response -> - val convos = response - val fullConvos = if (offset == 0) { - convos + val newConvos = if (offset == 0) { + response } else { - this.convosFlow.value.plus(convos) + convos.plus(response) } val itemsCountSufficient = response.size == LOAD_COUNT - val paginationExhausted = !itemsCountSufficient && - this.convosFlow.value.isNotEmpty() + val paginationExhausted = !itemsCountSufficient && convos.isNotEmpty() screenState.updateValue { copy(isPaginationExhausted = paginationExhausted) @@ -294,9 +304,10 @@ class ConvosViewModel( convoUseCase.storeConvos(response) - this.convos.emit(fullConvos) + replaceConvos(newConvos) + + screenState.updateValue { copy(canPaginate = itemsCountSufficient) } syncUiConvos() - canPaginate.setValue { itemsCountSufficient } } ) @@ -314,13 +325,13 @@ class ConvosViewModel( state.processState( error = {}, success = { - val newConvos = convosFlow.value.toMutableList() + val newConvos = convos.toMutableList() val convoIndex = newConvos.indexOfFirstOrNull { it.id == peerId } ?: return@processState newConvos.removeAt(convoIndex) - convos.update { newConvos.sorted() } + replaceConvos(newConvos.sorted()) syncUiConvos() } ) @@ -338,7 +349,7 @@ class ConvosViewModel( LongPollParsedEvent.ChatMajorChanged( peerId = peerId, majorId = if (pin) { - pinnedConvosCount.value.plus(1) * 16 + pinnedConvosCount.plus(1) * 16 } else { 0 } @@ -357,7 +368,7 @@ class ConvosViewModel( state.processState( error = {}, success = { - convosFlow.value.find { it.id == peerId }?.let { convo -> + convos.find { it.id == peerId }?.let { convo -> handleChatArchived( LongPollParsedEvent.ChatArchived( convo = convo, @@ -374,7 +385,7 @@ class ConvosViewModel( private fun handleNewMessage(event: LongPollParsedEvent.NewMessage) { val message = event.message - val newConvos = convosFlow.value.toMutableList() + val newConvos = convos.toMutableList() val convoIndex = newConvos.indexOfFirstOrNull { it.id == message.peerId } @@ -392,8 +403,8 @@ class ConvosViewModel( val convo = (response.firstOrNull() ?: return@listenValue) .copy(lastMessage = message) - newConvos.add(pinnedConvosCount.value, convo) - convos.update { newConvos.sorted() } + newConvos.add(pinnedConvosCount, convo) + replaceConvos(newConvos.sorted()) syncUiConvos() } ) @@ -429,19 +440,17 @@ class ConvosViewModel( newConvos[convoIndex] = newConvo } else { newConvos.removeAt(convoIndex) - - val toPosition = pinnedConvosCount.value - newConvos.add(toPosition, newConvo) + newConvos.add(pinnedConvosCount, newConvo) } - convos.update { newConvos.sorted() } + replaceConvos(newConvos.sorted()) syncUiConvos() } } private fun handleEditedMessage(event: LongPollParsedEvent.MessageEdited) { val message = event.message - val newConvos = convosFlow.value.toMutableList() + val newConvos = convos.toMutableList() val convoIndex = newConvos.indexOfFirstOrNull { it.id == message.peerId } if (convoIndex == null) { // диалога нет в списке @@ -453,13 +462,14 @@ class ConvosViewModel( lastMessageId = message.id, lastCmId = message.cmId ) - convos.update { newConvos } + + replaceConvos(newConvos) syncUiConvos() } } private fun handleReadIncomingMessage(event: LongPollParsedEvent.IncomingMessageRead) { - val newConvos = convosFlow.value.toMutableList() + val newConvos = convos.toMutableList() val convoIndex = newConvos.indexOfFirstOrNull { it.id == event.peerId } @@ -473,13 +483,13 @@ class ConvosViewModel( unreadCount = event.unreadCount ) - convos.update { newConvos } + replaceConvos(newConvos) syncUiConvos() } } private fun handleReadOutgoingMessage(event: LongPollParsedEvent.OutgoingMessageRead) { - val newConvos = convosFlow.value.toMutableList() + val newConvos = convos.toMutableList() val convoIndex = newConvos.indexOfFirstOrNull { it.id == event.peerId } @@ -493,7 +503,7 @@ class ConvosViewModel( unreadCount = event.unreadCount ) - convos.update { newConvos } + replaceConvos(newConvos) syncUiConvos() } } @@ -503,7 +513,7 @@ class ConvosViewModel( val peerId = event.peerId val userIds = event.userIds - val newConvos = convosFlow.value.toMutableList() + val newConvos = convos.toMutableList() val convoAndIndex = newConvos.findWithIndex { it.id == peerId } @@ -514,7 +524,7 @@ class ConvosViewModel( interactionIds = userIds ) - convos.update { newConvos } + replaceConvos(newConvos) syncUiConvos() interactionsTimers[peerId]?.let { interactionJob -> @@ -546,7 +556,7 @@ class ConvosViewModel( private fun stopInteraction(peerId: Long, interactionJob: InteractionJob) { interactionsTimers[peerId] ?: return - val newConvos = convosFlow.value.toMutableList() + val newConvos = convos.toMutableList() val convoAndIndex = newConvos.findWithIndex { it.id == peerId } ?: return @@ -556,7 +566,7 @@ class ConvosViewModel( interactionIds = emptyList() ) - convos.update { newConvos } + replaceConvos(newConvos) syncUiConvos() interactionJob.timerJob.cancel() @@ -564,7 +574,7 @@ class ConvosViewModel( } private fun handleChatMajorChanged(event: LongPollParsedEvent.ChatMajorChanged) { - val newConvos = convosFlow.value.toMutableList() + val newConvos = convos.toMutableList() val convoIndex = newConvos.indexOfFirstOrNull { it.id == event.peerId } @@ -574,13 +584,13 @@ class ConvosViewModel( newConvos[convoIndex] = newConvos[convoIndex].copy(majorId = event.majorId) - convos.setValue { newConvos.sorted() } + replaceConvos(newConvos.sorted()) syncUiConvos() } } private fun handleChatMinorChanged(event: LongPollParsedEvent.ChatMinorChanged) { - val newConvos = convosFlow.value.toMutableList() + val newConvos = convos.toMutableList() val convoIndex = newConvos.indexOfFirstOrNull { it.id == event.peerId } @@ -590,13 +600,13 @@ class ConvosViewModel( newConvos[convoIndex] = newConvos[convoIndex].copy(minorId = event.minorId) - convos.setValue { newConvos.sorted() } + replaceConvos(newConvos.sorted()) syncUiConvos() } } private fun handleChatClearing(event: LongPollParsedEvent.ChatCleared) { - val newConvos = convosFlow.value.toMutableList() + val newConvos = convos.toMutableList() val convoIndex = newConvos.indexOfFirstOrNull { it.id == event.peerId } @@ -605,7 +615,7 @@ class ConvosViewModel( } else { newConvos.removeAt(convoIndex) - convos.setValue { newConvos.sorted() } + replaceConvos(newConvos.sorted()) syncUiConvos() } } @@ -613,7 +623,7 @@ class ConvosViewModel( private fun handleChatArchived(event: LongPollParsedEvent.ChatArchived) { val convo = event.convo - val newConvos = convosFlow.value.toMutableList() + val newConvos = convos.toMutableList() when (filter) { ConvosFilter.BUSINESS_NOTIFY -> Unit @@ -628,7 +638,7 @@ class ConvosViewModel( newConvos.removeAt(index) } - convos.update { newConvos } + replaceConvos(newConvos) syncUiConvos() } @@ -639,10 +649,10 @@ class ConvosViewModel( newConvos.removeAt(index) } else { - newConvos.add(pinnedConvosCount.value, convo) + newConvos.add(pinnedConvosCount, convo) } - convos.update { newConvos.sorted() } + replaceConvos(newConvos.sorted()) syncUiConvos() } } @@ -656,7 +666,7 @@ class ConvosViewModel( state.processState( error = {}, success = { - val newConvos = convosFlow.value.toMutableList() + val newConvos = convos.toMutableList() val convoIndex = newConvos.indexOfFirstOrNull { it.id == peerId } ?: return@listenValue @@ -664,7 +674,7 @@ class ConvosViewModel( newConvos[convoIndex] = newConvos[convoIndex].copy(inRead = startMessageId) - convos.update { newConvos } + replaceConvos(newConvos) syncUiConvos() } ) @@ -696,47 +706,44 @@ class ConvosViewModel( } private fun syncUiConvos(): List { - val convos = convosFlow.value - val newUiConvos = convos.map { convo -> - val options = mutableListOf() - convo.lastMessage?.run { - if (!convo.isRead() && !this.isOut) { - options += ConvoOption.MarkAsRead + val options: ImmutableList = buildImmutableList { + if (!convo.isRead() && convo.lastMessage != null && convo.lastMessage?.isOut == false) { + add(ConvoOption.MarkAsRead) } + + if (convo.isPinned()) { + add(ConvoOption.Unpin) + } + + if (convos.size > 4 && pinnedConvosCount < 5 && !convo.isPinned()) { + add(ConvoOption.Pin) + } + + when (filter) { + ConvosFilter.BUSINESS_NOTIFY -> Unit + ConvosFilter.ARCHIVE -> add(ConvoOption.Unarchive) + + ConvosFilter.ALL, + ConvosFilter.UNREAD -> { + if (convo.id != UserConfig.userId) { + add(ConvoOption.Archive) + } + } + } + + add(ConvoOption.Delete) } - val convosSize = this.convosFlow.value.size - val pinnedCount = pinnedConvosCount.value - - val canPinOneMoreDialog = - convosSize > 4 && pinnedCount < 5 && !convo.isPinned() - - if (convo.isPinned()) { - options += ConvoOption.Unpin - } else if (canPinOneMoreDialog) { - options += ConvoOption.Pin - } - - when (filter) { - ConvosFilter.ARCHIVE -> ConvoOption.Unarchive - - ConvosFilter.UNREAD, - ConvosFilter.ALL -> ConvoOption.Archive - - ConvosFilter.BUSINESS_NOTIFY -> null - }?.let(options::add) - - options += ConvoOption.Delete - convo.asPresentation( resources = resources, - useContactName = useContactNames, - isExpanded = expandedConvoId.value == convo.id, - options = options.toImmutableList() + useContactName = userSettings.useContactNames.value, + isExpanded = screenState.value.expandedConvoId == convo.id, + options = options ) } - uiConvos.setValue { newUiConvos } + + screenState.updateValue { copy(convos = newUiConvos.toImmutableList()) } return newUiConvos } diff --git a/feature/convos/src/main/kotlin/dev/meloda/fast/convos/model/ConvoDialog.kt b/feature/convos/src/main/kotlin/dev/meloda/fast/convos/model/ConvoDialog.kt index 7066ed0b..342e0e3d 100644 --- a/feature/convos/src/main/kotlin/dev/meloda/fast/convos/model/ConvoDialog.kt +++ b/feature/convos/src/main/kotlin/dev/meloda/fast/convos/model/ConvoDialog.kt @@ -4,9 +4,9 @@ import androidx.compose.runtime.Immutable @Immutable sealed class ConvoDialog { - data class ConvoPin(val convoId: Long) : ConvoDialog() - data class ConvoUnpin(val convoId: Long) : ConvoDialog() - data class ConvoDelete(val convoId: Long) : ConvoDialog() - data class ConvoArchive(val convoId: Long) : ConvoDialog() - data class ConvoUnarchive(val convoId: Long) : ConvoDialog() + data object Pin : ConvoDialog() + data object Unpin : ConvoDialog() + data object Delete : ConvoDialog() + data object Archive : ConvoDialog() + data object Unarchive : ConvoDialog() } diff --git a/feature/convos/src/main/kotlin/dev/meloda/fast/convos/model/ConvoIntent.kt b/feature/convos/src/main/kotlin/dev/meloda/fast/convos/model/ConvoIntent.kt new file mode 100644 index 00000000..da763742 --- /dev/null +++ b/feature/convos/src/main/kotlin/dev/meloda/fast/convos/model/ConvoIntent.kt @@ -0,0 +1,29 @@ +package dev.meloda.fast.convos.model + +import android.os.Bundle +import dev.meloda.fast.ui.model.vk.ConvoOption + +sealed class ConvoIntent { + data class ItemClick(val convoId: Long) : ConvoIntent() + data class ItemLongClick(val convoId: Long) : ConvoIntent() + data class OptionItemClick(val option: ConvoOption) : ConvoIntent() + data object PaginationConditionsMet : ConvoIntent() + + data object Back : ConvoIntent() + data object Refresh : ConvoIntent() + data object CreateChatClick : ConvoIntent() + data object ArchiveClick : ConvoIntent() + + data class SetScrollIndex(val index: Int) : ConvoIntent() + data class SetScrollOffset(val offset: Int) : ConvoIntent() + + data object ErrorActionButtonClick : ConvoIntent() + + data object ConsumeScrollToTop : ConvoIntent() + + sealed class Dialog : ConvoIntent() { + data object Dismiss : Dialog() + data class Confirm(val bundle: Bundle? = null) : Dialog() + data class Cancel(val bundle: Bundle? = null) : Dialog() + } +} diff --git a/feature/convos/src/main/kotlin/dev/meloda/fast/convos/model/ConvoNavigation.kt b/feature/convos/src/main/kotlin/dev/meloda/fast/convos/model/ConvoNavigation.kt deleted file mode 100644 index b1bf8309..00000000 --- a/feature/convos/src/main/kotlin/dev/meloda/fast/convos/model/ConvoNavigation.kt +++ /dev/null @@ -1,11 +0,0 @@ -package dev.meloda.fast.convos.model - -import androidx.compose.runtime.Immutable - -@Immutable -sealed class ConvoNavigation { - - data class MessagesHistory(val peerId: Long) : ConvoNavigation() - - data object CreateChat : ConvoNavigation() -} diff --git a/feature/convos/src/main/kotlin/dev/meloda/fast/convos/model/ConvoNavigationIntent.kt b/feature/convos/src/main/kotlin/dev/meloda/fast/convos/model/ConvoNavigationIntent.kt new file mode 100644 index 00000000..dde49e8b --- /dev/null +++ b/feature/convos/src/main/kotlin/dev/meloda/fast/convos/model/ConvoNavigationIntent.kt @@ -0,0 +1,9 @@ +package dev.meloda.fast.convos.model + +sealed class ConvoNavigationIntent { + + data object Back : ConvoNavigationIntent() + data class MessagesHistory(val convoId: Long) : ConvoNavigationIntent() + data object CreateChat : ConvoNavigationIntent() + data object Archive : ConvoNavigationIntent() +} diff --git a/feature/convos/src/main/kotlin/dev/meloda/fast/convos/model/ConvosScreenState.kt b/feature/convos/src/main/kotlin/dev/meloda/fast/convos/model/ConvosScreenState.kt index f04ca53d..a411f307 100644 --- a/feature/convos/src/main/kotlin/dev/meloda/fast/convos/model/ConvosScreenState.kt +++ b/feature/convos/src/main/kotlin/dev/meloda/fast/convos/model/ConvosScreenState.kt @@ -1,6 +1,10 @@ package dev.meloda.fast.convos.model import androidx.compose.runtime.Immutable +import dev.meloda.fast.model.BaseError +import dev.meloda.fast.ui.model.vk.UiConvo +import dev.meloda.fast.ui.util.ImmutableList +import dev.meloda.fast.ui.util.emptyImmutableList @Immutable data class ConvosScreenState( @@ -10,7 +14,13 @@ data class ConvosScreenState( val profileImageUrl: String?, val scrollIndex: Int, val scrollOffset: Int, - val isArchive: Boolean + val canPaginate: Boolean, + val expandedConvoId: Long?, + val convos: ImmutableList, + val dialog: ConvoDialog?, + + // TODO: 30.05.2026, Danil Nikolaev: remove + val error: BaseError? ) { companion object { @@ -21,7 +31,11 @@ data class ConvosScreenState( profileImageUrl = null, scrollIndex = 0, scrollOffset = 0, - isArchive = false + canPaginate = false, + expandedConvoId = null, + convos = emptyImmutableList(), + dialog = null, + error = null ) } } diff --git a/feature/convos/src/main/kotlin/dev/meloda/fast/convos/navigation/ConvosNavigation.kt b/feature/convos/src/main/kotlin/dev/meloda/fast/convos/navigation/ConvosNavigation.kt index ccad5b46..0760b340 100644 --- a/feature/convos/src/main/kotlin/dev/meloda/fast/convos/navigation/ConvosNavigation.kt +++ b/feature/convos/src/main/kotlin/dev/meloda/fast/convos/navigation/ConvosNavigation.kt @@ -1,12 +1,16 @@ package dev.meloda.fast.convos.navigation import androidx.appcompat.app.AppCompatActivity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import androidx.navigation.navigation import dev.meloda.fast.convos.ConvosViewModel +import dev.meloda.fast.convos.model.ConvoNavigationIntent import dev.meloda.fast.convos.presentation.ConvosRoute -import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.ConvosFilter import dev.meloda.fast.ui.extensions.getOrThrow import dev.meloda.fast.ui.theme.LocalNavController @@ -24,44 +28,56 @@ object Convos object Archive fun NavGraphBuilder.convosGraph( + handleNavigationIntent: (ConvoNavigationIntent) -> Unit, activity: AppCompatActivity, - onError: (BaseError) -> Unit, - onNavigateToMessagesHistory: (id: Long) -> Unit, - onNavigateToCreateChat: () -> Unit, - onScrolledToTop: () -> Unit ) { navigation( startDestination = Convos ) { - val convosViewModel: ConvosViewModel = with(activity) { - getViewModel(qualifier = named(ConvosFilter.ALL)) - } composable { - val navController = LocalNavController.getOrThrow() - - ConvosRoute( - viewModel = convosViewModel, - onError = onError, - onNavigateToMessagesHistory = onNavigateToMessagesHistory, - onNavigateToCreateChat = onNavigateToCreateChat, - onNavigateToArchive = { navController.navigate(Archive) }, - onScrolledToTop = onScrolledToTop + ConvosRootRoute( + handleNavigationIntent = handleNavigationIntent, + viewModel = with(activity) { + getViewModel(named(ConvosFilter.ALL)) + } ) } composable { - val navController = LocalNavController.getOrThrow() - - ConvosRoute( + ConvosRootRoute( + handleNavigationIntent = handleNavigationIntent, viewModel = with(activity) { - getViewModel( - qualifier = named(ConvosFilter.ARCHIVE) - ) - }, - onBack = navController::navigateUp, - onError = onError, - onNavigateToMessagesHistory = onNavigateToMessagesHistory, - onScrolledToTop = onScrolledToTop + getViewModel(named(ConvosFilter.ARCHIVE)) + } ) } } } + +@Composable +private fun ConvosRootRoute( + handleNavigationIntent: (ConvoNavigationIntent) -> Unit, + viewModel: ConvosViewModel +) { + val navController = LocalNavController.getOrThrow() + + val screenState by viewModel.screenStateFlow.collectAsStateWithLifecycle() + val navigationIntent by viewModel.navigationIntentFlow.collectAsStateWithLifecycle() + + LaunchedEffect(navigationIntent) { + navigationIntent?.let { + when (navigationIntent) { + ConvoNavigationIntent.Back -> navController.navigateUp() + ConvoNavigationIntent.Archive -> navController.navigate(Archive) + else -> handleNavigationIntent(it) + } + + viewModel.onNavigationConsumed() + } + } + + ConvosRoute( + handleIntent = viewModel::handleIntent, + screenState = screenState, + isArchive = viewModel.filter == ConvosFilter.ARCHIVE, + ) +} diff --git a/feature/convos/src/main/kotlin/dev/meloda/fast/convos/presentation/ConvoDialogs.kt b/feature/convos/src/main/kotlin/dev/meloda/fast/convos/presentation/ConvoDialogs.kt index ecaf7fe1..684e7f0b 100644 --- a/feature/convos/src/main/kotlin/dev/meloda/fast/convos/presentation/ConvoDialogs.kt +++ b/feature/convos/src/main/kotlin/dev/meloda/fast/convos/presentation/ConvoDialogs.kt @@ -1,82 +1,78 @@ package dev.meloda.fast.convos.presentation -import android.os.Bundle import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource -import androidx.core.os.bundleOf import dev.meloda.fast.convos.model.ConvoDialog +import dev.meloda.fast.convos.model.ConvoIntent import dev.meloda.fast.convos.model.ConvosScreenState import dev.meloda.fast.ui.R import dev.meloda.fast.ui.components.MaterialDialog @Composable fun HandleDialogs( + handleIntent: (ConvoIntent) -> Unit, screenState: ConvosScreenState, - dialog: ConvoDialog?, - onConfirmed: (ConvoDialog, Bundle) -> Unit = { _, _ -> }, - onDismissed: (ConvoDialog) -> Unit = {}, - onItemPicked: (ConvoDialog, Bundle) -> Unit = { _, _ -> } ) { - when (dialog) { + when (screenState.dialog) { null -> Unit - is ConvoDialog.ConvoArchive -> { + is ConvoDialog.Archive -> { MaterialDialog( - onDismissRequest = { onDismissed(dialog) }, + onDismissRequest = { handleIntent(ConvoIntent.Dialog.Dismiss) }, title = stringResource(id = R.string.confirm_archive_convo), - confirmAction = { onConfirmed(dialog, bundleOf()) }, + confirmAction = { handleIntent(ConvoIntent.Dialog.Confirm()) }, confirmText = stringResource(id = R.string.action_archive), cancelText = stringResource(id = R.string.cancel), icon = ImageVector.vectorResource(R.drawable.ic_archive_fill_round_24) ) } - is ConvoDialog.ConvoUnarchive -> { + is ConvoDialog.Unarchive -> { MaterialDialog( - onDismissRequest = { onDismissed(dialog) }, + onDismissRequest = { handleIntent(ConvoIntent.Dialog.Dismiss) }, title = stringResource(id = R.string.confirm_unarchive_convo), - confirmAction = { onConfirmed(dialog, bundleOf()) }, + confirmAction = { handleIntent(ConvoIntent.Dialog.Confirm()) }, confirmText = stringResource(id = R.string.action_unarchive), cancelText = stringResource(id = R.string.cancel), icon = ImageVector.vectorResource(R.drawable.ic_unarchive_fill_round_24) ) } - is ConvoDialog.ConvoDelete -> { + is ConvoDialog.Delete -> { val errorColor = MaterialTheme.colorScheme.error MaterialDialog( - onDismissRequest = { onDismissed(dialog) }, + onDismissRequest = { handleIntent(ConvoIntent.Dialog.Dismiss) }, icon = ImageVector.vectorResource(R.drawable.ic_delete_fill_round_24), iconTint = errorColor, title = stringResource(id = R.string.confirm_delete_convo), - confirmAction = { onConfirmed(dialog, bundleOf()) }, + confirmAction = { handleIntent(ConvoIntent.Dialog.Confirm()) }, confirmText = stringResource(id = R.string.action_delete), confirmContainerColor = errorColor, cancelText = stringResource(id = R.string.cancel), ) } - is ConvoDialog.ConvoPin -> { + is ConvoDialog.Pin -> { MaterialDialog( - onDismissRequest = { onDismissed(dialog) }, + onDismissRequest = { handleIntent(ConvoIntent.Dialog.Dismiss) }, icon = ImageVector.vectorResource(R.drawable.ic_keep_fill_round_24), title = stringResource(id = R.string.confirm_pin_convo), - confirmAction = { onConfirmed(dialog, bundleOf()) }, + confirmAction = { handleIntent(ConvoIntent.Dialog.Confirm()) }, confirmText = stringResource(id = R.string.action_pin), cancelText = stringResource(id = R.string.cancel), ) } - is ConvoDialog.ConvoUnpin -> { + is ConvoDialog.Unpin -> { MaterialDialog( - onDismissRequest = { onDismissed(dialog) }, + onDismissRequest = { handleIntent(ConvoIntent.Dialog.Dismiss) }, icon = ImageVector.vectorResource(R.drawable.ic_keep_off_fill_round_24), title = stringResource(id = R.string.confirm_unpin_convo), - confirmAction = { onConfirmed(dialog, bundleOf()) }, + confirmAction = { handleIntent(ConvoIntent.Dialog.Confirm()) }, confirmText = stringResource(id = R.string.action_unpin), cancelText = stringResource(id = R.string.cancel), ) diff --git a/feature/convos/src/main/kotlin/dev/meloda/fast/convos/presentation/ConvoItem.kt b/feature/convos/src/main/kotlin/dev/meloda/fast/convos/presentation/ConvoItem.kt index 587edce3..4ef86313 100644 --- a/feature/convos/src/main/kotlin/dev/meloda/fast/convos/presentation/ConvoItem.kt +++ b/feature/convos/src/main/kotlin/dev/meloda/fast/convos/presentation/ConvoItem.kt @@ -62,9 +62,9 @@ val BirthdayColor = Color(0xffb00b69) @OptIn(ExperimentalFoundationApi::class) @Composable fun ConvoItem( - onItemClick: (UiConvo) -> Unit, - onItemLongClick: (convo: UiConvo) -> Unit, - onOptionClicked: (UiConvo, ConvoOption) -> Unit, + onItemClick: (convoId: Long) -> Unit, + onItemLongClick: (convoId: Long) -> Unit, + onOptionClicked: (ConvoOption) -> Unit, maxLines: Int, isUserAccount: Boolean, convo: UiConvo, @@ -81,9 +81,9 @@ fun ConvoItem( modifier = modifier .fillMaxWidth() .combinedClickable( - onClick = { onItemClick(convo) }, + onClick = { onItemClick(convo.id) }, onLongClick = { - onItemLongClick(convo) + onItemLongClick(convo.id) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) } ) @@ -281,7 +281,7 @@ fun ConvoItem( val builder = AnnotatedString.Builder(convo.message.text) - convo.message.spanStyles.map { spanStyleRange -> + convo.message.spanStyles.forEach { spanStyleRange -> val updatedSpanStyle = if (spanStyleRange.item.color == Color.Red) { spanStyleRange.item.copy(color = MaterialTheme.colorScheme.primary) @@ -378,7 +378,7 @@ fun ConvoItem( } ElevatedAssistChip( - onClick = { onOptionClicked(convo, option) }, + onClick = { onOptionClicked(option) }, leadingIcon = { option.icon.getResourcePainter()?.let { painter -> Icon( diff --git a/feature/convos/src/main/kotlin/dev/meloda/fast/convos/presentation/ConvosList.kt b/feature/convos/src/main/kotlin/dev/meloda/fast/convos/presentation/ConvosList.kt index 35f65c0c..50e229e9 100644 --- a/feature/convos/src/main/kotlin/dev/meloda/fast/convos/presentation/ConvosList.kt +++ b/feature/convos/src/main/kotlin/dev/meloda/fast/convos/presentation/ConvosList.kt @@ -36,12 +36,12 @@ import kotlinx.coroutines.launch fun ConvosList( modifier: Modifier = Modifier, convos: ImmutableList, - onConvosClick: (UiConvo) -> Unit, - onConvosLongClick: (UiConvo) -> Unit, + onConvosClick: (Long) -> Unit, + onConvosLongClick: (Long) -> Unit, screenState: ConvosScreenState, state: LazyListState, maxLines: Int, - onOptionClicked: (UiConvo, ConvoOption) -> Unit, + onOptionClicked: (ConvoOption) -> Unit, padding: PaddingValues ) { val theme = LocalThemeConfig.current diff --git a/feature/convos/src/main/kotlin/dev/meloda/fast/convos/presentation/ConvosRoute.kt b/feature/convos/src/main/kotlin/dev/meloda/fast/convos/presentation/ConvosRoute.kt index f558fd1e..83a7c749 100644 --- a/feature/convos/src/main/kotlin/dev/meloda/fast/convos/presentation/ConvosRoute.kt +++ b/feature/convos/src/main/kotlin/dev/meloda/fast/convos/presentation/ConvosRoute.kt @@ -1,79 +1,23 @@ package dev.meloda.fast.convos.presentation import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import dev.meloda.fast.convos.ConvosViewModel -import dev.meloda.fast.convos.model.ConvoNavigation -import dev.meloda.fast.model.BaseError -import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList +import dev.meloda.fast.convos.model.ConvoIntent +import dev.meloda.fast.convos.model.ConvosScreenState @Composable fun ConvosRoute( - viewModel: ConvosViewModel, - onBack: (() -> Unit)? = null, - onError: (BaseError) -> Unit, - onNavigateToMessagesHistory: (convoId: Long) -> Unit, - onNavigateToCreateChat: (() -> Unit)? = null, - onNavigateToArchive: (() -> Unit)? = null, - onScrolledToTop: () -> Unit, + handleIntent: (ConvoIntent) -> Unit, + screenState: ConvosScreenState, + isArchive: Boolean, ) { - val screenState by viewModel.screenStateFlow.collectAsStateWithLifecycle() - val navigationEvent by viewModel.navigationFlow.collectAsStateWithLifecycle() - val convos by viewModel.uiConvosFlow.collectAsStateWithLifecycle() - val dialog by viewModel.dialogFlow.collectAsStateWithLifecycle() - val baseError by viewModel.baseErrorFlow.collectAsStateWithLifecycle() - val canPaginate by viewModel.canPaginateFlow.collectAsStateWithLifecycle() - - LaunchedEffect(navigationEvent) { - val shouldBeConsumed: Boolean = when (val navigation = navigationEvent) { - null -> false - - is ConvoNavigation.CreateChat -> { - onNavigateToCreateChat?.invoke() - true - } - - is ConvoNavigation.MessagesHistory -> { - onNavigateToMessagesHistory(navigation.peerId) - true - } - } - - if (shouldBeConsumed) viewModel.onNavigationConsumed() - } - ConvosScreen( - onBack = { onBack?.invoke() }, + handleIntent = handleIntent, screenState = screenState, - convos = convos.toImmutableList(), - baseError = baseError, - canPaginate = canPaginate, - onConvoItemClicked = viewModel::onConvoItemClick, - onConvoItemLongClicked = viewModel::onConvoItemLongClick, - onOptionClicked = viewModel::onOptionClicked, - onPaginationConditionsMet = viewModel::onPaginationConditionsMet, - onRefresh = viewModel::onRefresh, - onCreateChatButtonClicked = viewModel::onCreateChatButtonClicked, - onArchiveActionClicked = { onNavigateToArchive?.invoke() }, - setScrollIndex = viewModel::setScrollIndex, - setScrollOffset = viewModel::setScrollOffset, - onConsumeReselection = onScrolledToTop, - onErrorViewButtonClicked = { - if (baseError in listOf(BaseError.AccountBlocked, BaseError.SessionExpired)) { - onError(requireNotNull(baseError)) - } else { - viewModel.onErrorButtonClicked() - } - } + isArchive = isArchive, ) HandleDialogs( + handleIntent = handleIntent, screenState = screenState, - dialog = dialog, - onConfirmed = viewModel::onDialogConfirmed, - onDismissed = viewModel::onDialogDismissed, - onItemPicked = viewModel::onDialogItemPicked ) } diff --git a/feature/convos/src/main/kotlin/dev/meloda/fast/convos/presentation/ConvosScreen.kt b/feature/convos/src/main/kotlin/dev/meloda/fast/convos/presentation/ConvosScreen.kt index 8709ff79..f0575bb4 100644 --- a/feature/convos/src/main/kotlin/dev/meloda/fast/convos/presentation/ConvosScreen.kt +++ b/feature/convos/src/main/kotlin/dev/meloda/fast/convos/presentation/ConvosScreen.kt @@ -56,25 +56,20 @@ import dev.chrisbanes.haze.hazeEffect import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.HazeMaterials +import dev.meloda.fast.convos.model.ConvoIntent import dev.meloda.fast.convos.model.ConvosScreenState import dev.meloda.fast.convos.navigation.ConvoGraph import dev.meloda.fast.datastore.AppSettings -import dev.meloda.fast.model.BaseError import dev.meloda.fast.ui.R import dev.meloda.fast.ui.components.FullScreenContainedLoader import dev.meloda.fast.ui.components.NoItemsView import dev.meloda.fast.ui.components.SegmentedButtonItem import dev.meloda.fast.ui.components.SegmentedButtonsRow -import dev.meloda.fast.ui.components.VkErrorView -import dev.meloda.fast.ui.model.vk.ConvoOption -import dev.meloda.fast.ui.model.vk.UiConvo import dev.meloda.fast.ui.theme.LocalBottomPadding import dev.meloda.fast.ui.theme.LocalHazeState import dev.meloda.fast.ui.theme.LocalReselectedTab import dev.meloda.fast.ui.theme.LocalThemeConfig -import dev.meloda.fast.ui.util.ImmutableList import dev.meloda.fast.ui.util.buildImmutableList -import dev.meloda.fast.ui.util.emptyImmutableList import dev.meloda.fast.ui.util.isScrollingUp import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce @@ -82,26 +77,14 @@ import kotlin.time.Duration.Companion.milliseconds @OptIn( ExperimentalMaterial3Api::class, - ExperimentalHazeMaterialsApi::class, ExperimentalMaterial3ExpressiveApi::class, + ExperimentalHazeMaterialsApi::class, + ExperimentalMaterial3ExpressiveApi::class, ) @Composable fun ConvosScreen( - screenState: ConvosScreenState = ConvosScreenState.EMPTY, - convos: ImmutableList = emptyImmutableList(), - baseError: BaseError? = null, - canPaginate: Boolean = false, - onBack: () -> Unit = {}, - onConvoItemClicked: (convo: UiConvo) -> Unit = {}, - onConvoItemLongClicked: (convo: UiConvo) -> Unit = {}, - onOptionClicked: (UiConvo, ConvoOption) -> Unit = { _, _ -> }, - onPaginationConditionsMet: () -> Unit = {}, - onRefresh: () -> Unit = {}, - onCreateChatButtonClicked: () -> Unit = {}, - onArchiveActionClicked: () -> Unit = {}, - setScrollIndex: (Int) -> Unit = {}, - setScrollOffset: (Int) -> Unit = {}, - onConsumeReselection: () -> Unit = {}, - onErrorViewButtonClicked: () -> Unit = {} + handleIntent: (ConvoIntent) -> Unit, + screenState: ConvosScreenState, + isArchive: Boolean, ) { val currentTheme = LocalThemeConfig.current val maxLines = if (currentTheme.enableMultiline) 2 else 1 @@ -114,14 +97,14 @@ fun ConvosScreen( val currentTabReselected = LocalReselectedTab.current[ConvoGraph] == true LaunchedEffect(currentTabReselected) { if (currentTabReselected) { - if (screenState.isArchive) { - onBack.invoke() + if (isArchive) { + handleIntent(ConvoIntent.Back) } else { if (listState.firstVisibleItemIndex > 14) { listState.scrollToItem(14) } listState.animateScrollToItem(0) - onConsumeReselection() + handleIntent(ConvoIntent.ConsumeScrollToTop) } } } @@ -129,18 +112,18 @@ fun ConvosScreen( LaunchedEffect(listState) { snapshotFlow { listState.firstVisibleItemIndex } .debounce(500L.milliseconds) - .collectLatest(setScrollIndex) + .collectLatest { handleIntent(ConvoIntent.SetScrollIndex(it)) } } LaunchedEffect(listState) { snapshotFlow { listState.firstVisibleItemScrollOffset } .debounce(500L.milliseconds) - .collectLatest(setScrollOffset) + .collectLatest { handleIntent(ConvoIntent.SetScrollOffset(it)) } } - val paginationConditionMet by remember(canPaginate, listState) { + val paginationConditionMet by remember(screenState.canPaginate, listState) { derivedStateOf { - canPaginate && + screenState.canPaginate && (listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -9) >= (listState.layoutInfo.totalItemsCount - 6) } @@ -148,7 +131,7 @@ fun ConvosScreen( LaunchedEffect(paginationConditionMet) { if (paginationConditionMet && !screenState.isPaginating) { - onPaginationConditionsMet() + handleIntent(ConvoIntent.PaginationConditionsMet) } } @@ -183,7 +166,7 @@ fun ConvosScreen( text = stringResource( id = when { screenState.isLoading -> R.string.title_loading - screenState.isArchive -> R.string.title_archive + isArchive -> R.string.title_archive else -> R.string.title_convos } ), @@ -193,8 +176,8 @@ fun ConvosScreen( ) }, navigationIcon = { - if (screenState.isArchive) { - IconButton(onClick = onBack) { + if (isArchive) { + IconButton(onClick = { handleIntent(ConvoIntent.Back) }) { Icon( painter = painterResource(R.drawable.ic_arrow_back_round_24), contentDescription = null @@ -206,7 +189,7 @@ fun ConvosScreen( val dropDownItems: List<@Composable () -> Unit> = buildList {} val items = buildImmutableList { - if (!screenState.isArchive) { + if (!isArchive) { add(SegmentedButtonItem("archive", R.drawable.ic_archive_round_24)) } @@ -225,8 +208,8 @@ fun ConvosScreen( items = items, onClick = { index -> when (items[index].key) { - "archive" -> onArchiveActionClicked() - "refresh" -> onRefresh() + "archive" -> handleIntent(ConvoIntent.ArchiveClick) + "refresh" -> handleIntent(ConvoIntent.Refresh) "more" -> dropDownMenuExpanded = true else -> Unit @@ -263,7 +246,7 @@ fun ConvosScreen( ) val showHorizontalProgressBar by remember(screenState) { - derivedStateOf { screenState.isLoading && convos.isNotEmpty() } + derivedStateOf { screenState.isLoading && screenState.convos.isNotEmpty() } } AnimatedVisibility(showHorizontalProgressBar) { LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) @@ -274,14 +257,14 @@ fun ConvosScreen( } }, floatingActionButton = { - if (!screenState.isArchive) { + if (!isArchive) { val offsetY by animateIntAsState( targetValue = if (listState.isScrollingUp()) 0 else 600 ) Column { FloatingActionButton( - onClick = onCreateChatButtonClicked, + onClick = { handleIntent(ConvoIntent.CreateChatClick) }, modifier = Modifier.offset { IntOffset(0, offsetY) } @@ -298,14 +281,15 @@ fun ConvosScreen( } ) { padding -> when { - baseError != null -> { - VkErrorView( - baseError = baseError, - onButtonClick = onErrorViewButtonClicked - ) - } + // TODO: 30.05.2026, Danil Nikolaev: move to UI State +// baseError != null -> { +// VkErrorView( +// baseError = baseError, +// onButtonClick = onErrorViewButtonClicked +// ) +// } - screenState.isLoading && convos.isEmpty() -> FullScreenContainedLoader() + screenState.isLoading && screenState.convos.isEmpty() -> FullScreenContainedLoader() else -> { val pullToRefreshState = rememberPullToRefreshState() @@ -318,7 +302,7 @@ fun ConvosScreen( .padding(bottom = padding.calculateBottomPadding()), state = pullToRefreshState, isRefreshing = screenState.isLoading, - onRefresh = onRefresh, + onRefresh = { handleIntent(ConvoIntent.Refresh) }, indicator = { PullToRefreshDefaults.Indicator( state = pullToRefreshState, @@ -330,9 +314,9 @@ fun ConvosScreen( } ) { ConvosList( - convos = convos, - onConvosClick = onConvoItemClicked, - onConvosLongClick = onConvoItemLongClicked, + convos = screenState.convos, + onConvosClick = { handleIntent(ConvoIntent.ItemClick(it)) }, + onConvosLongClick = { handleIntent(ConvoIntent.ItemLongClick(it)) }, screenState = screenState, state = listState, maxLines = maxLines, @@ -341,14 +325,14 @@ fun ConvosScreen( } else { Modifier }.fillMaxSize(), - onOptionClicked = onOptionClicked, + onOptionClicked = { handleIntent(ConvoIntent.OptionItemClick(it)) }, padding = padding ) - if (convos.isEmpty()) { + if (screenState.convos.isEmpty()) { NoItemsView( buttonText = stringResource(R.string.action_refresh), - onButtonClick = onRefresh + onButtonClick = { handleIntent(ConvoIntent.Refresh) } ) } }