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 f9cb6d5376..4b7398034d 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 @@ -13,7 +13,7 @@ internal fun DrawerContentPreview() { DrawerContent( state = DrawerContract.State( accounts = persistentListOf(), - currentAccount = null, + selectedAccount = null, folders = persistentListOf(), ), onEvent = {}, @@ -28,7 +28,8 @@ fun DrawerContentWithAccountPreview() { DrawerContent( state = DrawerContract.State( accounts = persistentListOf(DISPLAY_ACCOUNT), - currentAccount = DISPLAY_ACCOUNT, + selectedAccount = DISPLAY_ACCOUNT, + folders = persistentListOf(), ), onEvent = {}, ) diff --git a/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/FakeData.kt b/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/FakeData.kt index 6706173139..cb847e0c1e 100644 --- a/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/FakeData.kt +++ b/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/FakeData.kt @@ -3,9 +3,9 @@ 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.feature.navigation.drawer.domain.entity.DisplayAccount +import app.k9mail.feature.navigation.drawer.domain.entity.DisplayAccountFolder import app.k9mail.legacy.account.Account import app.k9mail.legacy.account.Identity -import app.k9mail.legacy.ui.folder.DisplayFolder internal object FakeData { @@ -45,7 +45,8 @@ internal object FakeData { isLocalOnly = false, ) - val DISPLAY_FOLDER = DisplayFolder( + val DISPLAY_FOLDER = DisplayAccountFolder( + accountUuid = ACCOUNT_UUID, folder = FOLDER, isInTopGroup = false, unreadMessageCount = 14, diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/FolderDrawer.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/FolderDrawer.kt index b2ff0e540f..d7aac983e6 100644 --- a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/FolderDrawer.kt +++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/FolderDrawer.kt @@ -15,6 +15,9 @@ import org.koin.core.component.inject class FolderDrawer( override val parent: AppCompatActivity, + private val openAccount: (account: Account) -> Unit, + private val openFolder: (folderId: Long) -> Unit, + createDrawerListener: () -> DrawerLayout.DrawerListener, ) : NavigationDrawer, KoinComponent { private val themeProvider: FeatureThemeProvider by inject() @@ -28,10 +31,15 @@ class FolderDrawer( sliderView.visibility = View.GONE drawerView.visibility = View.VISIBLE swipeRefreshLayout.isEnabled = false + drawer.addDrawerListener(createDrawerListener()) drawerView.setContent { themeProvider.WithTheme { - DrawerView() + DrawerView( + openAccount = openAccount, + openFolder = openFolder, + closeDrawer = { close() }, + ) } } } diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/NavigationDrawerExternalContract.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/NavigationDrawerExternalContract.kt new file mode 100644 index 0000000000..16db5fc39a --- /dev/null +++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/NavigationDrawerExternalContract.kt @@ -0,0 +1,10 @@ +package app.k9mail.feature.navigation.drawer + +import app.k9mail.feature.navigation.drawer.domain.entity.DrawerConfig + +interface NavigationDrawerExternalContract { + + fun interface DrawerConfigLoader { + fun loadDrawerConfig(): DrawerConfig + } +} diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/NavigationDrawerModule.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/NavigationDrawerModule.kt index 835f3a893e..5d751563bb 100644 --- a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/NavigationDrawerModule.kt +++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/NavigationDrawerModule.kt @@ -3,6 +3,8 @@ package app.k9mail.feature.navigation.drawer import app.k9mail.feature.navigation.drawer.domain.DomainContract.UseCase import app.k9mail.feature.navigation.drawer.domain.usecase.GetDisplayAccounts import app.k9mail.feature.navigation.drawer.domain.usecase.GetDisplayFoldersForAccount +import app.k9mail.feature.navigation.drawer.domain.usecase.GetDrawerConfig +import app.k9mail.feature.navigation.drawer.domain.usecase.SyncMail import app.k9mail.feature.navigation.drawer.legacy.AccountsViewModel import app.k9mail.feature.navigation.drawer.legacy.FoldersViewModel import app.k9mail.feature.navigation.drawer.ui.DrawerViewModel @@ -14,6 +16,12 @@ import org.koin.dsl.module val navigationDrawerModule: Module = module { + single { + GetDrawerConfig( + configProver = get(), + ) + } + single { GetDisplayAccounts( accountManager = get(), @@ -28,6 +36,12 @@ val navigationDrawerModule: Module = module { ) } + single { + SyncMail( + messagingController = get(), + ) + } + viewModel { AccountsViewModel( getDisplayAccounts = get(), @@ -48,8 +62,10 @@ val navigationDrawerModule: Module = module { viewModel { DrawerViewModel( + getDrawerConfig = get(), getDisplayAccounts = get(), getDisplayFoldersForAccount = get(), + syncMail = get(), ) } } diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/DomainContract.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/DomainContract.kt index 468de79c14..ed9eec4433 100644 --- a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/DomainContract.kt +++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/DomainContract.kt @@ -1,18 +1,33 @@ package app.k9mail.feature.navigation.drawer.domain import app.k9mail.feature.navigation.drawer.domain.entity.DisplayAccount -import app.k9mail.legacy.ui.folder.DisplayFolder +import app.k9mail.feature.navigation.drawer.domain.entity.DisplayAccountFolder +import app.k9mail.feature.navigation.drawer.domain.entity.DrawerConfig +import app.k9mail.legacy.account.Account import kotlinx.coroutines.flow.Flow interface DomainContract { interface UseCase { + fun interface GetDrawerConfig { + operator fun invoke(): Flow + } + fun interface GetDisplayAccounts { operator fun invoke(): Flow> } fun interface GetDisplayFoldersForAccount { - operator fun invoke(accountUuid: String): Flow> + operator fun invoke(accountUuid: String): Flow> + } + + /** + * Synchronize mail for the given account. + * + * Account can be null to synchronize unified inbox or account list. + */ + fun interface SyncMail { + operator fun invoke(account: Account?): Flow> } } } diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/entity/DisplayAccountFolder.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/entity/DisplayAccountFolder.kt new file mode 100644 index 0000000000..f5f561d1df --- /dev/null +++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/entity/DisplayAccountFolder.kt @@ -0,0 +1,11 @@ +package app.k9mail.feature.navigation.drawer.domain.entity + +import app.k9mail.core.mail.folder.api.Folder + +data class DisplayAccountFolder( + val accountUuid: String, + val folder: Folder, + val isInTopGroup: Boolean, + val unreadMessageCount: Int, + val starredMessageCount: Int, +) diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/entity/DrawerConfig.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/entity/DrawerConfig.kt new file mode 100644 index 0000000000..07c6168016 --- /dev/null +++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/entity/DrawerConfig.kt @@ -0,0 +1,6 @@ +package app.k9mail.feature.navigation.drawer.domain.entity + +data class DrawerConfig( + val showUnifiedInbox: Boolean, + val showStarredCount: Boolean, +) diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/GetDisplayFoldersForAccount.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/GetDisplayFoldersForAccount.kt index a25c7b5033..df5449c4b0 100644 --- a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/GetDisplayFoldersForAccount.kt +++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/GetDisplayFoldersForAccount.kt @@ -1,14 +1,25 @@ package app.k9mail.feature.navigation.drawer.domain.usecase import app.k9mail.feature.navigation.drawer.domain.DomainContract.UseCase -import app.k9mail.legacy.ui.folder.DisplayFolder +import app.k9mail.feature.navigation.drawer.domain.entity.DisplayAccountFolder import app.k9mail.legacy.ui.folder.DisplayFolderRepository import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map class GetDisplayFoldersForAccount( private val repository: DisplayFolderRepository, ) : UseCase.GetDisplayFoldersForAccount { - override fun invoke(accountUuid: String): Flow> { - return repository.getDisplayFoldersFlow(accountUuid) + override fun invoke(accountUuid: String): Flow> { + return repository.getDisplayFoldersFlow(accountUuid).map { displayFolders -> + displayFolders.map { displayFolder -> + DisplayAccountFolder( + accountUuid = accountUuid, + folder = displayFolder.folder, + isInTopGroup = displayFolder.isInTopGroup, + unreadMessageCount = displayFolder.unreadMessageCount, + starredMessageCount = displayFolder.starredMessageCount, + ) + } + } } } diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/GetDrawerConfig.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/GetDrawerConfig.kt new file mode 100644 index 0000000000..c812623bc0 --- /dev/null +++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/GetDrawerConfig.kt @@ -0,0 +1,18 @@ +package app.k9mail.feature.navigation.drawer.domain.usecase + +import app.k9mail.feature.navigation.drawer.NavigationDrawerExternalContract.DrawerConfigLoader +import app.k9mail.feature.navigation.drawer.domain.DomainContract.UseCase +import app.k9mail.feature.navigation.drawer.domain.entity.DrawerConfig +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +class GetDrawerConfig( + private val configProver: DrawerConfigLoader, +) : UseCase.GetDrawerConfig { + override operator fun invoke(): Flow { + // TODO This needs to be updated when the config changes + return flow { + emit(configProver.loadDrawerConfig()) + } + } +} diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/SyncMail.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/SyncMail.kt new file mode 100644 index 0000000000..36e8b27436 --- /dev/null +++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/SyncMail.kt @@ -0,0 +1,37 @@ +package app.k9mail.feature.navigation.drawer.domain.usecase + +import android.content.Context +import app.k9mail.feature.navigation.drawer.domain.DomainContract.UseCase +import app.k9mail.legacy.account.Account +import app.k9mail.legacy.message.controller.MessagingControllerMailChecker +import app.k9mail.legacy.message.controller.SimpleMessagingListener +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.flowOn + +class SyncMail( + private val messagingController: MessagingControllerMailChecker, + private val coroutineContext: CoroutineContext = Dispatchers.IO, +) : UseCase.SyncMail { + override fun invoke(account: Account?): Flow> = callbackFlow { + val listener = object : SimpleMessagingListener() { + override fun checkMailFinished(context: Context?, account: Account?) { + trySend(Result.success(Unit)) + close() + } + } + + messagingController.checkMail( + account = account, + ignoreLastCheckedTime = true, + useManualWakeLock = true, + notify = true, + listener = listener, + ) + + awaitClose() + }.flowOn(coroutineContext) +} 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 202bf9c7b0..44f46c729c 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 @@ -34,7 +34,7 @@ fun DrawerContent( ), verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.default), ) { - state.currentAccount?.let { + state.selectedAccount?.let { AccountView( displayName = it.account.displayName, emailAddress = it.account.email, @@ -46,9 +46,11 @@ fun DrawerContent( } FolderList( folders = state.folders, - selectedFolder = state.folders.firstOrNull(), // TODO Use selected folder from state - onFolderClick = { }, - showStarredCount = state.showStarredCount, + selectedFolder = state.selectedFolder, + onFolderClick = { folder -> + onEvent(Event.OnFolderClick(folder)) + }, + showStarredCount = state.config.showStarredCount, ) } } 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 d2652ddc2f..f7fd2c8bd6 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 @@ -3,7 +3,9 @@ package app.k9mail.feature.navigation.drawer.ui import androidx.compose.runtime.Stable 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 app.k9mail.feature.navigation.drawer.domain.entity.DisplayAccountFolder +import app.k9mail.feature.navigation.drawer.domain.entity.DrawerConfig +import app.k9mail.legacy.account.Account import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -13,18 +15,27 @@ interface DrawerContract { @Stable data class State( - val currentAccount: DisplayAccount? = null, + val config: DrawerConfig = DrawerConfig( + showUnifiedInbox = false, + showStarredCount = false, + ), val accounts: ImmutableList = persistentListOf(), - val folders: ImmutableList = persistentListOf(), - val showStarredCount: Boolean = false, + val selectedAccount: DisplayAccount? = null, + val folders: ImmutableList = persistentListOf(), + val selectedFolder: DisplayAccountFolder? = null, val isLoading: Boolean = false, ) sealed interface Event { - data object OnRefresh : Event data class OnAccountClick(val account: DisplayAccount) : Event data class OnAccountViewClick(val account: DisplayAccount) : Event + data class OnFolderClick(val folder: DisplayAccountFolder) : Event + data object OnRefresh : Event } - sealed interface Effect + sealed interface Effect { + data class OpenAccount(val account: Account) : Effect + data class OpenFolder(val folderId: Long) : Effect + data object CloseDrawer : 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 ca14902c94..3d86bdc93e 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,15 +3,26 @@ 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 app.k9mail.feature.navigation.drawer.ui.DrawerContract.Effect import app.k9mail.feature.navigation.drawer.ui.DrawerContract.Event import app.k9mail.feature.navigation.drawer.ui.DrawerContract.ViewModel +import app.k9mail.legacy.account.Account import org.koin.androidx.compose.koinViewModel @Composable fun DrawerView( + openAccount: (account: Account) -> Unit, + openFolder: (folderId: Long) -> Unit, + closeDrawer: () -> Unit, viewModel: ViewModel = koinViewModel(), ) { - val (state, dispatch) = viewModel.observe { } + val (state, dispatch) = viewModel.observe { effect -> + when (effect) { + is Effect.OpenAccount -> openAccount(effect.account) + is Effect.OpenFolder -> openFolder(effect.folderId) + Effect.CloseDrawer -> closeDrawer() + } + } PullToRefreshBox( isRefreshing = state.value.isLoading, 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 87a7f664bc..abc9ff3f67 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 @@ -4,6 +4,7 @@ import androidx.lifecycle.viewModelScope import app.k9mail.core.ui.compose.common.mvi.BaseViewModel import app.k9mail.feature.navigation.drawer.domain.DomainContract.UseCase import app.k9mail.feature.navigation.drawer.domain.entity.DisplayAccount +import app.k9mail.feature.navigation.drawer.domain.entity.DisplayAccountFolder import app.k9mail.feature.navigation.drawer.ui.DrawerContract.Effect import app.k9mail.feature.navigation.drawer.ui.DrawerContract.Event import app.k9mail.feature.navigation.drawer.ui.DrawerContract.State @@ -12,6 +13,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest @@ -20,8 +22,10 @@ import kotlinx.coroutines.launch @Suppress("MagicNumber") class DrawerViewModel( + private val getDrawerConfig: UseCase.GetDrawerConfig, private val getDisplayAccounts: UseCase.GetDisplayAccounts, private val getDisplayFoldersForAccount: UseCase.GetDisplayFoldersForAccount, + private val syncMail: UseCase.SyncMail, initialState: State = State(), ) : BaseViewModel( initialState = initialState, @@ -29,6 +33,14 @@ class DrawerViewModel( ViewModel { init { + viewModelScope.launch { + getDrawerConfig().collectLatest { config -> + updateState { + it.copy(config = config) + } + } + } + viewModelScope.launch { loadAccounts() } @@ -45,38 +57,45 @@ class DrawerViewModel( } private fun updateAccounts(accounts: List) { - val currentAccountUuid = state.value.currentAccount?.account?.uuid - val isCurrentAccountAvailable = accounts.any { currentAccountUuid == it.account.uuid } + val selectedAccount = accounts.find { it.account.uuid == state.value.selectedAccount?.account?.uuid } + ?: accounts.firstOrNull() updateState { - if (isCurrentAccountAvailable) { - it.copy(accounts = accounts.toImmutableList()) - } else { - it.copy( - accounts = accounts.toImmutableList(), - currentAccount = accounts.firstOrNull(), - ) - } + it.copy( + accounts = accounts.toImmutableList(), + selectedAccount = selectedAccount, + ) } } @OptIn(ExperimentalCoroutinesApi::class) private suspend fun loadFolders() { - state.mapNotNull { it.currentAccount?.account?.uuid } + state.mapNotNull { it.selectedAccount?.account?.uuid } .distinctUntilChanged() .flatMapLatest { accountUuid -> getDisplayFoldersForAccount(accountUuid) }.collectLatest { folders -> - updateState { - it.copy(folders = folders.toImmutableList()) - } + updateFolders(folders) } } + private fun updateFolders(displayFolders: List) { + val selectedFolder = displayFolders.find { it == state.value.selectedFolder } + ?: displayFolders.firstOrNull() + + updateState { + it.copy( + folders = displayFolders.toImmutableList(), + selectedFolder = selectedFolder, + ) + } + } + override fun event(event: Event) { when (event) { Event.OnRefresh -> refresh() is Event.OnAccountClick -> selectAccount(event.account) + is Event.OnFolderClick -> selectFolder(event.folder) is Event.OnAccountViewClick -> { selectAccount( state.value.accounts.nextOrFirst(event.account)!!, @@ -89,10 +108,12 @@ class DrawerViewModel( viewModelScope.launch { updateState { it.copy( - currentAccount = account, + selectedAccount = account, ) } } + + emitEffect(Effect.OpenAccount(account.account)) } private fun ImmutableList.nextOrFirst(account: DisplayAccount): DisplayAccount? { @@ -106,17 +127,25 @@ class DrawerViewModel( } } - private fun refresh() { - if (state.value.isLoading) { - return + private fun selectFolder(folder: DisplayAccountFolder) { + updateState { + it.copy(selectedFolder = folder) } + emitEffect(Effect.OpenFolder(folder.folder.id)) + + viewModelScope.launch { + delay(DRAWER_CLOSE_DELAY) + emitEffect(Effect.CloseDrawer) + } + } + + private fun refresh() { viewModelScope.launch { updateState { it.copy(isLoading = true) } - // TODO: replace with actual data loading - delay(500) + syncMail(state.value.selectedAccount?.account).collect() updateState { it.copy(isLoading = false) @@ -124,3 +153,9 @@ class DrawerViewModel( } } } + +/** + * Delay before closing the drawer to avoid the drawer being closed immediately and give time + * for the ripple effect to finish. + */ +private const val DRAWER_CLOSE_DELAY = 250L diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/folder/FolderList.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/folder/FolderList.kt index cfbe88d7c9..7e26419f7b 100644 --- a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/folder/FolderList.kt +++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/folder/FolderList.kt @@ -5,14 +5,14 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import app.k9mail.legacy.ui.folder.DisplayFolder +import app.k9mail.feature.navigation.drawer.domain.entity.DisplayAccountFolder import kotlinx.collections.immutable.ImmutableList @Composable fun FolderList( - folders: ImmutableList, - selectedFolder: DisplayFolder?, - onFolderClick: (DisplayFolder) -> Unit, + folders: ImmutableList, + selectedFolder: DisplayAccountFolder?, + onFolderClick: (DisplayAccountFolder) -> Unit, showStarredCount: Boolean, modifier: Modifier = Modifier, ) { diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/folder/FolderListItem.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/folder/FolderListItem.kt index c9019e2379..7090599fa6 100644 --- a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/folder/FolderListItem.kt +++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/folder/FolderListItem.kt @@ -7,14 +7,14 @@ import app.k9mail.core.mail.folder.api.FolderType import app.k9mail.core.ui.compose.designsystem.atom.icon.Icon import app.k9mail.core.ui.compose.designsystem.atom.icon.Icons import app.k9mail.core.ui.compose.designsystem.organism.drawer.NavigationDrawerItem -import app.k9mail.legacy.ui.folder.DisplayFolder +import app.k9mail.feature.navigation.drawer.domain.entity.DisplayAccountFolder @Composable fun FolderListItem( - displayFolder: DisplayFolder, + displayFolder: DisplayAccountFolder, selected: Boolean, showStarredCount: Boolean, - onClick: (DisplayFolder) -> Unit, + onClick: (DisplayAccountFolder) -> Unit, modifier: Modifier = Modifier, ) { NavigationDrawerItem( diff --git a/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/GetDrawerConfigTest.kt b/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/GetDrawerConfigTest.kt new file mode 100644 index 0000000000..60cb138c81 --- /dev/null +++ b/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/GetDrawerConfigTest.kt @@ -0,0 +1,32 @@ +package app.k9mail.feature.navigation.drawer.domain.usecase + +import app.k9mail.feature.navigation.drawer.domain.entity.DrawerConfig +import assertk.assertThat +import assertk.assertions.isEqualTo +import kotlin.test.Test +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest + +class GetDrawerConfigTest { + + @Test + fun `should get drawer config`() = runTest { + val drawerConfig = DrawerConfig( + showUnifiedInbox = true, + showStarredCount = true, + ) + + val testSubject = GetDrawerConfig( + configProver = { drawerConfig }, + ) + + val result = testSubject().first() + + assertThat(result).isEqualTo( + DrawerConfig( + showUnifiedInbox = true, + showStarredCount = true, + ), + ) + } +} diff --git a/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/SyncMailTest.kt b/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/SyncMailTest.kt new file mode 100644 index 0000000000..66295504ed --- /dev/null +++ b/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/SyncMailTest.kt @@ -0,0 +1,43 @@ +package app.k9mail.feature.navigation.drawer.domain.usecase + +import app.k9mail.legacy.account.Account +import app.k9mail.legacy.message.controller.MessagingControllerMailChecker +import app.k9mail.legacy.message.controller.MessagingListener +import assertk.assertThat +import assertk.assertions.isEqualTo +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class SyncMailTest { + + @Test + fun `should sync mail`() = runTest { + val listenerExecutor: (MessagingListener?) -> Unit = { listener -> + listener?.checkMailFinished(null, null) + } + val testSubject = SyncMail( + messagingController = FakeMessagingControllerMailChecker( + listenerExecutor = listenerExecutor, + ), + ) + + val result = testSubject(null).first() + + assertThat(result.isSuccess).isEqualTo(true) + } + + private class FakeMessagingControllerMailChecker( + private val listenerExecutor: (MessagingListener?) -> Unit = {}, + ) : MessagingControllerMailChecker { + override fun checkMail( + account: Account?, + ignoreLastCheckedTime: Boolean, + useManualWakeLock: Boolean, + notify: Boolean, + listener: MessagingListener?, + ) { + listenerExecutor(listener) + } + } +} diff --git a/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerStateTest.kt b/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerStateTest.kt new file mode 100644 index 0000000000..93f88e09d9 --- /dev/null +++ b/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerStateTest.kt @@ -0,0 +1,30 @@ +package app.k9mail.feature.navigation.drawer.ui + +import app.k9mail.feature.navigation.drawer.domain.entity.DrawerConfig +import app.k9mail.feature.navigation.drawer.ui.DrawerContract.State +import assertk.assertThat +import assertk.assertions.isEqualTo +import kotlinx.collections.immutable.persistentListOf +import org.junit.Test + +class DrawerStateTest { + + @Test + fun `should set default values`() { + val state = State() + + assertThat(state).isEqualTo( + State( + config = DrawerConfig( + showUnifiedInbox = false, + showStarredCount = false, + ), + accounts = persistentListOf(), + selectedAccount = null, + folders = persistentListOf(), + selectedFolder = null, + isLoading = false, + ), + ) + } +} diff --git a/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewKtTest.kt b/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewKtTest.kt index 82c0c76c8a..41c82b3215 100644 --- a/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewKtTest.kt +++ b/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewKtTest.kt @@ -5,12 +5,53 @@ import androidx.compose.ui.test.printToString import app.k9mail.core.ui.compose.testing.ComposeTest import app.k9mail.core.ui.compose.testing.onNodeWithTag import app.k9mail.core.ui.compose.testing.setContentWithTheme +import app.k9mail.feature.navigation.drawer.ui.DrawerContract.Effect import app.k9mail.feature.navigation.drawer.ui.DrawerContract.State +import assertk.assertThat +import assertk.assertions.isEqualTo import kotlin.test.Test import kotlinx.coroutines.test.runTest class DrawerViewKtTest : ComposeTest() { + @Test + fun `should delegate effects`() = runTest { + val initialState = State() + val viewModel = FakeDrawerViewModel(initialState) + var openAccountCounter = 0 + var openFolderCounter = 0 + var closeDrawerCounter = 0 + + setContentWithTheme { + DrawerView( + openAccount = { openAccountCounter++ }, + openFolder = { openFolderCounter++ }, + closeDrawer = { closeDrawerCounter++ }, + viewModel = viewModel, + ) + } + + assertThat(openAccountCounter).isEqualTo(0) + assertThat(openFolderCounter).isEqualTo(0) + assertThat(closeDrawerCounter).isEqualTo(0) + + viewModel.effect(Effect.OpenAccount(FakeData.ACCOUNT)) + + assertThat(openAccountCounter).isEqualTo(1) + + viewModel.effect(Effect.OpenFolder(1)) + + assertThat(openAccountCounter).isEqualTo(1) + assertThat(openFolderCounter).isEqualTo(1) + assertThat(closeDrawerCounter).isEqualTo(0) + + viewModel.effect(Effect.CloseDrawer) + + assertThat(openAccountCounter).isEqualTo(1) + assertThat(openFolderCounter).isEqualTo(1) + assertThat(closeDrawerCounter).isEqualTo(1) + } + @Test fun `pull refresh should listen to view model state`() = runTest { val initialState = State( @@ -20,6 +61,9 @@ class DrawerViewKtTest : ComposeTest() { setContentWithTheme { DrawerView( + openAccount = {}, + openFolder = {}, + closeDrawer = {}, viewModel = viewModel, ) } 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 e4fdece880..30bbd1a4d1 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 @@ -3,17 +3,23 @@ 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.assertThatAndEffectTurbineConsumed import app.k9mail.core.ui.compose.testing.mvi.eventStateTest +import app.k9mail.core.ui.compose.testing.mvi.turbinesWithInitialStateCheck import app.k9mail.feature.navigation.drawer.domain.entity.DisplayAccount +import app.k9mail.feature.navigation.drawer.domain.entity.DisplayAccountFolder +import app.k9mail.feature.navigation.drawer.domain.entity.DrawerConfig +import app.k9mail.feature.navigation.drawer.ui.DrawerContract.Effect import app.k9mail.feature.navigation.drawer.ui.DrawerContract.Event import app.k9mail.feature.navigation.drawer.ui.DrawerContract.State import app.k9mail.legacy.account.Account import app.k9mail.legacy.account.Identity -import app.k9mail.legacy.ui.folder.DisplayFolder import assertk.assertThat import assertk.assertions.isEqualTo import kotlin.test.Test +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flow @@ -27,9 +33,35 @@ class DrawerViewModelTest { @get:Rule val mainDispatcherRule = MainDispatcherRule() + @Test + fun `should collect drawer config`() = runTest { + val drawerConfig = createDrawerConfig() + val getDrawerConfigFlow = MutableStateFlow(drawerConfig) + val testSubject = createTestSubject( + drawerConfigFlow = getDrawerConfigFlow, + ) + + advanceUntilIdle() + + assertThat(testSubject.state.value.config).isEqualTo(drawerConfig) + + val newDrawerConfig = createDrawerConfig(showUnifiedInbox = true) + + getDrawerConfigFlow.emit(newDrawerConfig) + + advanceUntilIdle() + + assertThat(testSubject.state.value.config).isEqualTo(newDrawerConfig) + } + @Test fun `should change loading state when OnRefresh event is received`() = runTest { - val testSubject = createTestSubject() + val testSubject = createTestSubject( + syncMailFlow = flow { + delay(25) + emit(Result.success(Unit)) + }, + ) eventStateTest( viewModel = testSubject, @@ -45,8 +77,8 @@ class DrawerViewModelTest { } @Test - fun `should collect display accounts when created and select first as current`() = runTest { - val displayAccounts = createDisplayAccountList(3) + fun `should collect display accounts when created and select first as selected`() = runTest { + val displayAccounts = createDisplayAccountList(2) val getDisplayAccountsFlow = MutableStateFlow(displayAccounts) val testSubject = createTestSubject( displayAccountsFlow = getDisplayAccountsFlow, @@ -56,11 +88,11 @@ class DrawerViewModelTest { assertThat(testSubject.state.value.accounts.size).isEqualTo(displayAccounts.size) assertThat(testSubject.state.value.accounts).isEqualTo(displayAccounts) - assertThat(testSubject.state.value.currentAccount).isEqualTo(displayAccounts.first()) + assertThat(testSubject.state.value.selectedAccount).isEqualTo(displayAccounts.first()) } @Test - fun `should reselect current account when old not present anymore`() = runTest { + fun `should reselect selected account when old not present anymore`() = runTest { val displayAccounts = createDisplayAccountList(3) val getDisplayAccountsFlow = MutableStateFlow(displayAccounts) val testSubject = createTestSubject( @@ -76,11 +108,11 @@ class DrawerViewModelTest { assertThat(testSubject.state.value.accounts.size).isEqualTo(newDisplayAccounts.size) assertThat(testSubject.state.value.accounts).isEqualTo(newDisplayAccounts) - assertThat(testSubject.state.value.currentAccount).isEqualTo(newDisplayAccounts.first()) + assertThat(testSubject.state.value.selectedAccount).isEqualTo(newDisplayAccounts.first()) } @Test - fun `should set current account to null when no accounts are present`() = runTest { + fun `should set selected account to null when no accounts are present`() = runTest { val getDisplayAccountsFlow = MutableStateFlow(emptyList()) val testSubject = createTestSubject( displayAccountsFlow = getDisplayAccountsFlow, @@ -89,16 +121,23 @@ class DrawerViewModelTest { advanceUntilIdle() assertThat(testSubject.state.value.accounts.size).isEqualTo(0) - assertThat(testSubject.state.value.currentAccount).isEqualTo(null) + assertThat(testSubject.state.value.selectedAccount).isEqualTo(null) } @Test - fun `should set current account when OnAccountClick event is received`() = runTest { + fun `should set selected account when OnAccountClick event is received`() = runTest { val displayAccounts = createDisplayAccountList(3) val getDisplayAccountsFlow = MutableStateFlow(displayAccounts) val testSubject = createTestSubject( displayAccountsFlow = getDisplayAccountsFlow, ) + val turbines = turbinesWithInitialStateCheck( + testSubject, + State( + accounts = displayAccounts.toImmutableList(), + selectedAccount = displayAccounts.first(), + ), + ) advanceUntilIdle() @@ -106,11 +145,15 @@ class DrawerViewModelTest { advanceUntilIdle() - assertThat(testSubject.state.value.currentAccount).isEqualTo(displayAccounts[1]) + assertThat(turbines.awaitStateItem().selectedAccount).isEqualTo(displayAccounts[1]) + + turbines.assertThatAndEffectTurbineConsumed { + isEqualTo(Effect.OpenAccount(displayAccounts[1].account)) + } } @Test - fun `should collect display folders for current account`() = runTest { + fun `should collect display folders for selected account`() = runTest { val displayAccounts = createDisplayAccountList(3) val getDisplayAccountsFlow = MutableStateFlow(displayAccounts) val displayFoldersMap = mapOf( @@ -129,7 +172,7 @@ class DrawerViewModelTest { } @Test - fun `should collect display folders when current account is changed`() = runTest { + fun `should collect display folders when selected account is changed`() = runTest { val displayAccounts = createDisplayAccountList(3) val getDisplayAccountsFlow = MutableStateFlow(displayAccounts) val displayFoldersMap = mapOf( @@ -153,15 +196,62 @@ class DrawerViewModelTest { assertThat(testSubject.state.value.folders).isEqualTo(displayFolders) } + @Test + fun `should set selected folder when OnFolderClick event is received`() = runTest { + val displayAccounts = createDisplayAccountList(3) + val getDisplayAccountsFlow = MutableStateFlow(displayAccounts) + val displayFoldersMap = mapOf( + displayAccounts[0].account.uuid to createDisplayFolderList(3), + ) + val initialState = State( + accounts = displayAccounts.toImmutableList(), + selectedAccount = displayAccounts[0], + folders = displayFoldersMap[displayAccounts[0].account.uuid]!!.toImmutableList(), + selectedFolder = displayFoldersMap[displayAccounts[0].account.uuid]!![0], + ) + val testSubject = createTestSubject( + displayAccountsFlow = getDisplayAccountsFlow, + displayFoldersMap = displayFoldersMap, + ) + val turbines = turbinesWithInitialStateCheck(testSubject, initialState) + + advanceUntilIdle() + + val displayFolders = displayFoldersMap[displayAccounts[0].account.uuid] ?: emptyList() + testSubject.event(Event.OnFolderClick(displayFolders[1])) + + assertThat(turbines.awaitStateItem().selectedFolder).isEqualTo(displayFolders[1]) + + assertThat(turbines.awaitEffectItem()).isEqualTo(Effect.OpenFolder(displayFolders[1].folder.id)) + + turbines.assertThatAndEffectTurbineConsumed { + isEqualTo(Effect.CloseDrawer) + } + } + private fun createTestSubject( + drawerConfigFlow: Flow = flow { emit(createDrawerConfig()) }, displayAccountsFlow: Flow> = flow { emit(emptyList()) }, - displayFoldersMap: Map> = emptyMap(), + displayFoldersMap: Map> = emptyMap(), + syncMailFlow: Flow> = flow { emit(Result.success(Unit)) }, ): DrawerViewModel { return DrawerViewModel( + getDrawerConfig = { drawerConfigFlow }, getDisplayAccounts = { displayAccountsFlow }, getDisplayFoldersForAccount = { accountUuid -> flow { emit(displayFoldersMap[accountUuid] ?: emptyList()) } }, + syncMail = { syncMailFlow }, + ) + } + + private fun createDrawerConfig( + showUnifiedInbox: Boolean = false, + showStarredCount: Boolean = false, + ): DrawerConfig { + return DrawerConfig( + showUnifiedInbox = showUnifiedInbox, + showStarredCount = showStarredCount, ) } @@ -204,12 +294,13 @@ class DrawerViewModelTest { } private fun createDisplayFolder( + accountUuid: String = "uuid", id: Long = 1234, name: String = "name", type: FolderType = FolderType.REGULAR, unreadCount: Int = 0, starredCount: Int = 0, - ): DisplayFolder { + ): DisplayAccountFolder { val folder = Folder( id = id, name = name, @@ -217,7 +308,8 @@ class DrawerViewModelTest { isLocalOnly = false, ) - return DisplayFolder( + return DisplayAccountFolder( + accountUuid = accountUuid, folder = folder, isInTopGroup = false, unreadMessageCount = unreadCount, @@ -225,7 +317,7 @@ class DrawerViewModelTest { ) } - private fun createDisplayFolderList(count: Int): List { + private fun createDisplayFolderList(count: Int): List { return List(count) { index -> createDisplayFolder( id = index.toLong() + 100, diff --git a/legacy/common/build.gradle.kts b/legacy/common/build.gradle.kts index 9eaf6c8c6c..2d980ed0de 100644 --- a/legacy/common/build.gradle.kts +++ b/legacy/common/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { implementation(projects.feature.account.setup) implementation(projects.feature.account.edit) + implementation(projects.feature.navigation.drawer) implementation(projects.feature.settings.import) implementation(projects.feature.widget.unread) diff --git a/legacy/common/src/main/java/com/fsck/k9/feature/FeatureModule.kt b/legacy/common/src/main/java/com/fsck/k9/feature/FeatureModule.kt index d9e2e7c222..94e259d0b2 100644 --- a/legacy/common/src/main/java/com/fsck/k9/feature/FeatureModule.kt +++ b/legacy/common/src/main/java/com/fsck/k9/feature/FeatureModule.kt @@ -1,6 +1,7 @@ package com.fsck.k9.feature import app.k9mail.feature.launcher.FeatureLauncherExternalContract +import app.k9mail.feature.navigation.drawer.NavigationDrawerExternalContract import org.koin.android.ext.koin.androidContext import org.koin.dsl.module @@ -10,4 +11,8 @@ val featureModule = module { context = androidContext(), ) } + + single { + NavigationDrawerConfigLoader() + } } diff --git a/legacy/common/src/main/java/com/fsck/k9/feature/NavigationDrawerConfigLoader.kt b/legacy/common/src/main/java/com/fsck/k9/feature/NavigationDrawerConfigLoader.kt new file mode 100644 index 0000000000..fb8fb70ff6 --- /dev/null +++ b/legacy/common/src/main/java/com/fsck/k9/feature/NavigationDrawerConfigLoader.kt @@ -0,0 +1,14 @@ +package com.fsck.k9.feature + +import app.k9mail.feature.navigation.drawer.NavigationDrawerExternalContract.DrawerConfigLoader +import app.k9mail.feature.navigation.drawer.domain.entity.DrawerConfig +import com.fsck.k9.K9 + +class NavigationDrawerConfigLoader : DrawerConfigLoader { + override fun loadDrawerConfig(): DrawerConfig { + return DrawerConfig( + showUnifiedInbox = K9.isShowUnifiedInbox, + showStarredCount = K9.isShowStarredCount, + ) + } +} diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt index 1a269b058c..986179682c 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt @@ -608,6 +608,9 @@ open class MessageList : private fun initializeFolderDrawer() { navigationDrawer = FolderDrawer( parent = this, + openAccount = { account -> openRealAccount(account) }, + openFolder = { folderId -> openFolder(folderId) }, + createDrawerListener = { createDrawerListener() }, ) } @@ -630,7 +633,7 @@ open class MessageList : } } - fun openFolder(folderId: Long) { + private fun openFolder(folderId: Long) { if (displayMode == DisplayMode.SPLIT_VIEW) { removeMessageViewContainerFragment() showMessageViewPlaceHolder()