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

Merge pull request #8136 from wmontwe/add-drawer-account-top-view

Add drawer account view
This commit is contained in:
Wolf-Martell Montwé 2024-09-12 17:32:22 +02:00 committed by GitHub
commit c05533a26f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 428 additions and 28 deletions

View File

@ -113,4 +113,4 @@ private fun ColorScheme.toDynamicThemeColorScheme(
)
}
private fun Color.toHarmonizedColor(target: Color) = Color(MaterialColors.harmonize(toArgb(), target.toArgb()))
fun Color.toHarmonizedColor(target: Color) = Color(MaterialColors.harmonize(toArgb(), target.toArgb()))

View File

@ -3,11 +3,30 @@ 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
@Composable
@Preview(showBackground = true)
internal fun DrawerContentPreview() {
PreviewWithTheme {
DrawerContent()
DrawerContent(
state = DrawerContract.State(
accounts = emptyList(),
currentAccount = null,
),
)
}
}
@Composable
@Preview(showBackground = true)
fun DrawerContentWithAccountPreview() {
PreviewWithTheme {
DrawerContent(
state = DrawerContract.State(
accounts = listOf(DISPLAY_ACCOUNT),
currentAccount = DISPLAY_ACCOUNT,
),
)
}
}

View File

@ -0,0 +1,43 @@
package app.k9mail.feature.navigation.drawer.ui.account
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.PreviewWithThemes
import app.k9mail.core.ui.compose.theme2.MainTheme
@Composable
@Preview(showBackground = true)
internal fun AccountIndicatorPreview() {
PreviewWithThemes {
AccountIndicator(
accountColor = 0,
modifier = Modifier.height(MainTheme.spacings.double),
)
}
}
@Composable
@Preview(showBackground = true)
internal fun AccountIndicatorPreviewWithYellowAccountColor() {
PreviewWithThemes {
AccountIndicator(
accountColor = Color.Yellow.toArgb(),
modifier = Modifier.height(MainTheme.spacings.double),
)
}
}
@Composable
@Preview(showBackground = true)
internal fun AccountIndicatorPreviewWithGrayAccountColor() {
PreviewWithThemes {
AccountIndicator(
accountColor = Color.Gray.toArgb(),
modifier = Modifier.height(MainTheme.spacings.double),
)
}
}

View File

@ -0,0 +1,56 @@
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
@Composable
@Preview(showBackground = true)
internal fun AccountViewPreview() {
PreviewWithThemes {
AccountView(
displayName = DISPLAY_NAME,
emailAddress = EMAIL_ADDRESS,
accountColor = 0,
)
}
}
@Composable
@Preview(showBackground = true)
internal fun AccountViewWithColorPreview() {
PreviewWithThemes {
AccountView(
displayName = DISPLAY_NAME,
emailAddress = EMAIL_ADDRESS,
accountColor = 0xFF0000,
)
}
}
@Composable
@Preview(showBackground = true)
internal fun AccountViewWithLongDisplayName() {
PreviewWithThemes {
AccountView(
displayName = "$LONG_TEXT $DISPLAY_NAME",
emailAddress = EMAIL_ADDRESS,
accountColor = 0,
)
}
}
@Composable
@Preview(showBackground = true)
internal fun AccountViewWithLongEmailPreview() {
PreviewWithThemes {
AccountView(
displayName = DISPLAY_NAME,
emailAddress = "$LONG_TEXT@example.com",
accountColor = 0,
)
}
}

View File

@ -0,0 +1,37 @@
package app.k9mail.feature.navigation.drawer.ui.account
import app.k9mail.feature.navigation.drawer.domain.entity.DisplayAccount
import app.k9mail.legacy.account.Account
import app.k9mail.legacy.account.Identity
internal object FakeData {
const val ACCOUNT_UUID = "uuid"
const val DISPLAY_NAME = "Account Name"
const val EMAIL_ADDRESS = "test@example.com"
const val LONG_TEXT = "loremipsumdolorsitametconsetetursadipscingelitr" +
"seddiamnonumyeirmodtemporinviduntutlaboreetdoloremagnaaliquyameratseddiamvoluptua"
val ACCOUNT = Account(
uuid = ACCOUNT_UUID,
).apply {
identities = ArrayList()
val identity = Identity(
signatureUse = false,
signature = "",
description = "",
)
identities.add(identity)
name = DISPLAY_NAME
email = EMAIL_ADDRESS
}
val DISPLAY_ACCOUNT = DisplayAccount(
account = ACCOUNT,
unreadMessageCount = 0,
starredMessageCount = 0,
)
}

View File

@ -39,5 +39,9 @@ val navigationDrawerModule: Module = module {
)
}
viewModel { DrawerViewModel() }
viewModel {
DrawerViewModel(
getDisplayAccounts = get(),
)
}
}

View File

@ -7,7 +7,7 @@ interface DomainContract {
interface UseCase {
fun interface GetDisplayAccounts {
fun execute(): Flow<List<DisplayAccount>>
operator fun invoke(): Flow<List<DisplayAccount>>
}
}
}

View File

@ -27,7 +27,7 @@ class GetDisplayAccounts(
) : UseCase.GetDisplayAccounts {
@OptIn(ExperimentalCoroutinesApi::class)
override fun execute(): Flow<List<DisplayAccount>> {
override fun invoke(): Flow<List<DisplayAccount>> {
return accountManager.getAccountsFlow()
.flatMapLatest { accounts ->
val messageCountsFlows: List<Flow<MessageCounts>> = accounts.map { account ->

View File

@ -9,5 +9,5 @@ import app.k9mail.feature.navigation.drawer.domain.entity.DisplayAccount
class AccountsViewModel(
getDisplayAccounts: UseCase.GetDisplayAccounts,
) : ViewModel() {
val displayAccountsLiveData: LiveData<List<DisplayAccount>> = getDisplayAccounts.execute().asLiveData()
val displayAccountsLiveData: LiveData<List<DisplayAccount>> = getDisplayAccounts().asLiveData()
}

View File

@ -1,17 +1,23 @@
package app.k9mail.feature.navigation.drawer.ui
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
@Composable
fun DrawerContent(
state: State,
modifier: Modifier = Modifier,
) {
Surface(
@ -19,33 +25,48 @@ fun DrawerContent(
.fillMaxSize()
.testTag("DrawerContent"),
) {
LazyColumn(
Column(
modifier = Modifier
.fillMaxSize()
.padding(
vertical = MainTheme.spacings.oneHalf,
),
verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.default),
) {
item {
NavigationDrawerItem(
label = "Folder1",
selected = true,
onClick = {},
state.currentAccount?.let {
AccountView(
displayName = it.account.displayName,
emailAddress = it.account.email,
accountColor = it.account.chipColor,
)
DividerHorizontal()
}
item {
NavigationDrawerItem(
label = "Folder2",
selected = false,
onClick = {},
)
}
item {
NavigationDrawerItem(
label = "Folder3",
selected = false,
onClick = {},
)
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 = {},
)
}
}
}
}

View File

@ -1,12 +1,15 @@
package app.k9mail.feature.navigation.drawer.ui
import app.k9mail.core.ui.compose.common.mvi.UnidirectionalViewModel
import app.k9mail.feature.navigation.drawer.domain.entity.DisplayAccount
interface DrawerContract {
interface ViewModel : UnidirectionalViewModel<State, Event, Effect>
data class State(
val currentAccount: DisplayAccount? = null,
val accounts: List<DisplayAccount> = emptyList(),
val isLoading: Boolean = false,
)

View File

@ -15,6 +15,8 @@ fun DrawerView(
isRefreshing = state.value.isLoading,
onRefresh = { dispatch(DrawerContract.Event.OnRefresh) },
) {
DrawerContent()
DrawerContent(
state = state.value,
)
}
}

View File

@ -2,6 +2,8 @@ package app.k9mail.feature.navigation.drawer.ui
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.ui.DrawerContract.Effect
import app.k9mail.feature.navigation.drawer.ui.DrawerContract.Event
import app.k9mail.feature.navigation.drawer.ui.DrawerContract.State
@ -11,12 +13,35 @@ import kotlinx.coroutines.launch
@Suppress("MagicNumber")
class DrawerViewModel(
private val getDisplayAccounts: UseCase.GetDisplayAccounts,
initialState: State = State(),
) : BaseViewModel<State, Event, Effect>(
initialState = initialState,
),
ViewModel {
init {
viewModelScope.launch {
getDisplayAccounts().collect { accounts -> updateAccounts(accounts) }
}
}
private fun updateAccounts(accounts: List<DisplayAccount>) {
val currentAccountUuid = state.value.currentAccount?.account?.uuid
val isCurrentAccountAvailable = accounts.any { currentAccountUuid == it.account.uuid }
updateState {
if (isCurrentAccountAvailable) {
it.copy(accounts = accounts)
} else {
it.copy(
currentAccount = accounts.firstOrNull(),
accounts = accounts,
)
}
}
}
override fun event(event: Event) {
when (event) {
Event.OnRefresh -> refresh()

View File

@ -0,0 +1,32 @@
package app.k9mail.feature.navigation.drawer.ui.account
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import app.k9mail.core.ui.compose.designsystem.atom.Surface
import app.k9mail.core.ui.compose.theme2.MainTheme
import app.k9mail.core.ui.compose.theme2.toHarmonizedColor
@Composable
fun AccountIndicator(
accountColor: Int,
modifier: Modifier = Modifier,
) {
val color = if (accountColor == 0) {
MainTheme.colors.primary
} else {
Color(accountColor).toHarmonizedColor(MainTheme.colors.surface)
}
Surface(
modifier = modifier
.width(MainTheme.spacings.half)
.defaultMinSize(
minHeight = MainTheme.spacings.default,
),
color = color,
shape = MainTheme.shapes.medium,
) {}
}

View File

@ -0,0 +1,58 @@
package app.k9mail.feature.navigation.drawer.ui.account
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodyLarge
import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodyMedium
import app.k9mail.core.ui.compose.theme2.MainTheme
@Composable
fun AccountView(
displayName: String,
emailAddress: String,
accountColor: Int,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.fillMaxWidth()
.height(intrinsicSize = IntrinsicSize.Max)
.padding(
top = MainTheme.spacings.default,
start = MainTheme.spacings.double,
end = MainTheme.spacings.triple,
bottom = MainTheme.spacings.oneHalf,
),
verticalAlignment = Alignment.CenterVertically,
) {
AccountIndicator(
accountColor = accountColor,
modifier = Modifier
.fillMaxHeight()
.padding(
end = MainTheme.spacings.default,
),
)
Column(
verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.half),
) {
TextBodyLarge(
text = displayName,
color = MainTheme.colors.onSurface,
)
TextBodyMedium(
text = emailAddress,
color = MainTheme.colors.onSurfaceVariant,
)
}
}
}

View File

@ -2,12 +2,18 @@ package app.k9mail.feature.navigation.drawer.ui
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
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 assertk.assertThat
import assertk.assertions.isEqualTo
import kotlin.test.Test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.Rule
@ -18,10 +24,10 @@ class DrawerViewModelTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
private val testSubject = DrawerViewModel()
@Test
fun `should change loading state when OnRefresh event is received`() = runTest {
val testSubject = createTestSubject()
eventStateTest(
viewModel = testSubject,
initialState = State(isLoading = false),
@ -34,4 +40,98 @@ class DrawerViewModelTest {
assertThat(testSubject.state.value.isLoading).isEqualTo(false)
}
@Test
fun `should collect display accounts when created and select first as current`() = runTest {
val displayAccounts = createDisplayAccountList(3)
val getDisplayAccountsFlow = MutableStateFlow(displayAccounts)
val testSubject = createTestSubject(
getDisplayAccountsFlow = getDisplayAccountsFlow,
)
advanceUntilIdle()
assertThat(testSubject.state.value.accounts.size).isEqualTo(displayAccounts.size)
assertThat(testSubject.state.value.accounts).isEqualTo(displayAccounts)
assertThat(testSubject.state.value.currentAccount).isEqualTo(displayAccounts.first())
}
@Test
fun `should reselect current account when old not present anymore`() = runTest {
val displayAccounts = createDisplayAccountList(3)
val getDisplayAccountsFlow = MutableStateFlow(displayAccounts)
val testSubject = createTestSubject(
getDisplayAccountsFlow = getDisplayAccountsFlow,
)
advanceUntilIdle()
val newDisplayAccounts = displayAccounts.drop(1)
getDisplayAccountsFlow.emit(newDisplayAccounts)
advanceUntilIdle()
assertThat(testSubject.state.value.accounts.size).isEqualTo(newDisplayAccounts.size)
assertThat(testSubject.state.value.accounts).isEqualTo(newDisplayAccounts)
assertThat(testSubject.state.value.currentAccount).isEqualTo(newDisplayAccounts.first())
}
@Test
fun `should set current account to null when no accounts are present`() = runTest {
val getDisplayAccountsFlow = MutableStateFlow(emptyList<DisplayAccount>())
val testSubject = createTestSubject(
getDisplayAccountsFlow = getDisplayAccountsFlow,
)
advanceUntilIdle()
assertThat(testSubject.state.value.accounts.size).isEqualTo(0)
assertThat(testSubject.state.value.currentAccount).isEqualTo(null)
}
private fun createTestSubject(
getDisplayAccountsFlow: Flow<List<DisplayAccount>> = flow { emit(emptyList()) },
): DrawerViewModel {
return DrawerViewModel(
getDisplayAccounts = { getDisplayAccountsFlow },
)
}
private fun createDisplayAccount(
uuid: String = "uuid",
name: String = "name",
email: String = "test@example.com",
unreadCount: Int = 0,
starredCount: Int = 0,
): DisplayAccount {
val account = Account(
uuid = uuid,
).also {
it.identities = ArrayList()
val identity = Identity(
signatureUse = false,
signature = "",
description = "",
)
it.identities.add(identity)
it.name = name
it.email = email
}
return DisplayAccount(
account = account,
unreadMessageCount = unreadCount,
starredMessageCount = starredCount,
)
}
private fun createDisplayAccountList(count: Int): List<DisplayAccount> {
return List(count) { index ->
createDisplayAccount(
uuid = "uuid-$index",
)
}
}
}