From be4ba7ca2777200b91593c81e10985bc5631e6ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolf-Martell=20Montw=C3=A9?= Date: Thu, 12 Sep 2024 11:53:32 +0200 Subject: [PATCH 1/7] Add GetDisplayFoldersForAccount usecase --- .../navigation/drawer/NavigationDrawerModule.kt | 7 +++++++ .../navigation/drawer/domain/DomainContract.kt | 5 +++++ .../domain/usecase/GetDisplayFoldersForAccount.kt | 14 ++++++++++++++ 3 files changed, 26 insertions(+) create mode 100644 feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/GetDisplayFoldersForAccount.kt 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 edb1f961952..428809208b3 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 @@ -2,6 +2,7 @@ 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.legacy.AccountsViewModel import app.k9mail.feature.navigation.drawer.legacy.FoldersViewModel import app.k9mail.feature.navigation.drawer.ui.DrawerViewModel @@ -21,6 +22,12 @@ val navigationDrawerModule: Module = module { ) } + single { + GetDisplayFoldersForAccount( + repository = get(), + ) + } + viewModel { AccountsViewModel( getDisplayAccounts = 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 dec8f13f825..468de79c149 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,6 +1,7 @@ package app.k9mail.feature.navigation.drawer.domain import app.k9mail.feature.navigation.drawer.domain.entity.DisplayAccount +import app.k9mail.legacy.ui.folder.DisplayFolder import kotlinx.coroutines.flow.Flow interface DomainContract { @@ -9,5 +10,9 @@ interface DomainContract { fun interface GetDisplayAccounts { operator fun invoke(): Flow> } + + fun interface GetDisplayFoldersForAccount { + operator fun invoke(accountUuid: String): Flow> + } } } 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 new file mode 100644 index 00000000000..a25c7b5033a --- /dev/null +++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/domain/usecase/GetDisplayFoldersForAccount.kt @@ -0,0 +1,14 @@ +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.legacy.ui.folder.DisplayFolderRepository +import kotlinx.coroutines.flow.Flow + +class GetDisplayFoldersForAccount( + private val repository: DisplayFolderRepository, +) : UseCase.GetDisplayFoldersForAccount { + override fun invoke(accountUuid: String): Flow> { + return repository.getDisplayFoldersFlow(accountUuid) + } +} From 74653a7bd87bdd2fdaaad81c575ebc4b053cc942 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolf-Martell=20Montw=C3=A9?= Date: Thu, 12 Sep 2024 12:49:47 +0200 Subject: [PATCH 2/7] Add optional icon to NavigationDrawerItemBadge --- .../NavigationDrawerItemBadgePreview.kt | 14 +++++++- .../drawer/NavigationDrawerItemBadge.kt | 32 +++++++++++++++++-- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/core/ui/compose/designsystem/src/debug/kotlin/app/k9mail/core/ui/compose/designsystem/organism/drawer/NavigationDrawerItemBadgePreview.kt b/core/ui/compose/designsystem/src/debug/kotlin/app/k9mail/core/ui/compose/designsystem/organism/drawer/NavigationDrawerItemBadgePreview.kt index 7cd4ec5c7c9..071ce35fe5a 100644 --- a/core/ui/compose/designsystem/src/debug/kotlin/app/k9mail/core/ui/compose/designsystem/organism/drawer/NavigationDrawerItemBadgePreview.kt +++ b/core/ui/compose/designsystem/src/debug/kotlin/app/k9mail/core/ui/compose/designsystem/organism/drawer/NavigationDrawerItemBadgePreview.kt @@ -3,13 +3,25 @@ package app.k9mail.core.ui.compose.designsystem.organism.drawer import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview import app.k9mail.core.ui.compose.designsystem.PreviewWithThemes +import app.k9mail.core.ui.compose.designsystem.atom.icon.Icons @Composable @Preview(showBackground = true) internal fun NavigationDrawerItemBadgePreview() { PreviewWithThemes { NavigationDrawerItemBadge( - label = "100+", + label = "99+", + ) + } +} + +@Composable +@Preview(showBackground = true) +internal fun NavigationDrawerItemBadgeWithIconPreview() { + PreviewWithThemes { + NavigationDrawerItemBadge( + label = "99+", + imageVector = Icons.Outlined.Info, ) } } diff --git a/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/organism/drawer/NavigationDrawerItemBadge.kt b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/organism/drawer/NavigationDrawerItemBadge.kt index 97b2682fa23..d4c331080b5 100644 --- a/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/organism/drawer/NavigationDrawerItemBadge.kt +++ b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/organism/drawer/NavigationDrawerItemBadge.kt @@ -1,16 +1,42 @@ package app.k9mail.core.ui.compose.designsystem.organism.drawer +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import app.k9mail.core.ui.compose.designsystem.atom.icon.Icon import app.k9mail.core.ui.compose.designsystem.atom.text.TextLabelLarge +import app.k9mail.core.ui.compose.theme2.MainTheme +/** + * A badge for a navigation drawer item with an optional icon. + * + * @param label The label to display. + * @param modifier The modifier to apply. + * @param imageVector The image vector to display (optional). + */ @Composable fun NavigationDrawerItemBadge( label: String, modifier: Modifier = Modifier, + imageVector: ImageVector? = null, ) { - TextLabelLarge( - text = label, + Row( modifier = modifier, - ) + verticalAlignment = Alignment.CenterVertically, + ) { + if (imageVector != null) { + Icon( + imageVector = imageVector, + modifier = Modifier.size(MainTheme.sizes.iconSmall) + .padding(end = MainTheme.spacings.quarter), + ) + } + TextLabelLarge( + text = label, + ) + } } From f07ba6fced7b1669bd4b2fdd0d7c94819ed9a1b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolf-Martell=20Montw=C3=A9?= Date: Thu, 12 Sep 2024 12:47:54 +0200 Subject: [PATCH 3/7] Add Dot and Star icons --- .../ui/catalog/ui/atom/items/IconItems.kt | 27 +++++++++++++++++-- .../compose/designsystem/atom/icon/Icons.kt | 12 +++++++-- .../designsystem/atom/icon/filled/Dot.kt | 27 +++++++++++++++++++ .../core/ui/compose/theme2/ThemeSizes.kt | 3 ++- .../theme2/default/DefaultThemeSizes.kt | 3 ++- .../permissions/ui/PermissionBox.kt | 2 +- 6 files changed, 67 insertions(+), 7 deletions(-) create mode 100644 core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/icon/filled/Dot.kt diff --git a/app-ui-catalog/src/main/java/app/k9mail/ui/catalog/ui/atom/items/IconItems.kt b/app-ui-catalog/src/main/java/app/k9mail/ui/catalog/ui/atom/items/IconItems.kt index 240fc813639..bd508fe6e57 100644 --- a/app-ui-catalog/src/main/java/app/k9mail/ui/catalog/ui/atom/items/IconItems.kt +++ b/app-ui-catalog/src/main/java/app/k9mail/ui/catalog/ui/atom/items/IconItems.kt @@ -3,6 +3,7 @@ package app.k9mail.ui.catalog.ui.atom.items import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.grid.LazyGridScope import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -24,6 +25,28 @@ fun LazyGridScope.iconItems() { sectionHeaderItem( text = "Compose Icons", ) + sectionSubtitleItem(text = "Sizes") + defaultItem { + IconItem( + name = "Small", + imageVector = Icons.Outlined.Info, + modifier = Modifier.size(MainTheme.sizes.iconSmall), + ) + } + defaultItem { + IconItem( + name = "Default", + imageVector = Icons.Outlined.Info, + modifier = Modifier.size(MainTheme.sizes.icon), + ) + } + defaultItem { + IconItem( + name = "Large", + imageVector = Icons.Outlined.Info, + modifier = Modifier.size(MainTheme.sizes.iconLarge), + ) + } sectionSubtitleItem(text = "Filled") getIconsFor(Icons.Filled) sectionSubtitleItem(text = "Outlined") @@ -81,13 +104,13 @@ private fun IconItem( ) { Column( modifier = Modifier - .padding(defaultItemPadding()) - .then(modifier), + .padding(defaultItemPadding()), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.default), ) { Icon( imageVector = imageVector, + modifier = modifier, ) TextBodySmall(text = name) } diff --git a/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/icon/Icons.kt b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/icon/Icons.kt index 2bd04f6b54e..ba8173f3dfc 100644 --- a/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/icon/Icons.kt +++ b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/icon/Icons.kt @@ -4,6 +4,7 @@ import androidx.compose.material.icons.automirrored.outlined.ArrowBack import androidx.compose.material.icons.filled.Cancel import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Outbox +import androidx.compose.material.icons.filled.Star import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material.icons.outlined.AccountCircle import androidx.compose.material.icons.outlined.Check @@ -16,16 +17,23 @@ import androidx.compose.material.icons.outlined.Menu import androidx.compose.material.icons.outlined.Security import androidx.compose.material.icons.outlined.Visibility import androidx.compose.ui.graphics.vector.ImageVector +import app.k9mail.core.ui.compose.designsystem.atom.icon.filled.Dot import androidx.compose.material.icons.Icons as MaterialIcons // We're using getters so not all icons are loaded into memory as soon as one of the nested objects is accessed. object Icons { object Filled { + val Cancel: ImageVector + get() = MaterialIcons.Filled.Cancel + val CheckCircle: ImageVector get() = MaterialIcons.Filled.CheckCircle - val Cancel: ImageVector - get() = MaterialIcons.Filled.Cancel + val Dot: ImageVector + get() = MaterialIcons.Filled.Dot + + val Star: ImageVector + get() = MaterialIcons.Filled.Star } object Outlined { diff --git a/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/icon/filled/Dot.kt b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/icon/filled/Dot.kt new file mode 100644 index 00000000000..cc85e7bf1cc --- /dev/null +++ b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/icon/filled/Dot.kt @@ -0,0 +1,27 @@ +package app.k9mail.core.ui.compose.designsystem.atom.icon.filled + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.materialIcon +import androidx.compose.material.icons.materialPath +import androidx.compose.ui.graphics.vector.ImageVector + +@Suppress("MagicNumber") +val Icons.Filled.Dot: ImageVector + get() { + if (instance != null) { + return instance!! + } + instance = materialIcon(name = "Filled.Dot") { + materialPath { + moveTo(12.0f, 6.0f) + curveToRelative(-3.31f, 0.0f, -6.0f, 2.69f, -6.0f, 6.0f) + reflectiveCurveToRelative(2.69f, 6.0f, 6.0f, 6.0f) + reflectiveCurveToRelative(6.0f, -2.69f, 6.0f, -6.0f) + reflectiveCurveToRelative(-2.69f, -6.0f, -6.0f, -6.0f) + close() + } + } + return instance!! + } + +private var instance: ImageVector? = null diff --git a/core/ui/compose/theme2/common/src/main/kotlin/app/k9mail/core/ui/compose/theme2/ThemeSizes.kt b/core/ui/compose/theme2/common/src/main/kotlin/app/k9mail/core/ui/compose/theme2/ThemeSizes.kt index 9ddb6140e87..6d4f80c92a3 100644 --- a/core/ui/compose/theme2/common/src/main/kotlin/app/k9mail/core/ui/compose/theme2/ThemeSizes.kt +++ b/core/ui/compose/theme2/common/src/main/kotlin/app/k9mail/core/ui/compose/theme2/ThemeSizes.kt @@ -14,8 +14,9 @@ data class ThemeSizes( val huge: Dp, val huger: Dp, + val iconSmall: Dp, val icon: Dp, - val largeIcon: Dp, + val iconLarge: Dp, val topBarHeight: Dp, val bottomBarHeight: Dp, diff --git a/core/ui/compose/theme2/common/src/main/kotlin/app/k9mail/core/ui/compose/theme2/default/DefaultThemeSizes.kt b/core/ui/compose/theme2/common/src/main/kotlin/app/k9mail/core/ui/compose/theme2/default/DefaultThemeSizes.kt index 4db563b49f6..fb84c4cfd40 100644 --- a/core/ui/compose/theme2/common/src/main/kotlin/app/k9mail/core/ui/compose/theme2/default/DefaultThemeSizes.kt +++ b/core/ui/compose/theme2/common/src/main/kotlin/app/k9mail/core/ui/compose/theme2/default/DefaultThemeSizes.kt @@ -12,8 +12,9 @@ val defaultThemeSizes = ThemeSizes( huge = 256.dp, huger = 384.dp, + iconSmall = 16.dp, icon = 24.dp, - largeIcon = 32.dp, + iconLarge = 32.dp, topBarHeight = 64.dp, bottomBarHeight = 80.dp, diff --git a/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/ui/PermissionBox.kt b/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/ui/PermissionBox.kt index f3fbed4b0fd..665a156ef2b 100644 --- a/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/ui/PermissionBox.kt +++ b/feature/onboarding/permissions/src/main/kotlin/app/k9mail/feature/onboarding/permissions/ui/PermissionBox.kt @@ -85,7 +85,7 @@ private fun IconWithPermissionStateOverlay( permissionState: UiPermissionState, ) { Box { - val iconSize = MainTheme.sizes.largeIcon + val iconSize = MainTheme.sizes.iconLarge val overlayIconSize = iconSize / 2 val overlayIconOffset = overlayIconSize / 2 val scalingFactor = iconSize / icon.image.defaultHeight From a2533e87e4ecc1633b0a572dbaa2c38d19698f88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolf-Martell=20Montw=C3=A9?= Date: Thu, 12 Sep 2024 11:52:10 +0200 Subject: [PATCH 4/7] Add FolderListItemBadge --- .../ui/folder/FolderListItemBadgePreview.kt | 101 +++++++++++++++++ .../drawer/ui/folder/FolderListItemBadge.kt | 107 ++++++++++++++++++ .../drawer/src/main/res/values/strings.xml | 2 + 3 files changed, 210 insertions(+) create mode 100644 feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/folder/FolderListItemBadgePreview.kt create mode 100644 feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/folder/FolderListItemBadge.kt diff --git a/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/folder/FolderListItemBadgePreview.kt b/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/folder/FolderListItemBadgePreview.kt new file mode 100644 index 00000000000..1574f5a29ca --- /dev/null +++ b/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/folder/FolderListItemBadgePreview.kt @@ -0,0 +1,101 @@ +package app.k9mail.feature.navigation.drawer.ui.folder + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import app.k9mail.core.ui.compose.designsystem.PreviewWithThemes + +@Composable +@Preview(showBackground = true) +internal fun FolderListItemBadgePreview() { + PreviewWithThemes { + FolderListItemBadge( + unreadCount = 99, + starredCount = 0, + showStarredCount = true, + ) + } +} + +@Composable +@Preview(showBackground = true) +internal fun FolderListItemBadgeWithStarredCountPreview() { + PreviewWithThemes { + FolderListItemBadge( + unreadCount = 99, + starredCount = 1, + showStarredCount = true, + ) + } +} + +@Composable +@Preview(showBackground = true) +internal fun FolderListItemBadgeWithZeroUnreadCountPreview() { + PreviewWithThemes { + FolderListItemBadge( + unreadCount = 0, + starredCount = 1, + showStarredCount = true, + ) + } +} + +@Composable +@Preview(showBackground = true) +internal fun FolderListItemBadgeWithZeroStarredCountPreview() { + PreviewWithThemes { + FolderListItemBadge( + unreadCount = 99, + starredCount = 0, + showStarredCount = true, + ) + } +} + +@Composable +@Preview(showBackground = true) +internal fun FolderListItemBadgeWithZeroCountsPreview() { + PreviewWithThemes { + FolderListItemBadge( + unreadCount = 0, + starredCount = 0, + showStarredCount = true, + ) + } +} + +@Composable +@Preview(showBackground = true) +internal fun FolderListItemBadgeWithoutStarredCountPreview() { + PreviewWithThemes { + FolderListItemBadge( + unreadCount = 99, + starredCount = 1, + showStarredCount = false, + ) + } +} + +@Composable +@Preview(showBackground = true) +internal fun FolderListItemBadgeWith100CountsPreview() { + PreviewWithThemes { + FolderListItemBadge( + unreadCount = 100, + starredCount = 100, + showStarredCount = true, + ) + } +} + +@Composable +@Preview(showBackground = true) +internal fun FolderListItemBadgeWith1000CountsPreview() { + PreviewWithThemes { + FolderListItemBadge( + unreadCount = 1000, + starredCount = 1000, + showStarredCount = true, + ) + } +} diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/folder/FolderListItemBadge.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/folder/FolderListItemBadge.kt new file mode 100644 index 00000000000..9fdbf8d2851 --- /dev/null +++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/folder/FolderListItemBadge.kt @@ -0,0 +1,107 @@ +package app.k9mail.feature.navigation.drawer.ui.folder + +import android.content.res.Resources +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import app.k9mail.core.ui.compose.designsystem.atom.icon.Icons +import app.k9mail.core.ui.compose.designsystem.organism.drawer.NavigationDrawerItemBadge +import app.k9mail.core.ui.compose.theme2.MainTheme +import app.k9mail.feature.navigation.drawer.R + +@Composable +fun FolderListItemBadge( + unreadCount: Int, + starredCount: Int, + showStarredCount: Boolean, + modifier: Modifier = Modifier, +) { + if (showStarredCount) { + FolderCountAndStarredBadge( + unreadCount = unreadCount, + starredCount = starredCount, + modifier = modifier, + ) + } else { + FolderCountBadge( + unreadCount = unreadCount, + modifier = modifier, + ) + } +} + +@Composable +private fun FolderCountBadge( + unreadCount: Int, + modifier: Modifier = Modifier, +) { + if (unreadCount > 0) { + val resources = LocalContext.current.resources + + NavigationDrawerItemBadge( + label = labelForCount( + count = unreadCount, + resources = resources, + ), + modifier = modifier, + ) + } +} + +@Composable +private fun FolderCountAndStarredBadge( + unreadCount: Int, + starredCount: Int, + modifier: Modifier = Modifier, +) { + if (unreadCount > 0 || starredCount > 0) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(MainTheme.spacings.default), + ) { + val resources = LocalContext.current.resources + + if (unreadCount > 0) { + NavigationDrawerItemBadge( + label = labelForCount( + count = unreadCount, + resources = resources, + ), + imageVector = Icons.Filled.Dot, + ) + } + + if (starredCount > 0) { + NavigationDrawerItemBadge( + label = labelForCount( + count = starredCount, + resources = resources, + ), + imageVector = Icons.Filled.Star, + ) + } + } + } +} + +@Suppress("MagicNumber") +private fun labelForCount( + count: Int, + resources: Resources, +) = when { + count in 1..99 -> "$count" + + count in 100..1000 -> resources.getString( + R.string.navigation_drawer_folder_item_badge_count_greater_than_99, + ) + + count > 1000 -> resources.getString( + R.string.navigation_drawer_folder_item_badge_count_greater_than_1_000, + ) + + else -> "" +} diff --git a/feature/navigation/drawer/src/main/res/values/strings.xml b/feature/navigation/drawer/src/main/res/values/strings.xml index f71f5c69b51..05163e56402 100644 --- a/feature/navigation/drawer/src/main/res/values/strings.xml +++ b/feature/navigation/drawer/src/main/res/values/strings.xml @@ -3,4 +3,6 @@ Settings Manage folders Unified Inbox + 99+ + 1k+ From 7b473d64d0bf961f8603c05e3138415f4e9b5cb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolf-Martell=20Montw=C3=A9?= Date: Wed, 11 Sep 2024 18:01:17 +0200 Subject: [PATCH 5/7] Add FolderListItem --- .../compose/designsystem/atom/icon/Icons.kt | 25 ++++++ .../drawer/ui/DrawerContentPreview.kt | 2 +- .../drawer/ui/{account => }/FakeData.kt | 19 ++++- .../drawer/ui/account/AccountViewPreview.kt | 6 +- .../drawer/ui/folder/FolderListItemPreview.kt | 76 +++++++++++++++++++ .../drawer/ui/folder/FolderListItem.kt | 51 +++++++++++++ 6 files changed, 174 insertions(+), 5 deletions(-) rename feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/{account => }/FakeData.kt (65%) create mode 100644 feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/folder/FolderListItemPreview.kt create mode 100644 feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/folder/FolderListItem.kt diff --git a/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/icon/Icons.kt b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/icon/Icons.kt index ba8173f3dfc..d4d712d9543 100644 --- a/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/icon/Icons.kt +++ b/core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/atom/icon/Icons.kt @@ -1,20 +1,27 @@ package app.k9mail.core.ui.compose.designsystem.atom.icon import androidx.compose.material.icons.automirrored.outlined.ArrowBack +import androidx.compose.material.icons.automirrored.outlined.Send import androidx.compose.material.icons.filled.Cancel import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Outbox import androidx.compose.material.icons.filled.Star import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material.icons.outlined.AccountCircle +import androidx.compose.material.icons.outlined.Archive import androidx.compose.material.icons.outlined.Check +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Drafts import androidx.compose.material.icons.outlined.ErrorOutline import androidx.compose.material.icons.outlined.ExpandLess import androidx.compose.material.icons.outlined.ExpandMore +import androidx.compose.material.icons.outlined.Folder import androidx.compose.material.icons.outlined.Inbox import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.Menu +import androidx.compose.material.icons.outlined.Report import androidx.compose.material.icons.outlined.Security +import androidx.compose.material.icons.outlined.Send import androidx.compose.material.icons.outlined.Visibility import androidx.compose.ui.graphics.vector.ImageVector import app.k9mail.core.ui.compose.designsystem.atom.icon.filled.Dot @@ -40,12 +47,21 @@ object Icons { val AccountCircle: ImageVector get() = MaterialIcons.Outlined.AccountCircle + val Archive: ImageVector + get() = MaterialIcons.Outlined.Archive + val ArrowBack: ImageVector get() = MaterialIcons.AutoMirrored.Outlined.ArrowBack val Check: ImageVector get() = MaterialIcons.Outlined.Check + val Delete: ImageVector + get() = MaterialIcons.Outlined.Delete + + val Drafts: ImageVector + get() = MaterialIcons.Outlined.Drafts + val ErrorOutline: ImageVector get() = MaterialIcons.Outlined.ErrorOutline @@ -55,6 +71,9 @@ object Icons { val ExpandLess: ImageVector get() = MaterialIcons.Outlined.ExpandLess + val Folder: ImageVector + get() = MaterialIcons.Outlined.Folder + val Inbox: ImageVector get() = MaterialIcons.Outlined.Inbox @@ -70,6 +89,12 @@ object Icons { val Security: ImageVector get() = MaterialIcons.Outlined.Security + val Send: ImageVector + get() = MaterialIcons.AutoMirrored.Outlined.Send + + val Report: ImageVector + get() = MaterialIcons.Outlined.Report + val Visibility: ImageVector get() = MaterialIcons.Outlined.Visibility 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 0d377461317..ffd065bc82f 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 @@ -3,7 +3,7 @@ package app.k9mail.feature.navigation.drawer.ui import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview import app.k9mail.core.ui.compose.designsystem.PreviewWithTheme -import app.k9mail.feature.navigation.drawer.ui.account.FakeData.DISPLAY_ACCOUNT +import app.k9mail.feature.navigation.drawer.ui.FakeData.DISPLAY_ACCOUNT @Composable @Preview(showBackground = true) diff --git a/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/account/FakeData.kt b/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/FakeData.kt similarity index 65% rename from feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/account/FakeData.kt rename to feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/FakeData.kt index 3bcbd017480..67061731396 100644 --- a/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/account/FakeData.kt +++ b/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/FakeData.kt @@ -1,8 +1,11 @@ -package app.k9mail.feature.navigation.drawer.ui.account +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.legacy.account.Account import app.k9mail.legacy.account.Identity +import app.k9mail.legacy.ui.folder.DisplayFolder internal object FakeData { @@ -34,4 +37,18 @@ internal object FakeData { unreadMessageCount = 0, starredMessageCount = 0, ) + + val FOLDER = Folder( + id = 1, + name = "Folder Name", + type = FolderType.REGULAR, + isLocalOnly = false, + ) + + val DISPLAY_FOLDER = DisplayFolder( + folder = FOLDER, + isInTopGroup = false, + unreadMessageCount = 14, + starredMessageCount = 5, + ) } diff --git a/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/account/AccountViewPreview.kt b/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/account/AccountViewPreview.kt index b268f894894..cded6eea7c3 100644 --- a/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/account/AccountViewPreview.kt +++ b/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/account/AccountViewPreview.kt @@ -3,9 +3,9 @@ package app.k9mail.feature.navigation.drawer.ui.account import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview import app.k9mail.core.ui.compose.designsystem.PreviewWithThemes -import app.k9mail.feature.navigation.drawer.ui.account.FakeData.DISPLAY_NAME -import app.k9mail.feature.navigation.drawer.ui.account.FakeData.EMAIL_ADDRESS -import app.k9mail.feature.navigation.drawer.ui.account.FakeData.LONG_TEXT +import app.k9mail.feature.navigation.drawer.ui.FakeData.DISPLAY_NAME +import app.k9mail.feature.navigation.drawer.ui.FakeData.EMAIL_ADDRESS +import app.k9mail.feature.navigation.drawer.ui.FakeData.LONG_TEXT @Composable @Preview(showBackground = true) diff --git a/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/folder/FolderListItemPreview.kt b/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/folder/FolderListItemPreview.kt new file mode 100644 index 00000000000..18835b183b2 --- /dev/null +++ b/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/folder/FolderListItemPreview.kt @@ -0,0 +1,76 @@ +package app.k9mail.feature.navigation.drawer.ui.folder + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import app.k9mail.core.mail.folder.api.FolderType +import app.k9mail.core.ui.compose.designsystem.PreviewWithThemes +import app.k9mail.feature.navigation.drawer.ui.FakeData.DISPLAY_FOLDER + +@Composable +@Preview(showBackground = true) +fun FolderListItemPreview() { + PreviewWithThemes { + FolderListItem( + displayFolder = DISPLAY_FOLDER, + selected = false, + showStarredCount = false, + onClick = {}, + ) + } +} + +@Composable +@Preview(showBackground = true) +fun FolderListItemSelectedPreview() { + PreviewWithThemes { + FolderListItem( + displayFolder = DISPLAY_FOLDER, + selected = true, + showStarredCount = false, + onClick = {}, + ) + } +} + +@Composable +@Preview(showBackground = true) +fun FolderListItemWithStarredPreview() { + PreviewWithThemes { + FolderListItem( + displayFolder = DISPLAY_FOLDER, + selected = false, + showStarredCount = true, + onClick = {}, + ) + } +} + +@Composable +@Preview(showBackground = true) +fun FolderListItemWithStarredSelectedPreview() { + PreviewWithThemes { + FolderListItem( + displayFolder = DISPLAY_FOLDER, + selected = true, + showStarredCount = true, + onClick = {}, + ) + } +} + +@Composable +@Preview(showBackground = true) +fun FolderListItemWithInboxFolderPreview() { + PreviewWithThemes { + FolderListItem( + displayFolder = DISPLAY_FOLDER.copy( + folder = DISPLAY_FOLDER.folder.copy( + type = FolderType.INBOX, + ), + ), + selected = false, + showStarredCount = true, + onClick = {}, + ) + } +} 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 new file mode 100644 index 00000000000..c9019e23791 --- /dev/null +++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/folder/FolderListItem.kt @@ -0,0 +1,51 @@ +package app.k9mail.feature.navigation.drawer.ui.folder + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +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 + +@Composable +fun FolderListItem( + displayFolder: DisplayFolder, + selected: Boolean, + showStarredCount: Boolean, + onClick: (DisplayFolder) -> Unit, + modifier: Modifier = Modifier, +) { + NavigationDrawerItem( + label = displayFolder.folder.name, + selected = selected, + modifier = modifier, + onClick = { onClick(displayFolder) }, + icon = { + Icon( + imageVector = mapFolderIcon(displayFolder.folder.type), + ) + }, + badge = { + FolderListItemBadge( + unreadCount = displayFolder.unreadMessageCount, + starredCount = displayFolder.starredMessageCount, + showStarredCount = showStarredCount, + ) + }, + ) +} + +private fun mapFolderIcon(type: FolderType): ImageVector { + return when (type) { + FolderType.INBOX -> Icons.Outlined.Inbox + FolderType.OUTBOX -> Icons.Outlined.Outbox + FolderType.SENT -> Icons.Outlined.Send + FolderType.TRASH -> Icons.Outlined.Delete + FolderType.DRAFTS -> Icons.Outlined.Drafts + FolderType.ARCHIVE -> Icons.Outlined.Archive + FolderType.SPAM -> Icons.Outlined.Report + FolderType.REGULAR -> Icons.Outlined.Folder + } +} From 8d34ada1015f2aa6d0bac92af10108d240346db1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolf-Martell=20Montw=C3=A9?= Date: Thu, 12 Sep 2024 15:05:39 +0200 Subject: [PATCH 6/7] Add FolderList --- .../drawer/ui/DrawerContentPreview.kt | 6 ++-- .../drawer/NavigationDrawerModule.kt | 1 + .../navigation/drawer/ui/DrawerContent.kt | 35 ++++-------------- .../navigation/drawer/ui/DrawerContract.kt | 10 +++++- .../navigation/drawer/ui/DrawerView.kt | 6 ++-- .../navigation/drawer/ui/DrawerViewModel.kt | 36 +++++++++++++++++-- .../navigation/drawer/ui/folder/FolderList.kt | 32 +++++++++++++++++ .../drawer/ui/DrawerViewModelTest.kt | 3 ++ 8 files changed, 93 insertions(+), 36 deletions(-) create mode 100644 feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/folder/FolderList.kt 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 ffd065bc82f..07f21cc8169 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 @@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview import app.k9mail.core.ui.compose.designsystem.PreviewWithTheme import app.k9mail.feature.navigation.drawer.ui.FakeData.DISPLAY_ACCOUNT +import kotlinx.collections.immutable.persistentListOf @Composable @Preview(showBackground = true) @@ -11,8 +12,9 @@ internal fun DrawerContentPreview() { PreviewWithTheme { DrawerContent( state = DrawerContract.State( - accounts = emptyList(), currentAccount = null, + accounts = persistentListOf(), + folders = persistentListOf(), ), ) } @@ -24,7 +26,7 @@ fun DrawerContentWithAccountPreview() { PreviewWithTheme { DrawerContent( state = DrawerContract.State( - accounts = listOf(DISPLAY_ACCOUNT), + accounts = persistentListOf(DISPLAY_ACCOUNT), currentAccount = DISPLAY_ACCOUNT, ), ) 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 428809208b3..835f3a893e2 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 @@ -49,6 +49,7 @@ val navigationDrawerModule: Module = module { viewModel { DrawerViewModel( getDisplayAccounts = get(), + getDisplayFoldersForAccount = get(), ) } } 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 fd765cacfee..db0c7ee30ab 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 @@ -4,16 +4,15 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import app.k9mail.core.ui.compose.designsystem.atom.DividerHorizontal import app.k9mail.core.ui.compose.designsystem.atom.Surface -import app.k9mail.core.ui.compose.designsystem.organism.drawer.NavigationDrawerItem import app.k9mail.core.ui.compose.theme2.MainTheme import app.k9mail.feature.navigation.drawer.ui.DrawerContract.State import app.k9mail.feature.navigation.drawer.ui.account.AccountView +import app.k9mail.feature.navigation.drawer.ui.folder.FolderList @Composable fun DrawerContent( @@ -42,32 +41,12 @@ fun DrawerContent( DividerHorizontal() } - LazyColumn( - modifier = Modifier - .fillMaxSize(), - ) { - item { - NavigationDrawerItem( - label = "Folder1", - selected = true, - onClick = {}, - ) - } - item { - NavigationDrawerItem( - label = "Folder2", - selected = false, - onClick = {}, - ) - } - item { - NavigationDrawerItem( - label = "Folder3", - selected = false, - onClick = {}, - ) - } - } + FolderList( + folders = state.folders, + selectedFolder = state.folders.firstOrNull(), // TODO Use selected folder from state + onFolderClick = { }, + showStarredCount = state.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 3f1cf11f9e8..4d9f14a78aa 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 @@ -1,15 +1,23 @@ 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 kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.immutableListOf +import kotlinx.collections.immutable.persistentListOf interface DrawerContract { interface ViewModel : UnidirectionalViewModel + @Stable data class State( val currentAccount: DisplayAccount? = null, - val accounts: List = emptyList(), + val accounts: ImmutableList = persistentListOf(), + val folders: ImmutableList = persistentListOf(), + val showStarredCount: Boolean = false, val isLoading: Boolean = false, ) 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 01a4b5b6ce0..7f7ca2d888b 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 @@ -4,16 +4,18 @@ import androidx.compose.runtime.Composable import app.k9mail.core.ui.compose.common.mvi.observe import app.k9mail.core.ui.compose.designsystem.molecule.PullToRefreshBox import org.koin.androidx.compose.koinViewModel +import app.k9mail.feature.navigation.drawer.ui.DrawerContract.Event +import app.k9mail.feature.navigation.drawer.ui.DrawerContract.ViewModel @Composable fun DrawerView( - viewModel: DrawerContract.ViewModel = koinViewModel(), + viewModel: ViewModel = koinViewModel(), ) { val (state, dispatch) = viewModel.observe { } PullToRefreshBox( isRefreshing = state.value.isLoading, - onRefresh = { dispatch(DrawerContract.Event.OnRefresh) }, + onRefresh = { dispatch(Event.OnRefresh) }, ) { DrawerContent( state = state.value, 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 65b43bb4740..92c9430ccbf 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 @@ -8,12 +8,18 @@ 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.feature.navigation.drawer.ui.DrawerContract.ViewModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @Suppress("MagicNumber") class DrawerViewModel( private val getDisplayAccounts: UseCase.GetDisplayAccounts, + private val getDisplayFoldersForAccount: UseCase.GetDisplayFoldersForAccount, initialState: State = State(), ) : BaseViewModel( initialState = initialState, @@ -22,7 +28,17 @@ class DrawerViewModel( init { viewModelScope.launch { - getDisplayAccounts().collect { accounts -> updateAccounts(accounts) } + loadAccounts() + } + + viewModelScope.launch { + loadFolders() + } + } + + private suspend fun loadAccounts() { + getDisplayAccounts().collectLatest { accounts -> + updateAccounts(accounts) } } @@ -32,16 +48,30 @@ class DrawerViewModel( updateState { if (isCurrentAccountAvailable) { - it.copy(accounts = accounts) + it.copy(accounts = accounts.toImmutableList()) } else { it.copy( + accounts = accounts.toImmutableList(), currentAccount = accounts.firstOrNull(), - accounts = accounts, ) } } } + private suspend fun loadFolders() { + state.map { it.currentAccount } + .distinctUntilChanged() + .collectLatest { currentAccount -> + if (currentAccount != null) { + getDisplayFoldersForAccount(currentAccount.account.uuid).collectLatest { folders -> + updateState { + it.copy(folders = folders.toImmutableList()) + } + } + } + } + } + override fun event(event: Event) { when (event) { Event.OnRefresh -> refresh() 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 new file mode 100644 index 00000000000..cfbe88d7c90 --- /dev/null +++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/folder/FolderList.kt @@ -0,0 +1,32 @@ +package app.k9mail.feature.navigation.drawer.ui.folder + +import androidx.compose.foundation.layout.fillMaxSize +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 kotlinx.collections.immutable.ImmutableList + +@Composable +fun FolderList( + folders: ImmutableList, + selectedFolder: DisplayFolder?, + onFolderClick: (DisplayFolder) -> Unit, + showStarredCount: Boolean, + modifier: Modifier = Modifier, +) { + LazyColumn( + modifier = modifier + .fillMaxSize(), + ) { + items(folders) { folder -> + FolderListItem( + displayFolder = folder, + selected = folder == selectedFolder, + showStarredCount = showStarredCount, + onClick = onFolderClick, + ) + } + } +} 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 c1d0725bc4f..3ef98c9a39f 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 @@ -7,6 +7,7 @@ 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 @@ -91,9 +92,11 @@ class DrawerViewModelTest { private fun createTestSubject( getDisplayAccountsFlow: Flow> = flow { emit(emptyList()) }, + getDisplayFoldersForAccount: Flow> = flow { emit(emptyList()) }, ): DrawerViewModel { return DrawerViewModel( getDisplayAccounts = { getDisplayAccountsFlow }, + getDisplayFoldersForAccount = { getDisplayFoldersForAccount }, ) } From 8afe58c644f78ca4d45ac65d39648ffd40dd18d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolf-Martell=20Montw=C3=A9?= Date: Fri, 13 Sep 2024 12:04:30 +0200 Subject: [PATCH 7/7] Add OnAccountClick event to trigger account selection and add OnAccountViewClick on the account view for testing --- .../drawer/ui/DrawerContentPreview.kt | 4 +- .../drawer/ui/account/AccountViewPreview.kt | 4 + .../navigation/drawer/ui/DrawerContent.kt | 3 + .../navigation/drawer/ui/DrawerContract.kt | 3 +- .../navigation/drawer/ui/DrawerView.kt | 3 +- .../navigation/drawer/ui/DrawerViewModel.kt | 46 ++++++-- .../drawer/ui/account/AccountView.kt | 3 + .../drawer/ui/DrawerViewModelTest.kt | 109 ++++++++++++++++-- 8 files changed, 156 insertions(+), 19 deletions(-) diff --git a/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContentPreview.kt b/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContentPreview.kt index 07f21cc8169..f9cb6d53766 100644 --- a/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContentPreview.kt +++ b/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContentPreview.kt @@ -12,10 +12,11 @@ internal fun DrawerContentPreview() { PreviewWithTheme { DrawerContent( state = DrawerContract.State( - currentAccount = null, accounts = persistentListOf(), + currentAccount = null, folders = persistentListOf(), ), + onEvent = {}, ) } } @@ -29,6 +30,7 @@ fun DrawerContentWithAccountPreview() { accounts = persistentListOf(DISPLAY_ACCOUNT), currentAccount = DISPLAY_ACCOUNT, ), + onEvent = {}, ) } } diff --git a/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/account/AccountViewPreview.kt b/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/account/AccountViewPreview.kt index cded6eea7c3..63595649232 100644 --- a/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/account/AccountViewPreview.kt +++ b/feature/navigation/drawer/src/debug/kotlin/app/k9mail/feature/navigation/drawer/ui/account/AccountViewPreview.kt @@ -15,6 +15,7 @@ internal fun AccountViewPreview() { displayName = DISPLAY_NAME, emailAddress = EMAIL_ADDRESS, accountColor = 0, + onClick = {}, ) } } @@ -27,6 +28,7 @@ internal fun AccountViewWithColorPreview() { displayName = DISPLAY_NAME, emailAddress = EMAIL_ADDRESS, accountColor = 0xFF0000, + onClick = {}, ) } } @@ -39,6 +41,7 @@ internal fun AccountViewWithLongDisplayName() { displayName = "$LONG_TEXT $DISPLAY_NAME", emailAddress = EMAIL_ADDRESS, accountColor = 0, + onClick = {}, ) } } @@ -51,6 +54,7 @@ internal fun AccountViewWithLongEmailPreview() { displayName = DISPLAY_NAME, emailAddress = "$LONG_TEXT@example.com", accountColor = 0, + onClick = {}, ) } } diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContent.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContent.kt index db0c7ee30ab..202bf9c7b08 100644 --- a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContent.kt +++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContent.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.platform.testTag import app.k9mail.core.ui.compose.designsystem.atom.DividerHorizontal import app.k9mail.core.ui.compose.designsystem.atom.Surface import app.k9mail.core.ui.compose.theme2.MainTheme +import app.k9mail.feature.navigation.drawer.ui.DrawerContract.Event import app.k9mail.feature.navigation.drawer.ui.DrawerContract.State import app.k9mail.feature.navigation.drawer.ui.account.AccountView import app.k9mail.feature.navigation.drawer.ui.folder.FolderList @@ -17,6 +18,7 @@ import app.k9mail.feature.navigation.drawer.ui.folder.FolderList @Composable fun DrawerContent( state: State, + onEvent: (Event) -> Unit, modifier: Modifier = Modifier, ) { Surface( @@ -37,6 +39,7 @@ fun DrawerContent( displayName = it.account.displayName, emailAddress = it.account.email, accountColor = it.account.chipColor, + onClick = { onEvent(Event.OnAccountViewClick(it)) }, ) DividerHorizontal() diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContract.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContract.kt index 4d9f14a78aa..d2652ddc2f6 100644 --- a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContract.kt +++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerContract.kt @@ -5,7 +5,6 @@ import app.k9mail.core.ui.compose.common.mvi.UnidirectionalViewModel import app.k9mail.feature.navigation.drawer.domain.entity.DisplayAccount import app.k9mail.legacy.ui.folder.DisplayFolder import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.immutableListOf import kotlinx.collections.immutable.persistentListOf interface DrawerContract { @@ -23,6 +22,8 @@ interface DrawerContract { sealed interface Event { data object OnRefresh : Event + data class OnAccountClick(val account: DisplayAccount) : Event + data class OnAccountViewClick(val account: DisplayAccount) : Event } sealed interface Effect diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerView.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerView.kt index 7f7ca2d888b..ca14902c944 100644 --- a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerView.kt +++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerView.kt @@ -3,9 +3,9 @@ package app.k9mail.feature.navigation.drawer.ui import androidx.compose.runtime.Composable import app.k9mail.core.ui.compose.common.mvi.observe import app.k9mail.core.ui.compose.designsystem.molecule.PullToRefreshBox -import org.koin.androidx.compose.koinViewModel import app.k9mail.feature.navigation.drawer.ui.DrawerContract.Event import app.k9mail.feature.navigation.drawer.ui.DrawerContract.ViewModel +import org.koin.androidx.compose.koinViewModel @Composable fun DrawerView( @@ -19,6 +19,7 @@ fun DrawerView( ) { DrawerContent( state = state.value, + onEvent = { dispatch(it) }, ) } } diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewModel.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewModel.kt index 92c9430ccbf..87a7f664bc6 100644 --- a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewModel.kt +++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewModel.kt @@ -10,10 +10,12 @@ import app.k9mail.feature.navigation.drawer.ui.DrawerContract.State import app.k9mail.feature.navigation.drawer.ui.DrawerContract.ViewModel import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.launch @Suppress("MagicNumber") @@ -58,16 +60,15 @@ class DrawerViewModel( } } + @OptIn(ExperimentalCoroutinesApi::class) private suspend fun loadFolders() { - state.map { it.currentAccount } + state.mapNotNull { it.currentAccount?.account?.uuid } .distinctUntilChanged() - .collectLatest { currentAccount -> - if (currentAccount != null) { - getDisplayFoldersForAccount(currentAccount.account.uuid).collectLatest { folders -> - updateState { - it.copy(folders = folders.toImmutableList()) - } - } + .flatMapLatest { accountUuid -> + getDisplayFoldersForAccount(accountUuid) + }.collectLatest { folders -> + updateState { + it.copy(folders = folders.toImmutableList()) } } } @@ -75,6 +76,33 @@ class DrawerViewModel( override fun event(event: Event) { when (event) { Event.OnRefresh -> refresh() + is Event.OnAccountClick -> selectAccount(event.account) + is Event.OnAccountViewClick -> { + selectAccount( + state.value.accounts.nextOrFirst(event.account)!!, + ) + } + } + } + + private fun selectAccount(account: DisplayAccount) { + viewModelScope.launch { + updateState { + it.copy( + currentAccount = account, + ) + } + } + } + + private fun ImmutableList.nextOrFirst(account: DisplayAccount): DisplayAccount? { + val index = indexOf(account) + return if (index == -1) { + null + } else if (index == size - 1) { + get(0) + } else { + get(index + 1) } } diff --git a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/account/AccountView.kt b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/account/AccountView.kt index d4d16d78894..4cdbeda9110 100644 --- a/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/account/AccountView.kt +++ b/feature/navigation/drawer/src/main/kotlin/app/k9mail/feature/navigation/drawer/ui/account/AccountView.kt @@ -1,5 +1,6 @@ package app.k9mail.feature.navigation.drawer.ui.account +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize @@ -21,11 +22,13 @@ fun AccountView( emailAddress: String, accountColor: Int, modifier: Modifier = Modifier, + onClick: () -> Unit, ) { Row( modifier = modifier .fillMaxWidth() .height(intrinsicSize = IntrinsicSize.Max) + .clickable(onClick = onClick) .padding( top = MainTheme.spacings.default, start = MainTheme.spacings.double, diff --git a/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewModelTest.kt b/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewModelTest.kt index 3ef98c9a39f..e4fdece880c 100644 --- a/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewModelTest.kt +++ b/feature/navigation/drawer/src/test/kotlin/app/k9mail/feature/navigation/drawer/ui/DrawerViewModelTest.kt @@ -1,5 +1,7 @@ package app.k9mail.feature.navigation.drawer.ui +import app.k9mail.core.mail.folder.api.Folder +import app.k9mail.core.mail.folder.api.FolderType import app.k9mail.core.ui.compose.testing.MainDispatcherRule import app.k9mail.core.ui.compose.testing.mvi.eventStateTest import app.k9mail.feature.navigation.drawer.domain.entity.DisplayAccount @@ -47,7 +49,7 @@ class DrawerViewModelTest { val displayAccounts = createDisplayAccountList(3) val getDisplayAccountsFlow = MutableStateFlow(displayAccounts) val testSubject = createTestSubject( - getDisplayAccountsFlow = getDisplayAccountsFlow, + displayAccountsFlow = getDisplayAccountsFlow, ) advanceUntilIdle() @@ -62,7 +64,7 @@ class DrawerViewModelTest { val displayAccounts = createDisplayAccountList(3) val getDisplayAccountsFlow = MutableStateFlow(displayAccounts) val testSubject = createTestSubject( - getDisplayAccountsFlow = getDisplayAccountsFlow, + displayAccountsFlow = getDisplayAccountsFlow, ) advanceUntilIdle() @@ -81,7 +83,7 @@ class DrawerViewModelTest { fun `should set current account to null when no accounts are present`() = runTest { val getDisplayAccountsFlow = MutableStateFlow(emptyList()) val testSubject = createTestSubject( - getDisplayAccountsFlow = getDisplayAccountsFlow, + displayAccountsFlow = getDisplayAccountsFlow, ) advanceUntilIdle() @@ -90,13 +92,76 @@ class DrawerViewModelTest { assertThat(testSubject.state.value.currentAccount).isEqualTo(null) } + @Test + fun `should set current account when OnAccountClick event is received`() = runTest { + val displayAccounts = createDisplayAccountList(3) + val getDisplayAccountsFlow = MutableStateFlow(displayAccounts) + val testSubject = createTestSubject( + displayAccountsFlow = getDisplayAccountsFlow, + ) + + advanceUntilIdle() + + testSubject.event(Event.OnAccountClick(displayAccounts[1])) + + advanceUntilIdle() + + assertThat(testSubject.state.value.currentAccount).isEqualTo(displayAccounts[1]) + } + + @Test + fun `should collect display folders for current account`() = runTest { + val displayAccounts = createDisplayAccountList(3) + val getDisplayAccountsFlow = MutableStateFlow(displayAccounts) + val displayFoldersMap = mapOf( + displayAccounts[0].account.uuid to createDisplayFolderList(3), + ) + val testSubject = createTestSubject( + displayAccountsFlow = getDisplayAccountsFlow, + displayFoldersMap = displayFoldersMap, + ) + + advanceUntilIdle() + + val displayFolders = displayFoldersMap[displayAccounts[0].account.uuid] ?: emptyList() + assertThat(testSubject.state.value.folders.size).isEqualTo(displayFolders.size) + assertThat(testSubject.state.value.folders).isEqualTo(displayFolders) + } + + @Test + fun `should collect display folders when current account is changed`() = runTest { + val displayAccounts = createDisplayAccountList(3) + val getDisplayAccountsFlow = MutableStateFlow(displayAccounts) + val displayFoldersMap = mapOf( + displayAccounts[0].account.uuid to createDisplayFolderList(1), + displayAccounts[1].account.uuid to createDisplayFolderList(5), + displayAccounts[2].account.uuid to createDisplayFolderList(10), + ) + val testSubject = createTestSubject( + displayAccountsFlow = getDisplayAccountsFlow, + displayFoldersMap = displayFoldersMap, + ) + + advanceUntilIdle() + + testSubject.event(Event.OnAccountClick(displayAccounts[1])) + + advanceUntilIdle() + + val displayFolders = displayFoldersMap[displayAccounts[1].account.uuid] ?: emptyList() + assertThat(testSubject.state.value.folders.size).isEqualTo(displayFolders.size) + assertThat(testSubject.state.value.folders).isEqualTo(displayFolders) + } + private fun createTestSubject( - getDisplayAccountsFlow: Flow> = flow { emit(emptyList()) }, - getDisplayFoldersForAccount: Flow> = flow { emit(emptyList()) }, + displayAccountsFlow: Flow> = flow { emit(emptyList()) }, + displayFoldersMap: Map> = emptyMap(), ): DrawerViewModel { return DrawerViewModel( - getDisplayAccounts = { getDisplayAccountsFlow }, - getDisplayFoldersForAccount = { getDisplayFoldersForAccount }, + getDisplayAccounts = { displayAccountsFlow }, + getDisplayFoldersForAccount = { accountUuid -> + flow { emit(displayFoldersMap[accountUuid] ?: emptyList()) } + }, ) } @@ -137,4 +202,34 @@ class DrawerViewModelTest { ) } } + + private fun createDisplayFolder( + id: Long = 1234, + name: String = "name", + type: FolderType = FolderType.REGULAR, + unreadCount: Int = 0, + starredCount: Int = 0, + ): DisplayFolder { + val folder = Folder( + id = id, + name = name, + type = type, + isLocalOnly = false, + ) + + return DisplayFolder( + folder = folder, + isInTopGroup = false, + unreadMessageCount = unreadCount, + starredMessageCount = starredCount, + ) + } + + private fun createDisplayFolderList(count: Int): List { + return List(count) { index -> + createDisplayFolder( + id = index.toLong() + 100, + ) + } + } }