0
0
mirror of https://github.com/thunderbird/thunderbird-android.git synced 2024-09-19 19:52:14 +02:00

Merge pull request #8143 from wmontwe/add-drawer-folder-list-part4

Add drawer folder list - part 4
This commit is contained in:
Wolf-Martell Montwé 2024-09-17 13:09:32 +00:00 committed by GitHub
commit 41c06cebbb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 523 additions and 66 deletions

View File

@ -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 = {},
)

View File

@ -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,

View File

@ -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() },
)
}
}
}

View File

@ -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
}
}

View File

@ -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<UseCase.GetDrawerConfig> {
GetDrawerConfig(
configProver = get(),
)
}
single<UseCase.GetDisplayAccounts> {
GetDisplayAccounts(
accountManager = get(),
@ -28,6 +36,12 @@ val navigationDrawerModule: Module = module {
)
}
single<UseCase.SyncMail> {
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(),
)
}
}

View File

@ -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<DrawerConfig>
}
fun interface GetDisplayAccounts {
operator fun invoke(): Flow<List<DisplayAccount>>
}
fun interface GetDisplayFoldersForAccount {
operator fun invoke(accountUuid: String): Flow<List<DisplayFolder>>
operator fun invoke(accountUuid: String): Flow<List<DisplayAccountFolder>>
}
/**
* 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<Result<Unit>>
}
}
}

View File

@ -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,
)

View File

@ -0,0 +1,6 @@
package app.k9mail.feature.navigation.drawer.domain.entity
data class DrawerConfig(
val showUnifiedInbox: Boolean,
val showStarredCount: Boolean,
)

View File

@ -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<List<DisplayFolder>> {
return repository.getDisplayFoldersFlow(accountUuid)
override fun invoke(accountUuid: String): Flow<List<DisplayAccountFolder>> {
return repository.getDisplayFoldersFlow(accountUuid).map { displayFolders ->
displayFolders.map { displayFolder ->
DisplayAccountFolder(
accountUuid = accountUuid,
folder = displayFolder.folder,
isInTopGroup = displayFolder.isInTopGroup,
unreadMessageCount = displayFolder.unreadMessageCount,
starredMessageCount = displayFolder.starredMessageCount,
)
}
}
}
}

View File

@ -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<DrawerConfig> {
// TODO This needs to be updated when the config changes
return flow {
emit(configProver.loadDrawerConfig())
}
}
}

View File

@ -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<Result<Unit>> = 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)
}

View File

@ -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,
)
}
}

View File

@ -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<DisplayAccount> = persistentListOf(),
val folders: ImmutableList<DisplayFolder> = persistentListOf(),
val showStarredCount: Boolean = false,
val selectedAccount: DisplayAccount? = null,
val folders: ImmutableList<DisplayAccountFolder> = 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
}
}

View File

@ -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<DrawerViewModel>(),
) {
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,

View File

@ -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<State, Event, Effect>(
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<DisplayAccount>) {
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<DisplayAccountFolder>) {
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<DisplayAccount>.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

View File

@ -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<DisplayFolder>,
selectedFolder: DisplayFolder?,
onFolderClick: (DisplayFolder) -> Unit,
folders: ImmutableList<DisplayAccountFolder>,
selectedFolder: DisplayAccountFolder?,
onFolderClick: (DisplayAccountFolder) -> Unit,
showStarredCount: Boolean,
modifier: Modifier = Modifier,
) {

View File

@ -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(

View File

@ -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,
),
)
}
}

View File

@ -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)
}
}
}

View File

@ -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,
),
)
}
}

View File

@ -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,
)
}

View File

@ -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<DisplayAccount>())
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<DrawerConfig> = flow { emit(createDrawerConfig()) },
displayAccountsFlow: Flow<List<DisplayAccount>> = flow { emit(emptyList()) },
displayFoldersMap: Map<String, List<DisplayFolder>> = emptyMap(),
displayFoldersMap: Map<String, List<DisplayAccountFolder>> = emptyMap(),
syncMailFlow: Flow<Result<Unit>> = 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<DisplayFolder> {
private fun createDisplayFolderList(count: Int): List<DisplayAccountFolder> {
return List(count) { index ->
createDisplayFolder(
id = index.toLong() + 100,

View File

@ -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)

View File

@ -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<NavigationDrawerExternalContract.DrawerConfigLoader> {
NavigationDrawerConfigLoader()
}
}

View File

@ -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,
)
}
}

View File

@ -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()