diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/ChatListComponent.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/ChatListComponent.kt index dd059a11..f8885580 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/ChatListComponent.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/ChatListComponent.kt @@ -7,6 +7,7 @@ import org.monogram.domain.repository.ConnectionStatus import org.monogram.presentation.core.util.AppPreferences interface ChatListComponent { + val state: StateFlow val uiState: StateFlow val foldersState: StateFlow val chatsState: StateFlow @@ -58,6 +59,42 @@ interface ChatListComponent { fun updateScrollPosition(folderId: Int, index: Int, offset: Int) + data class State( + val chatsByFolder: Map> = emptyMap(), + val folders: List = emptyList(), + val selectedFolderId: Int = -1, + val currentUser: UserModel? = null, + val isLoadingByFolder: Map = emptyMap(), + val selectedChatIds: Set = emptySet(), + val activeChatId: Long? = null, + val isSearchActive: Boolean = false, + val searchQuery: String = "", + val searchResults: List = emptyList(), + val globalSearchResults: List = emptyList(), + val messageSearchResults: List = emptyList(), + val recentUsers: List = emptyList(), + val recentOthers: List = emptyList(), + val connectionStatus: ConnectionStatus = ConnectionStatus.Connected, + val isArchivePinned: Boolean = true, + val isArchiveAlwaysVisible: Boolean = false, + val isForwarding: Boolean = false, + val canLoadMoreMessages: Boolean = false, + val instantViewUrl: String? = null, + val isProxyEnabled: Boolean = false, + val attachMenuBots: List = emptyList(), + val botWebAppUrl: String? = null, + val botWebAppName: String? = null, + val webAppUrl: String? = null, + val webAppBotId: Long? = null, + val webAppBotName: String? = null, + val webViewUrl: String? = null, + val updateState: UpdateState = UpdateState.Idle, + val scrollPositions: Map> = emptyMap() + ) { + val chats: List get() = chatsByFolder[selectedFolderId].orEmpty() + val isLoading: Boolean get() = isLoadingByFolder[selectedFolderId] ?: false + } + @Immutable data class UiState( val currentUser: UserModel? = null, diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/ChatListStore.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/ChatListStore.kt new file mode 100644 index 00000000..7a15de0b --- /dev/null +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/ChatListStore.kt @@ -0,0 +1,55 @@ +package org.monogram.presentation.features.chats + +import com.arkivanov.mvikotlin.core.store.Store + +interface ChatListStore : Store { + + sealed class Intent { + data class ChatClicked(val id: Long) : Intent() + data class ProfileClicked(val id: Long) : Intent() + data class MessageClicked(val chatId: Long, val messageId: Long) : Intent() + object SettingsClicked : Intent() + data class FolderClicked(val id: Int) : Intent() + data class LoadMore(val folderId: Int? = null) : Intent() + object LoadMoreMessages : Intent() + data class ChatLongClicked(val id: Long) : Intent() + object ClearSelection : Intent() + object RetryConnection : Intent() + object SearchToggle : Intent() + data class SearchQueryChange(val query: String) : Intent() + data class SetEmojiStatus(val customEmojiId: Long, val statusPath: String?) : Intent() + object ClearSearchHistory : Intent() + data class RemoveSearchHistoryItem(val chatId: Long) : Intent() + data class MuteSelected(val mute: Boolean) : Intent() + data class ArchiveSelected(val archive: Boolean) : Intent() + object PinSelected : Intent() + object ToggleReadSelected : Intent() + object DeleteSelected : Intent() + object ArchivePinToggle : Intent() + object ConfirmForwarding : Intent() + object NewChatClicked : Intent() + object ProxySettingsClicked : Intent() + object EditFoldersClicked : Intent() + data class DeleteFolder(val folderId: Int) : Intent() + data class EditFolder(val folderId: Int) : Intent() + data class OpenInstantView(val url: String) : Intent() + object DismissInstantView : Intent() + data class OpenWebApp(val url: String, val botUserId: Long, val botName: String) : Intent() + object DismissWebApp : Intent() + data class OpenWebView(val url: String) : Intent() + object DismissWebView : Intent() + object UpdateClicked : Intent() + data class UpdateScrollPosition(val folderId: Int, val index: Int, val offset: Int) : Intent() + data class UpdateState(val state: ChatListComponent.State) : Intent() + } + + sealed class Label { + data class OpenChat(val chatId: Long, val messageId: Long?) : Label() + data class OpenProfile(val id: Long) : Label() + object OpenSettings : Label() + object OpenProxySettings : Label() + object OpenNewChat : Label() + data class ConfirmForward(val selectedChatIds: Set) : Label() + data class EditFolders(val folderId: Int? = null) : Label() + } +} diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/ChatListStoreFactory.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/ChatListStoreFactory.kt new file mode 100644 index 00000000..746b892b --- /dev/null +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/ChatListStoreFactory.kt @@ -0,0 +1,82 @@ +package org.monogram.presentation.features.chats + +import com.arkivanov.mvikotlin.core.store.Reducer +import com.arkivanov.mvikotlin.core.store.Store +import com.arkivanov.mvikotlin.core.store.StoreFactory +import com.arkivanov.mvikotlin.extensions.coroutines.CoroutineExecutor +import org.monogram.presentation.features.chats.ChatListStore.Intent +import org.monogram.presentation.features.chats.ChatListStore.Label +import org.monogram.presentation.features.chats.chatList.DefaultChatListComponent + +class ChatListStoreFactory( + private val storeFactory: StoreFactory, + private val component: DefaultChatListComponent +) { + + fun create(): ChatListStore = + object : ChatListStore, Store by storeFactory.create( + name = "ChatListStore", + initialState = component._state.value, + executorFactory = ::ExecutorImpl, + reducer = ReducerImpl + ) {} + + private inner class ExecutorImpl : CoroutineExecutor() { + override fun executeIntent(intent: Intent) { + when (intent) { + is Intent.ChatClicked -> component.handleChatClicked(intent.id)?.let(::publish) + is Intent.ProfileClicked -> publish(Label.OpenProfile(intent.id)) + is Intent.MessageClicked -> component.handleMessageClicked(intent.chatId, intent.messageId)?.let(::publish) + Intent.SettingsClicked -> publish(Label.OpenSettings) + is Intent.FolderClicked -> component.handleFolderClicked(intent.id) + is Intent.LoadMore -> component.handleLoadMore(intent.folderId) + Intent.LoadMoreMessages -> component.handleLoadMoreMessages() + is Intent.ChatLongClicked -> component.handleChatLongClicked(intent.id) + Intent.ClearSelection -> component.handleClearSelection() + Intent.RetryConnection -> component.handleRetryConnection() + Intent.SearchToggle -> component.handleSearchToggle() + is Intent.SearchQueryChange -> component.handleSearchQueryChange(intent.query) + is Intent.SetEmojiStatus -> component.handleSetEmojiStatus(intent.customEmojiId, intent.statusPath) + Intent.ClearSearchHistory -> component.handleClearSearchHistory() + is Intent.RemoveSearchHistoryItem -> component.handleRemoveSearchHistoryItem(intent.chatId) + is Intent.MuteSelected -> component.handleMuteSelected(intent.mute) + is Intent.ArchiveSelected -> component.handleArchiveSelected(intent.archive) + Intent.PinSelected -> component.handlePinSelected() + Intent.ToggleReadSelected -> component.handleToggleReadSelected() + Intent.DeleteSelected -> component.handleDeleteSelected() + Intent.ArchivePinToggle -> component.handleArchivePinToggle() + Intent.ConfirmForwarding -> component.handleConfirmForwarding()?.let(::publish) + Intent.NewChatClicked -> publish(Label.OpenNewChat) + Intent.ProxySettingsClicked -> publish(Label.OpenProxySettings) + Intent.EditFoldersClicked -> publish(Label.EditFolders()) + is Intent.DeleteFolder -> component.handleDeleteFolder(intent.folderId) + is Intent.EditFolder -> component.handleEditFolder(intent.folderId)?.let(::publish) + is Intent.OpenInstantView -> component.handleOpenInstantView(intent.url) + Intent.DismissInstantView -> component.handleDismissInstantView() + is Intent.OpenWebApp -> component.handleOpenWebApp(intent.url, intent.botUserId, intent.botName) + Intent.DismissWebApp -> component.handleDismissWebApp() + is Intent.OpenWebView -> component.handleOpenWebView(intent.url) + Intent.DismissWebView -> component.handleDismissWebView() + Intent.UpdateClicked -> component.handleUpdateClicked()?.let(::publish) + is Intent.UpdateScrollPosition -> component.handleUpdateScrollPosition( + intent.folderId, + intent.index, + intent.offset + ) + + is Intent.UpdateState -> dispatch(Message.UpdateState(intent.state)) + } + } + } + + private object ReducerImpl : Reducer { + override fun ChatListComponent.State.reduce(msg: Message): ChatListComponent.State = + when (msg) { + is Message.UpdateState -> msg.state + } + } + + sealed class Message { + data class UpdateState(val state: ChatListComponent.State) : Message() + } +} diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/DefaultChatListComponent.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/DefaultChatListComponent.kt index c2e63947..670eabb3 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/DefaultChatListComponent.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/DefaultChatListComponent.kt @@ -2,6 +2,11 @@ package org.monogram.presentation.features.chats.chatList import android.util.Log import com.arkivanov.decompose.value.Value +import com.arkivanov.mvikotlin.core.instancekeeper.getStore +import com.arkivanov.mvikotlin.extensions.coroutines.labels +import com.arkivanov.mvikotlin.extensions.coroutines.stateFlow +import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -15,6 +20,8 @@ import org.monogram.presentation.core.util.AppPreferences import org.monogram.presentation.core.util.coRunCatching import org.monogram.presentation.core.util.componentScope import org.monogram.presentation.features.chats.ChatListComponent +import org.monogram.presentation.features.chats.ChatListStore +import org.monogram.presentation.features.chats.ChatListStoreFactory import org.monogram.presentation.root.AppComponentContext class DefaultChatListComponent( @@ -41,15 +48,30 @@ class DefaultChatListComponent( private val updateRepository: UpdateRepository = container.repositories.updateRepository override val appPreferences: AppPreferences = container.preferences.appPreferences - private val _uiState = MutableStateFlow(ChatListComponent.UiState(isForwarding = isForwarding)) - private val _foldersState = MutableStateFlow(ChatListComponent.FoldersState(isLoadingByFolder = mapOf(-1 to true))) - private val _chatsState = MutableStateFlow(ChatListComponent.ChatsState()) - private val _selectionState = MutableStateFlow(ChatListComponent.SelectionState()) - private val _searchState = MutableStateFlow(ChatListComponent.SearchState()) - + internal val _state = MutableStateFlow( + ChatListComponent.State( + isForwarding = isForwarding, + isLoadingByFolder = mapOf(-1 to true) + ) + ) + private val _uiState = MutableStateFlow(_state.value.toUiState()) + private val _foldersState = MutableStateFlow(_state.value.toFoldersState()) + private val _chatsState = MutableStateFlow(_state.value.toChatsState()) + private val _selectionState = MutableStateFlow(_state.value.toSelectionState()) + private val _searchState = MutableStateFlow(_state.value.toSearchState()) + + private val store = instanceKeeper.getStore { + ChatListStoreFactory( + storeFactory = DefaultStoreFactory(), + component = this + ).create() + } + + @OptIn(ExperimentalCoroutinesApi::class) + override val state: StateFlow = store.stateFlow override val uiState: StateFlow = _uiState.asStateFlow() override val foldersState: StateFlow = _foldersState.asStateFlow() - override val chatsState: StateFlow = _chatsState + override val chatsState: StateFlow = _chatsState.asStateFlow() override val selectionState: StateFlow = _selectionState.asStateFlow() override val searchState: StateFlow = _searchState.asStateFlow() @@ -58,15 +80,62 @@ class DefaultChatListComponent( private var isFetchingMoreMessages = false private var nextMessagesOffset = "" + private fun ChatListComponent.State.toUiState() = ChatListComponent.UiState( + currentUser = currentUser, + connectionStatus = connectionStatus, + isArchivePinned = isArchivePinned, + isArchiveAlwaysVisible = isArchiveAlwaysVisible, + isForwarding = isForwarding, + instantViewUrl = instantViewUrl, + isProxyEnabled = isProxyEnabled, + attachMenuBots = attachMenuBots, + botWebAppUrl = botWebAppUrl, + botWebAppName = botWebAppName, + webAppUrl = webAppUrl, + webAppBotId = webAppBotId, + webAppBotName = webAppBotName, + webViewUrl = webViewUrl, + updateState = updateState + ) + + private fun ChatListComponent.State.toFoldersState() = ChatListComponent.FoldersState( + chatsByFolder = chatsByFolder, + folders = folders, + selectedFolderId = selectedFolderId, + isLoadingByFolder = isLoadingByFolder, + scrollPositions = scrollPositions + ) + + private fun ChatListComponent.State.toChatsState() = ChatListComponent.ChatsState( + chats = chats, + isLoading = isLoading + ) + + private fun ChatListComponent.State.toSelectionState() = ChatListComponent.SelectionState( + selectedChatIds = selectedChatIds, + activeChatId = activeChatId + ) + + private fun ChatListComponent.State.toSearchState() = ChatListComponent.SearchState( + isSearchActive = isSearchActive, + searchQuery = searchQuery, + searchResults = searchResults, + globalSearchResults = globalSearchResults, + messageSearchResults = messageSearchResults, + recentUsers = recentUsers, + recentOthers = recentOthers, + canLoadMoreMessages = canLoadMoreMessages + ) + init { activeChatId.subscribe { id -> - _selectionState.update { it.copy(activeChatId = id) } + _state.update { it.copy(activeChatId = id) } } repositoryUser.currentUserFlow .onEach { user -> if (user != null) { - _uiState.update { it.copy(currentUser = user) } + _state.update { it.copy(currentUser = user) } } } .launchIn(scope) @@ -87,68 +156,57 @@ class DefaultChatListComponent( }" ) } - _foldersState.update { + _state.update { val newChatsByFolder = it.chatsByFolder.toMutableMap() newChatsByFolder[update.folderId] = distinctList it.copy(chatsByFolder = newChatsByFolder) } - - if (update.folderId == _foldersState.value.selectedFolderId) { - _chatsState.update { - it.copy(chats = distinctList) - } - } } .launchIn(scope) chatFolderRepository.foldersFlow .onEach { folders -> - _foldersState.update { it.copy(folders = folders) } + _state.update { it.copy(folders = folders) } } .launchIn(scope) chatFolderRepository.folderLoadingFlow .onEach { update -> - _foldersState.update { + _state.update { val newLoadingByFolder = it.isLoadingByFolder.toMutableMap() newLoadingByFolder[update.folderId] = update.isLoading it.copy(isLoadingByFolder = newLoadingByFolder) } - if (update.folderId == _foldersState.value.selectedFolderId) { - _chatsState.update { - it.copy(isLoading = update.isLoading) - } - } } .launchIn(scope) chatListRepository.connectionStateFlow .onEach { status -> - _uiState.update { it.copy(connectionStatus = status) } + _state.update { it.copy(connectionStatus = status) } } .launchIn(scope) appPreferences.enabledProxyId .onEach { enabledProxyId -> - _uiState.update { it.copy(isProxyEnabled = enabledProxyId != null) } + _state.update { it.copy(isProxyEnabled = enabledProxyId != null) } } .launchIn(scope) chatOperationsRepository.isArchivePinned .onEach { isPinned -> - _uiState.update { it.copy(isArchivePinned = isPinned) } + _state.update { it.copy(isArchivePinned = isPinned) } } .launchIn(scope) chatOperationsRepository.isArchiveAlwaysVisible .onEach { alwaysVisible -> - _uiState.update { it.copy(isArchiveAlwaysVisible = alwaysVisible) } + _state.update { it.copy(isArchiveAlwaysVisible = alwaysVisible) } } .launchIn(scope) chatSearchRepository.searchHistory .onEach { history -> - _searchState.update { + _state.update { it.copy( recentUsers = history.filter { chat -> (chat.type == ChatType.PRIVATE || chat.type == ChatType.SECRET) && !chat.isBot @@ -163,14 +221,14 @@ class DefaultChatListComponent( attachMenuBotRepository.getAttachMenuBots() .onEach { bots -> - _uiState.update { it.copy(attachMenuBots = bots) } + _state.update { it.copy(attachMenuBots = bots) } bots.firstOrNull()?.let { bot -> if (bot.botUserId != 0L) { val botInfo = botRepository.getBotInfo(bot.botUserId) val menuButton = botInfo?.menuButton if (menuButton is BotMenuButtonModel.WebApp) { - _uiState.update { + _state.update { it.copy( botWebAppUrl = menuButton.url, botWebAppName = menuButton.text @@ -184,7 +242,7 @@ class DefaultChatListComponent( updateRepository.updateState .onEach { updateState -> - _uiState.update { it.copy(updateState = updateState) } + _state.update { it.copy(updateState = updateState) } } .launchIn(scope) @@ -192,19 +250,46 @@ class DefaultChatListComponent( updateRepository.checkForUpdates() } + _state.onEach { + _uiState.value = it.toUiState() + _foldersState.value = it.toFoldersState() + _chatsState.value = it.toChatsState() + _selectionState.value = it.toSelectionState() + _searchState.value = it.toSearchState() + store.accept(ChatListStore.Intent.UpdateState(it)) + }.launchIn(scope) + + store.labels + .onEach { label -> + when (label) { + is ChatListStore.Label.OpenChat -> onSelect(label.chatId, label.messageId) + is ChatListStore.Label.OpenProfile -> onProfileSelect(label.id) + ChatListStore.Label.OpenSettings -> onSettingsClick() + ChatListStore.Label.OpenProxySettings -> onProxySettingsClick() + ChatListStore.Label.OpenNewChat -> onNewChatClick() + is ChatListStore.Label.ConfirmForward -> onConfirmForward(label.selectedChatIds) + is ChatListStore.Label.EditFolders -> onEditFoldersClick() + } + } + .launchIn(scope) + scope.launch(Dispatchers.IO) { - chatListRepository.selectFolder(_foldersState.value.selectedFolderId) + chatListRepository.selectFolder(_state.value.selectedFolderId) } } - override fun retryConnection() { + override fun retryConnection() = store.accept(ChatListStore.Intent.RetryConnection) + + internal fun handleRetryConnection() { chatListRepository.retryConnection() } - override fun onFolderClicked(id: Int) { - if (_foldersState.value.selectedFolderId == id) return + override fun onFolderClicked(id: Int) = store.accept(ChatListStore.Intent.FolderClicked(id)) + + internal fun handleFolderClicked(id: Int) { + if (_state.value.selectedFolderId == id) return - _foldersState.update { + _state.update { val loadingByFolder = it.isLoadingByFolder.toMutableMap() loadingByFolder[id] = true it.copy( @@ -213,37 +298,36 @@ class DefaultChatListComponent( ) } - _chatsState.value = ChatListComponent.ChatsState( - chats = _foldersState.value.chatsByFolder[id].orEmpty(), - isLoading = true - ) - scope.launch(Dispatchers.IO) { chatListRepository.selectFolder(id) } } - override fun loadMore(folderId: Int?) { - val targetFolderId = folderId ?: _foldersState.value.selectedFolderId - if (_foldersState.value.isLoadingByFolder[targetFolderId] == true) return + override fun loadMore(folderId: Int?) = store.accept(ChatListStore.Intent.LoadMore(folderId)) + + internal fun handleLoadMore(folderId: Int?) { + val targetFolderId = folderId ?: _state.value.selectedFolderId + if (_state.value.isLoadingByFolder[targetFolderId] == true) return scope.launch(Dispatchers.IO) { - if (folderId != null && folderId != _foldersState.value.selectedFolderId) { + if (folderId != null && folderId != _state.value.selectedFolderId) { return@launch } chatListRepository.loadNextChunk(20) } } - override fun loadMoreMessages() { + override fun loadMoreMessages() = store.accept(ChatListStore.Intent.LoadMoreMessages) + + internal fun handleLoadMoreMessages() { if (isFetchingMoreMessages || nextMessagesOffset.isEmpty()) return isFetchingMoreMessages = true - val query = _searchState.value.searchQuery + val query = _state.value.searchQuery scope.launch(Dispatchers.IO) { val result = chatSearchRepository.searchMessages(query, offset = nextMessagesOffset) nextMessagesOffset = result.nextOffset - _searchState.update { + _state.update { it.copy( messageSearchResults = it.messageSearchResults + result.messages, canLoadMoreMessages = nextMessagesOffset.isNotEmpty() @@ -253,51 +337,62 @@ class DefaultChatListComponent( } } - override fun onChatClicked(id: Long) { - if (_uiState.value.isForwarding) { + override fun onChatClicked(id: Long) = store.accept(ChatListStore.Intent.ChatClicked(id)) + + internal fun handleChatClicked(id: Long): ChatListStore.Label.OpenChat? { + if (_state.value.isForwarding) { toggleSelection(id) - } else if (_selectionState.value.selectedChatIds.isNotEmpty()) { + return null + } else if (_state.value.selectedChatIds.isNotEmpty()) { toggleSelection(id) + return null } else { - if (_searchState.value.isSearchActive) { + if (_state.value.isSearchActive) { chatSearchRepository.addSearchChatId(id) } - onSelect(id, null) + return ChatListStore.Label.OpenChat(id, null) } } - override fun onProfileClicked(id: Long) { - onProfileSelect(id) - } + override fun onProfileClicked(id: Long) = store.accept(ChatListStore.Intent.ProfileClicked(id)) - override fun onMessageClicked(chatId: Long, messageId: Long) { - if (_uiState.value.isForwarding) { + internal fun handleMessageClicked(chatId: Long, messageId: Long): ChatListStore.Label.OpenChat? { + if (_state.value.isForwarding) { toggleSelection(chatId) - } else if (_selectionState.value.selectedChatIds.isNotEmpty()) { + return null + } else if (_state.value.selectedChatIds.isNotEmpty()) { toggleSelection(chatId) + return null } else { - if (_searchState.value.isSearchActive) { + if (_state.value.isSearchActive) { chatSearchRepository.addSearchChatId(chatId) } - onSelect(chatId, messageId) + return ChatListStore.Label.OpenChat(chatId, messageId) } } - override fun onChatLongClicked(id: Long) { + override fun onMessageClicked(chatId: Long, messageId: Long) = + store.accept(ChatListStore.Intent.MessageClicked(chatId, messageId)) + + override fun onChatLongClicked(id: Long) = store.accept(ChatListStore.Intent.ChatLongClicked(id)) + + internal fun handleChatLongClicked(id: Long) { toggleSelection(id) } - override fun clearSelection() { - _selectionState.update { it.copy(selectedChatIds = emptySet()) } - } + override fun clearSelection() = store.accept(ChatListStore.Intent.ClearSelection) - override fun onSettingsClicked() { - onSettingsClick() + internal fun handleClearSelection() { + _state.update { it.copy(selectedChatIds = emptySet()) } } - override fun onSearchToggle() { - val isSearchActive = !_searchState.value.isSearchActive - _searchState.update { + override fun onSettingsClicked() = store.accept(ChatListStore.Intent.SettingsClicked) + + override fun onSearchToggle() = store.accept(ChatListStore.Intent.SearchToggle) + + internal fun handleSearchToggle() { + val isSearchActive = !_state.value.isSearchActive + _state.update { it.copy( isSearchActive = isSearchActive, searchQuery = "", @@ -310,22 +405,24 @@ class DefaultChatListComponent( nextMessagesOffset = "" } - override fun onSearchQueryChange(query: String) { - _searchState.update { it.copy(searchQuery = query) } + override fun onSearchQueryChange(query: String) = store.accept(ChatListStore.Intent.SearchQueryChange(query)) + + internal fun handleSearchQueryChange(query: String) { + _state.update { it.copy(searchQuery = query) } searchJob?.cancel() searchJob = scope.launch(Dispatchers.IO) { delay(300) if (query.isNotEmpty()) { - if (_foldersState.value.selectedFolderId == -2) { - val archivedChats = _foldersState.value.chatsByFolder[-2].orEmpty() + if (_state.value.selectedFolderId == -2) { + val archivedChats = _state.value.chatsByFolder[-2].orEmpty() val trimmedQuery = query.trim() val archiveResults = archivedChats.filter { chat -> chat.title.contains(trimmedQuery, ignoreCase = true) || chat.lastMessageText.contains(trimmedQuery, ignoreCase = true) } - _searchState.update { + _state.update { it.copy( searchQuery = query, searchResults = archiveResults, @@ -339,14 +436,14 @@ class DefaultChatListComponent( } val localResults = chatSearchRepository.searchChats(query) - _searchState.update { it.copy(searchResults = localResults) } + _state.update { it.copy(searchResults = localResults) } val globalResults = chatSearchRepository.searchPublicChats(query) - _searchState.update { it.copy(globalSearchResults = globalResults) } + _state.update { it.copy(globalSearchResults = globalResults) } val messageResults = chatSearchRepository.searchMessages(query) nextMessagesOffset = messageResults.nextOffset - _searchState.update { + _state.update { it.copy( messageSearchResults = messageResults.messages, canLoadMoreMessages = nextMessagesOffset.isNotEmpty() @@ -354,7 +451,7 @@ class DefaultChatListComponent( } } else { nextMessagesOffset = "" - _searchState.update { + _state.update { it.copy( searchQuery = "", searchResults = emptyList(), @@ -367,8 +464,11 @@ class DefaultChatListComponent( } } - override fun onSetEmojiStatus(customEmojiId: Long, statusPath: String?) { - _uiState.update { state -> + override fun onSetEmojiStatus(customEmojiId: Long, statusPath: String?) = + store.accept(ChatListStore.Intent.SetEmojiStatus(customEmojiId, statusPath)) + + internal fun handleSetEmojiStatus(customEmojiId: Long, statusPath: String?) { + _state.update { state -> val user = state.currentUser ?: return@update state state.copy( currentUser = user.copy( @@ -385,17 +485,24 @@ class DefaultChatListComponent( } } - override fun onClearSearchHistory() { + override fun onClearSearchHistory() = store.accept(ChatListStore.Intent.ClearSearchHistory) + + internal fun handleClearSearchHistory() { chatSearchRepository.clearSearchHistory() } - override fun onRemoveSearchHistoryItem(chatId: Long) { + override fun onRemoveSearchHistoryItem(chatId: Long) = + store.accept(ChatListStore.Intent.RemoveSearchHistoryItem(chatId)) + + internal fun handleRemoveSearchHistoryItem(chatId: Long) { chatSearchRepository.removeSearchChatId(chatId) } - override fun onMuteSelected(mute: Boolean) { - val selectedIds = _selectionState.value.selectedChatIds - val selectedChats = _chatsState.value.chats.filter { selectedIds.contains(it.id) } + override fun onMuteSelected(mute: Boolean) = store.accept(ChatListStore.Intent.MuteSelected(mute)) + + internal fun handleMuteSelected(mute: Boolean) { + val selectedIds = _state.value.selectedChatIds + val selectedChats = _state.value.chats.filter { selectedIds.contains(it.id) } val shouldMute = selectedChats.any { !it.isMuted } scope.launch(Dispatchers.IO) { @@ -404,94 +511,109 @@ class DefaultChatListComponent( } } - override fun onArchiveSelected(archive: Boolean) { - val selectedIds = _selectionState.value.selectedChatIds + override fun onArchiveSelected(archive: Boolean) = store.accept(ChatListStore.Intent.ArchiveSelected(archive)) + + internal fun handleArchiveSelected(archive: Boolean) { + val selectedIds = _state.value.selectedChatIds scope.launch(Dispatchers.IO) { chatOperationsRepository.toggleArchiveChats(selectedIds, archive) - clearSelection() + handleClearSelection() } } - override fun onPinSelected() { - val selectedIds = _selectionState.value.selectedChatIds - val selectedChats = _chatsState.value.chats.filter { selectedIds.contains(it.id) } + override fun onPinSelected() = store.accept(ChatListStore.Intent.PinSelected) + + internal fun handlePinSelected() { + val selectedIds = _state.value.selectedChatIds + val selectedChats = _state.value.chats.filter { selectedIds.contains(it.id) } val shouldPin = selectedChats.any { !it.isPinned } - val folderId = _foldersState.value.selectedFolderId + val folderId = _state.value.selectedFolderId scope.launch(Dispatchers.IO) { chatOperationsRepository.togglePinChats(selectedIds, shouldPin, folderId) - clearSelection() + handleClearSelection() } } - override fun onToggleReadSelected() { - val selectedIds = _selectionState.value.selectedChatIds - val selectedChats = _chatsState.value.chats.filter { selectedIds.contains(it.id) } + override fun onToggleReadSelected() = store.accept(ChatListStore.Intent.ToggleReadSelected) + + internal fun handleToggleReadSelected() { + val selectedIds = _state.value.selectedChatIds + val selectedChats = _state.value.chats.filter { selectedIds.contains(it.id) } val shouldMarkUnread = selectedChats.any { !it.isMarkedAsUnread } scope.launch(Dispatchers.IO) { chatOperationsRepository.toggleReadChats(selectedIds, shouldMarkUnread) - clearSelection() + handleClearSelection() } } - override fun onDeleteSelected() { - val selectedIds = _selectionState.value.selectedChatIds + override fun onDeleteSelected() = store.accept(ChatListStore.Intent.DeleteSelected) + + internal fun handleDeleteSelected() { + val selectedIds = _state.value.selectedChatIds scope.launch(Dispatchers.IO) { chatOperationsRepository.deleteChats(selectedIds) - clearSelection() + handleClearSelection() } } - override fun onArchivePinToggle() { - chatOperationsRepository.setArchivePinned(!_uiState.value.isArchivePinned) - } + override fun onArchivePinToggle() = store.accept(ChatListStore.Intent.ArchivePinToggle) - override fun onConfirmForwarding() { - val selectedChatIds = _selectionState.value.selectedChatIds - if (selectedChatIds.isNotEmpty()) { - onConfirmForward(selectedChatIds) - } + internal fun handleArchivePinToggle() { + chatOperationsRepository.setArchivePinned(!_state.value.isArchivePinned) } - override fun onNewChatClicked() { - onNewChatClick() - } + override fun onConfirmForwarding() = store.accept(ChatListStore.Intent.ConfirmForwarding) - override fun onProxySettingsClicked() { - onProxySettingsClick() + internal fun handleConfirmForwarding(): ChatListStore.Label.ConfirmForward? { + val selectedChatIds = _state.value.selectedChatIds + return selectedChatIds.takeIf { it.isNotEmpty() }?.let(ChatListStore.Label::ConfirmForward) } - override fun onEditFoldersClicked() { - onEditFoldersClick() - } + override fun onNewChatClicked() = store.accept(ChatListStore.Intent.NewChatClicked) - override fun onDeleteFolder(folderId: Int) { + override fun onProxySettingsClicked() = store.accept(ChatListStore.Intent.ProxySettingsClicked) + + override fun onEditFoldersClicked() = store.accept(ChatListStore.Intent.EditFoldersClicked) + + override fun onDeleteFolder(folderId: Int) = store.accept(ChatListStore.Intent.DeleteFolder(folderId)) + + internal fun handleDeleteFolder(folderId: Int) { if (folderId <= 0) return scope.launch(Dispatchers.IO) { chatFolderRepository.deleteFolder(folderId) - if (_foldersState.value.selectedFolderId == folderId) { - onFolderClicked(-1) + if (_state.value.selectedFolderId == folderId) { + handleFolderClicked(-1) } } } - override fun onEditFolder(folderId: Int) { - if (folderId <= 0) return - onEditFoldersClick() + override fun onEditFolder(folderId: Int) = store.accept(ChatListStore.Intent.EditFolder(folderId)) + + internal fun handleEditFolder(folderId: Int): ChatListStore.Label.EditFolders? { + if (folderId <= 0) return null + return ChatListStore.Label.EditFolders(folderId) } - override fun onOpenInstantView(url: String) { - _uiState.update { it.copy(instantViewUrl = url) } + override fun onOpenInstantView(url: String) = store.accept(ChatListStore.Intent.OpenInstantView(url)) + + internal fun handleOpenInstantView(url: String) { + _state.update { it.copy(instantViewUrl = url) } } - override fun onDismissInstantView() { - _uiState.update { it.copy(instantViewUrl = null) } + override fun onDismissInstantView() = store.accept(ChatListStore.Intent.DismissInstantView) + + internal fun handleDismissInstantView() { + _state.update { it.copy(instantViewUrl = null) } } - override fun onOpenWebApp(url: String, botUserId: Long, botName: String) { - _uiState.update { + override fun onOpenWebApp(url: String, botUserId: Long, botName: String) = + store.accept(ChatListStore.Intent.OpenWebApp(url, botUserId, botName)) + + internal fun handleOpenWebApp(url: String, botUserId: Long, botName: String) { + _state.update { it.copy( webAppUrl = url, webAppBotId = botUserId, @@ -500,8 +622,10 @@ class DefaultChatListComponent( } } - override fun onDismissWebApp() { - _uiState.update { + override fun onDismissWebApp() = store.accept(ChatListStore.Intent.DismissWebApp) + + internal fun handleDismissWebApp() { + _state.update { it.copy( webAppUrl = null, webAppBotId = null, @@ -510,16 +634,22 @@ class DefaultChatListComponent( } } - override fun onOpenWebView(url: String) { - _uiState.update { it.copy(webViewUrl = url) } + override fun onOpenWebView(url: String) = store.accept(ChatListStore.Intent.OpenWebView(url)) + + internal fun handleOpenWebView(url: String) { + _state.update { it.copy(webViewUrl = url) } } - override fun onDismissWebView() { - _uiState.update { it.copy(webViewUrl = null) } + override fun onDismissWebView() = store.accept(ChatListStore.Intent.DismissWebView) + + internal fun handleDismissWebView() { + _state.update { it.copy(webViewUrl = null) } } - override fun onUpdateClicked() { - val currentState = _uiState.value.updateState + override fun onUpdateClicked() = store.accept(ChatListStore.Intent.UpdateClicked) + + internal fun handleUpdateClicked(): ChatListStore.Label.OpenSettings? { + val currentState = _state.value.updateState when (currentState) { is UpdateState.UpdateAvailable -> { updateRepository.downloadUpdate() @@ -534,39 +664,40 @@ class DefaultChatListComponent( } else -> { - onSettingsClick() + return ChatListStore.Label.OpenSettings } } + return null } override fun handleBack(): Boolean { return when { - uiState.value.webViewUrl != null -> { - onDismissWebView() + state.value.webViewUrl != null -> { + handleDismissWebView() true } - uiState.value.webAppUrl != null -> { - onDismissWebApp() + state.value.webAppUrl != null -> { + handleDismissWebApp() true } - uiState.value.instantViewUrl != null -> { - onDismissInstantView() + state.value.instantViewUrl != null -> { + handleDismissInstantView() true } - searchState.value.isSearchActive -> { - onSearchToggle() + state.value.isSearchActive -> { + handleSearchToggle() true } - selectionState.value.selectedChatIds.isNotEmpty() -> { - clearSelection() + state.value.selectedChatIds.isNotEmpty() -> { + handleClearSelection() true } - foldersState.value.selectedFolderId == -2 -> { - onFolderClicked(-1) + state.value.selectedFolderId == -2 -> { + handleFolderClicked(-1) true } - uiState.value.isForwarding -> { + state.value.isForwarding -> { onSelect(0L, null) true } @@ -574,8 +705,11 @@ class DefaultChatListComponent( } } - override fun updateScrollPosition(folderId: Int, index: Int, offset: Int) { - _foldersState.update { + override fun updateScrollPosition(folderId: Int, index: Int, offset: Int) = + store.accept(ChatListStore.Intent.UpdateScrollPosition(folderId, index, offset)) + + internal fun handleUpdateScrollPosition(folderId: Int, index: Int, offset: Int) { + _state.update { val newPositions = it.scrollPositions.toMutableMap() newPositions[folderId] = index to offset it.copy(scrollPositions = newPositions) @@ -587,12 +721,12 @@ class DefaultChatListComponent( } private fun toggleSelection(id: Long) { - val currentSelection = _selectionState.value.selectedChatIds + val currentSelection = _state.value.selectedChatIds val newSelection = if (currentSelection.contains(id)) { currentSelection - id } else { currentSelection + id } - _selectionState.value = _selectionState.value.copy(selectedChatIds = newSelection) + _state.value = _state.value.copy(selectedChatIds = newSelection) } }