From 8afe58c644f78ca4d45ac65d39648ffd40dd18d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolf-Martell=20Montw=C3=A9?= Date: Fri, 13 Sep 2024 12:04:30 +0200 Subject: [PATCH] Add OnAccountClick event to trigger account selection and add OnAccountViewClick on the account view for testing --- .../drawer/ui/DrawerContentPreview.kt | 4 +- .../drawer/ui/account/AccountViewPreview.kt | 4 + .../navigation/drawer/ui/DrawerContent.kt | 3 + .../navigation/drawer/ui/DrawerContract.kt | 3 +- .../navigation/drawer/ui/DrawerView.kt | 3 +- .../navigation/drawer/ui/DrawerViewModel.kt | 46 ++++++-- .../drawer/ui/account/AccountView.kt | 3 + .../drawer/ui/DrawerViewModelTest.kt | 109 ++++++++++++++++-- 8 files changed, 156 insertions(+), 19 deletions(-) diff --git a/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContentPreview.kt b/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContentPreview.kt index 07f21cc8169..f9cb6d53766 100644 --- a/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContentPreview.kt +++ b/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContentPreview.kt @@ -12,10 +12,11 @@ internal fun DrawerContentPreview() { PreviewWithTheme { DrawerContent( state = DrawerContract.State( - currentAccount = null, accounts = persistentListOf(), + currentAccount = null, folders = persistentListOf(), ), + onEvent = {}, ) } } @@ -29,6 +30,7 @@ fun DrawerContentWithAccountPreview() { accounts = persistentListOf(DISPLAY_ACCOUNT), currentAccount = DISPLAY_ACCOUNT, ), + onEvent = {}, ) } } diff --git a/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/account/AccountViewPreview.kt b/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/account/AccountViewPreview.kt index cded6eea7c3..63595649232 100644 --- a/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/account/AccountViewPreview.kt +++ b/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/account/AccountViewPreview.kt @@ -15,6 +15,7 @@ internal fun AccountViewPreview() { displayName = DISPLAY_NAME, emailAddress = EMAIL_ADDRESS, accountColor = 0, + onClick = {}, ) } } @@ -27,6 +28,7 @@ internal fun AccountViewWithColorPreview() { displayName = DISPLAY_NAME, emailAddress = EMAIL_ADDRESS, accountColor = 0xFF0000, + onClick = {}, ) } } @@ -39,6 +41,7 @@ internal fun AccountViewWithLongDisplayName() { displayName = "$LONG_TEXT $DISPLAY_NAME", emailAddress = EMAIL_ADDRESS, accountColor = 0, + onClick = {}, ) } } @@ -51,6 +54,7 @@ internal fun AccountViewWithLongEmailPreview() { displayName = DISPLAY_NAME, emailAddress = "$LONG_TEXT@example.com", accountColor = 0, + onClick = {}, ) } } diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContent.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContent.kt index db0c7ee30ab..202bf9c7b08 100644 --- a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContent.kt +++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContent.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.platform.testTag import app.k9mail.core.ui.compose.designsystem.atom.DividerHorizontal import app.k9mail.core.ui.compose.designsystem.atom.Surface import app.k9mail.core.ui.compose.theme2.MainTheme +import app.k9mail.feature.navigation.drawer.ui.DrawerContract.Event import app.k9mail.feature.navigation.drawer.ui.DrawerContract.State import app.k9mail.feature.navigation.drawer.ui.account.AccountView import app.k9mail.feature.navigation.drawer.ui.folder.FolderList @@ -17,6 +18,7 @@ import app.k9mail.feature.navigation.drawer.ui.folder.FolderList @Composable fun DrawerContent( state: State, + onEvent: (Event) -> Unit, modifier: Modifier = Modifier, ) { Surface( @@ -37,6 +39,7 @@ fun DrawerContent( displayName = it.account.displayName, emailAddress = it.account.email, accountColor = it.account.chipColor, + onClick = { onEvent(Event.OnAccountViewClick(it)) }, ) DividerHorizontal() diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContract.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContract.kt index 4d9f14a78aa..d2652ddc2f6 100644 --- a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContract.kt +++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContract.kt @@ -5,7 +5,6 @@ import app.k9mail.core.ui.compose.common.mvi.UnidirectionalViewModel import app.k9mail.feature.navigation.drawer.domain.entity.DisplayAccount import app.k9mail.legacy.ui.folder.DisplayFolder import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.immutableListOf import kotlinx.collections.immutable.persistentListOf interface DrawerContract { @@ -23,6 +22,8 @@ interface DrawerContract { sealed interface Event { data object OnRefresh : Event + data class OnAccountClick(val account: DisplayAccount) : Event + data class OnAccountViewClick(val account: DisplayAccount) : Event } sealed interface Effect diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerView.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerView.kt index 7f7ca2d888b..ca14902c944 100644 --- a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerView.kt +++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerView.kt @@ -3,9 +3,9 @@ package app.k9mail.feature.navigation.drawer.ui import androidx.compose.runtime.Composable import app.k9mail.core.ui.compose.common.mvi.observe import app.k9mail.core.ui.compose.designsystem.molecule.PullToRefreshBox -import org.koin.androidx.compose.koinViewModel import app.k9mail.feature.navigation.drawer.ui.DrawerContract.Event import app.k9mail.feature.navigation.drawer.ui.DrawerContract.ViewModel +import org.koin.androidx.compose.koinViewModel @Composable fun DrawerView( @@ -19,6 +19,7 @@ fun DrawerView( ) { DrawerContent( state = state.value, + onEvent = { dispatch(it) }, ) } } diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewModel.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewModel.kt index 92c9430ccbf..87a7f664bc6 100644 --- a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewModel.kt +++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewModel.kt @@ -10,10 +10,12 @@ import app.k9mail.feature.navigation.drawer.ui.DrawerContract.State import app.k9mail.feature.navigation.drawer.ui.DrawerContract.ViewModel import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.launch @Suppress("MagicNumber") @@ -58,16 +60,15 @@ class DrawerViewModel( } } + @OptIn(ExperimentalCoroutinesApi::class) private suspend fun loadFolders() { - state.map { it.currentAccount } + state.mapNotNull { it.currentAccount?.account?.uuid } .distinctUntilChanged() - .collectLatest { currentAccount -> - if (currentAccount != null) { - getDisplayFoldersForAccount(currentAccount.account.uuid).collectLatest { folders -> - updateState { - it.copy(folders = folders.toImmutableList()) - } - } + .flatMapLatest { accountUuid -> + getDisplayFoldersForAccount(accountUuid) + }.collectLatest { folders -> + updateState { + it.copy(folders = folders.toImmutableList()) } } } @@ -75,6 +76,33 @@ class DrawerViewModel( override fun event(event: Event) { when (event) { Event.OnRefresh -> refresh() + is Event.OnAccountClick -> selectAccount(event.account) + is Event.OnAccountViewClick -> { + selectAccount( + state.value.accounts.nextOrFirst(event.account)!!, + ) + } + } + } + + private fun selectAccount(account: DisplayAccount) { + viewModelScope.launch { + updateState { + it.copy( + currentAccount = account, + ) + } + } + } + + private fun ImmutableList.nextOrFirst(account: DisplayAccount): DisplayAccount? { + val index = indexOf(account) + return if (index == -1) { + null + } else if (index == size - 1) { + get(0) + } else { + get(index + 1) } } diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/account/AccountView.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/account/AccountView.kt index d4d16d78894..4cdbeda9110 100644 --- a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/account/AccountView.kt +++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/account/AccountView.kt @@ -1,5 +1,6 @@ package app.k9mail.feature.navigation.drawer.ui.account +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize @@ -21,11 +22,13 @@ fun AccountView( emailAddress: String, accountColor: Int, modifier: Modifier = Modifier, + onClick: () -> Unit, ) { Row( modifier = modifier .fillMaxWidth() .height(intrinsicSize = IntrinsicSize.Max) + .clickable(onClick = onClick) .padding( top = MainTheme.spacings.default, start = MainTheme.spacings.double, diff --git a/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewModelTest.kt b/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewModelTest.kt index 3ef98c9a39f..e4fdece880c 100644 --- a/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewModelTest.kt +++ b/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewModelTest.kt @@ -1,5 +1,7 @@ package app.k9mail.feature.navigation.drawer.ui +import app.k9mail.core.mail.folder.api.Folder +import app.k9mail.core.mail.folder.api.FolderType import app.k9mail.core.ui.compose.testing.MainDispatcherRule import app.k9mail.core.ui.compose.testing.mvi.eventStateTest import app.k9mail.feature.navigation.drawer.domain.entity.DisplayAccount @@ -47,7 +49,7 @@ class DrawerViewModelTest { val displayAccounts = createDisplayAccountList(3) val getDisplayAccountsFlow = MutableStateFlow(displayAccounts) val testSubject = createTestSubject( - getDisplayAccountsFlow = getDisplayAccountsFlow, + displayAccountsFlow = getDisplayAccountsFlow, ) advanceUntilIdle() @@ -62,7 +64,7 @@ class DrawerViewModelTest { val displayAccounts = createDisplayAccountList(3) val getDisplayAccountsFlow = MutableStateFlow(displayAccounts) val testSubject = createTestSubject( - getDisplayAccountsFlow = getDisplayAccountsFlow, + displayAccountsFlow = getDisplayAccountsFlow, ) advanceUntilIdle() @@ -81,7 +83,7 @@ class DrawerViewModelTest { fun `should set current account to null when no accounts are present`() = runTest { val getDisplayAccountsFlow = MutableStateFlow(emptyList()) val testSubject = createTestSubject( - getDisplayAccountsFlow = getDisplayAccountsFlow, + displayAccountsFlow = getDisplayAccountsFlow, ) advanceUntilIdle() @@ -90,13 +92,76 @@ class DrawerViewModelTest { assertThat(testSubject.state.value.currentAccount).isEqualTo(null) } + @Test + fun `should set current account when OnAccountClick event is received`() = runTest { + val displayAccounts = createDisplayAccountList(3) + val getDisplayAccountsFlow = MutableStateFlow(displayAccounts) + val testSubject = createTestSubject( + displayAccountsFlow = getDisplayAccountsFlow, + ) + + advanceUntilIdle() + + testSubject.event(Event.OnAccountClick(displayAccounts[1])) + + advanceUntilIdle() + + assertThat(testSubject.state.value.currentAccount).isEqualTo(displayAccounts[1]) + } + + @Test + fun `should collect display folders for current account`() = runTest { + val displayAccounts = createDisplayAccountList(3) + val getDisplayAccountsFlow = MutableStateFlow(displayAccounts) + val displayFoldersMap = mapOf( + displayAccounts[0].account.uuid to createDisplayFolderList(3), + ) + val testSubject = createTestSubject( + displayAccountsFlow = getDisplayAccountsFlow, + displayFoldersMap = displayFoldersMap, + ) + + advanceUntilIdle() + + val displayFolders = displayFoldersMap[displayAccounts[0].account.uuid] ?: emptyList() + assertThat(testSubject.state.value.folders.size).isEqualTo(displayFolders.size) + assertThat(testSubject.state.value.folders).isEqualTo(displayFolders) + } + + @Test + fun `should collect display folders when current account is changed`() = runTest { + val displayAccounts = createDisplayAccountList(3) + val getDisplayAccountsFlow = MutableStateFlow(displayAccounts) + val displayFoldersMap = mapOf( + displayAccounts[0].account.uuid to createDisplayFolderList(1), + displayAccounts[1].account.uuid to createDisplayFolderList(5), + displayAccounts[2].account.uuid to createDisplayFolderList(10), + ) + val testSubject = createTestSubject( + displayAccountsFlow = getDisplayAccountsFlow, + displayFoldersMap = displayFoldersMap, + ) + + advanceUntilIdle() + + testSubject.event(Event.OnAccountClick(displayAccounts[1])) + + advanceUntilIdle() + + val displayFolders = displayFoldersMap[displayAccounts[1].account.uuid] ?: emptyList() + assertThat(testSubject.state.value.folders.size).isEqualTo(displayFolders.size) + assertThat(testSubject.state.value.folders).isEqualTo(displayFolders) + } + private fun createTestSubject( - getDisplayAccountsFlow: Flow> = flow { emit(emptyList()) }, - getDisplayFoldersForAccount: Flow> = flow { emit(emptyList()) }, + displayAccountsFlow: Flow> = flow { emit(emptyList()) }, + displayFoldersMap: Map> = emptyMap(), ): DrawerViewModel { return DrawerViewModel( - getDisplayAccounts = { getDisplayAccountsFlow }, - getDisplayFoldersForAccount = { getDisplayFoldersForAccount }, + getDisplayAccounts = { displayAccountsFlow }, + getDisplayFoldersForAccount = { accountUuid -> + flow { emit(displayFoldersMap[accountUuid] ?: emptyList()) } + }, ) } @@ -137,4 +202,34 @@ class DrawerViewModelTest { ) } } + + private fun createDisplayFolder( + id: Long = 1234, + name: String = "name", + type: FolderType = FolderType.REGULAR, + unreadCount: Int = 0, + starredCount: Int = 0, + ): DisplayFolder { + val folder = Folder( + id = id, + name = name, + type = type, + isLocalOnly = false, + ) + + return DisplayFolder( + folder = folder, + isInTopGroup = false, + unreadMessageCount = unreadCount, + starredMessageCount = starredCount, + ) + } + + private fun createDisplayFolderList(count: Int): List { + return List(count) { index -> + createDisplayFolder( + id = index.toLong() + 100, + ) + } + } }