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:
commit
41c06cebbb
@ -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 = {},
|
||||
)
|
||||
|
@ -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,
|
||||
|
@ -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() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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>>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
)
|
@ -0,0 +1,6 @@
|
||||
package app.k9mail.feature.navigation.drawer.domain.entity
|
||||
|
||||
data class DrawerConfig(
|
||||
val showUnifiedInbox: Boolean,
|
||||
val showStarredCount: Boolean,
|
||||
)
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
) {
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user