Skip to content

Commit 602d6e0

Browse files
authored
Merge pull request #10104 from wmontwe/feat/9031/add-account-monogram-customization-part-3
feat(account-setting): add account monogram customization part 3
2 parents c75d83e + c9cd5ee commit 602d6e0

120 files changed

Lines changed: 2539 additions & 1125 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app-common/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ dependencies {
5757

5858
implementation(libs.androidx.work.runtime)
5959
implementation(libs.androidx.lifecycle.process)
60+
implementation(libs.kotlinx.collections.immutable)
6061

6162
testImplementation(projects.feature.account.fake)
63+
testImplementation(projects.core.testing)
6264
}
Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,26 @@
11
package net.thunderbird.app.common.account
22

3-
import android.content.res.Resources
4-
import app.k9mail.core.ui.legacy.theme2.common.R
5-
import net.thunderbird.core.android.account.LegacyAccountDtoManager
3+
import kotlinx.collections.immutable.ImmutableList
4+
import kotlinx.coroutines.flow.first
5+
import net.thunderbird.feature.account.profile.AccountProfileRepository
66

77
internal class AccountColorPicker(
8-
private val accountManager: LegacyAccountDtoManager,
9-
private val resources: Resources,
8+
private val repository: AccountProfileRepository,
9+
private val accountColors: ImmutableList<Int>,
1010
) {
11-
fun pickColor(): Int {
12-
val accounts = accountManager.getAccounts()
13-
val usedAccountColors = accounts.map { it.chipColor }.toSet()
14-
val accountColors = resources.getIntArray(R.array.account_colors).toList()
11+
suspend fun pickColor(): Int {
12+
val profiles = repository.getAll().first()
13+
val usedCounts = profiles.groupingBy { it.color }.eachCount()
1514

16-
val availableColors = accountColors - usedAccountColors
17-
if (availableColors.isEmpty()) {
18-
return accountColors.random()
15+
val minCount = accountColors.minOf { usedCounts[it] ?: 0 }
16+
val candidates = accountColors.filter {
17+
(usedCounts[it] ?: 0) == minCount
1918
}
2019

21-
val defaultAccountColors = resources.getIntArray(R.array.default_account_colors)
22-
return availableColors.shuffled().minByOrNull { color ->
23-
val index = defaultAccountColors.indexOf(color)
24-
if (index != -1) index else defaultAccountColors.size
25-
} ?: error("availableColors must not be empty")
20+
return if (candidates.isNotEmpty()) {
21+
candidates.shuffled().first()
22+
} else {
23+
accountColors.shuffled().first()
24+
}
2625
}
2726
}

app-common/src/main/kotlin/net/thunderbird/app/common/account/AppCommonAccountModule.kt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package net.thunderbird.app.common.account
22

33
import app.k9mail.feature.account.setup.AccountSetupExternalContract
4+
import kotlinx.collections.immutable.ImmutableList
5+
import kotlinx.collections.immutable.toImmutableList
46
import net.thunderbird.app.common.account.data.DefaultAccountProfileLocalDataSource
57
import net.thunderbird.app.common.account.data.DefaultLegacyAccountManager
68
import net.thunderbird.core.android.account.AccountDefaultsProvider
@@ -13,8 +15,11 @@ import net.thunderbird.feature.account.core.featureAccountCoreModule
1315
import net.thunderbird.feature.account.storage.legacy.featureAccountStorageLegacyModule
1416
import net.thunderbird.feature.mail.account.api.AccountManager
1517
import org.koin.android.ext.koin.androidApplication
18+
import org.koin.android.ext.koin.androidContext
19+
import org.koin.core.qualifier.named
1620
import org.koin.dsl.binds
1721
import org.koin.dsl.module
22+
import app.k9mail.core.ui.legacy.theme2.common.R as ThemeCommonR
1823

1924
internal val appCommonAccountModule = module {
2025
includes(
@@ -43,10 +48,16 @@ internal val appCommonAccountModule = module {
4348
)
4449
}
4550

51+
factory<ImmutableList<Int>>(named("AccountColors")) {
52+
androidContext().resources.getIntArray(
53+
ThemeCommonR.array.account_colors,
54+
).toList().toImmutableList()
55+
}
56+
4657
factory {
4758
AccountColorPicker(
48-
accountManager = get(),
49-
resources = get(),
59+
repository = get(),
60+
accountColors = get(named("AccountColors")),
5061
)
5162
}
5263

app-common/src/main/kotlin/net/thunderbird/app/common/account/data/DefaultAccountProfileLocalDataSource.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ internal class DefaultAccountProfileLocalDataSource(
1414
private val dataMapper: AccountProfileDataMapper,
1515
) : AccountProfileLocalDataSource {
1616

17+
override fun getAll(): Flow<List<AccountProfile>> {
18+
return accountManager.getAll()
19+
.map { accounts ->
20+
accounts.map { dto ->
21+
dataMapper.toDomain(dto.profile)
22+
}
23+
}
24+
}
25+
1726
override fun getById(accountId: AccountId): Flow<AccountProfile?> {
1827
return accountManager.getById(accountId)
1928
.map { account ->
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package net.thunderbird.app.common.account
2+
3+
import assertk.assertThat
4+
import assertk.assertions.isEqualTo
5+
import assertk.assertions.isOneOf
6+
import kotlin.test.Test
7+
import kotlinx.collections.immutable.persistentListOf
8+
import kotlinx.coroutines.flow.MutableStateFlow
9+
import kotlinx.coroutines.test.runTest
10+
import net.thunderbird.app.common.account.data.FakeAccountProfileRepository
11+
import net.thunderbird.feature.account.AccountIdFactory
12+
import net.thunderbird.feature.account.avatar.Avatar
13+
import net.thunderbird.feature.account.profile.AccountProfile
14+
15+
class AccountColorPickerTest {
16+
17+
@Test
18+
fun `should pick random color when none used`() = runTest {
19+
// Arrange
20+
val profiles: MutableStateFlow<List<AccountProfile>> = MutableStateFlow(emptyList())
21+
val testSubject = AccountColorPicker(
22+
repository = FakeAccountProfileRepository(profiles),
23+
accountColors = ACCOUNT_COLORS,
24+
)
25+
26+
// Act
27+
val result = testSubject.pickColor()
28+
29+
// Assert
30+
assertThat(result).isOneOf(COLOR_RED, COLOR_GREEN, COLOR_BLUE)
31+
}
32+
33+
@Test
34+
fun `should pick one of the available colors when some are used`() = runTest {
35+
// Arrange
36+
val profiles: MutableStateFlow<List<AccountProfile>> = MutableStateFlow(
37+
listOf(
38+
ACCOUNT_PROFILE_GREEN_1,
39+
),
40+
)
41+
val testSubject = AccountColorPicker(
42+
repository = FakeAccountProfileRepository(profiles),
43+
accountColors = ACCOUNT_COLORS,
44+
)
45+
46+
// Act
47+
val result = testSubject.pickColor()
48+
49+
// Assert
50+
assertThat(result).isOneOf(COLOR_RED, COLOR_BLUE)
51+
}
52+
53+
@Test
54+
fun `should pick last available color when others are used`() = runTest {
55+
// Arrange
56+
val profiles: MutableStateFlow<List<AccountProfile>> = MutableStateFlow(
57+
listOf(
58+
ACCOUNT_PROFILE_RED_1,
59+
ACCOUNT_PROFILE_GREEN_1,
60+
),
61+
)
62+
val testSubject = AccountColorPicker(
63+
repository = FakeAccountProfileRepository(profiles),
64+
accountColors = ACCOUNT_COLORS,
65+
)
66+
67+
// Act
68+
val result = testSubject.pickColor()
69+
70+
// Assert
71+
assertThat(result).isEqualTo(COLOR_BLUE)
72+
}
73+
74+
@Test
75+
fun `should pick random color when all colors are used equally`() = runTest {
76+
// Arrange
77+
val profiles: MutableStateFlow<List<AccountProfile>> = MutableStateFlow(
78+
listOf(
79+
ACCOUNT_PROFILE_RED_1,
80+
ACCOUNT_PROFILE_GREEN_1,
81+
ACCOUNT_PROFILE_BLUE_1,
82+
),
83+
)
84+
val testSubject = AccountColorPicker(
85+
repository = FakeAccountProfileRepository(profiles),
86+
accountColors = ACCOUNT_COLORS,
87+
)
88+
89+
// Act
90+
val result = testSubject.pickColor()
91+
92+
// Assert
93+
assertThat(result).isOneOf(COLOR_RED, COLOR_GREEN, COLOR_BLUE)
94+
}
95+
96+
@Test
97+
fun `should pick from least used colors when colors are used multiple times`() = runTest {
98+
// Arrange
99+
val profiles: MutableStateFlow<List<AccountProfile>> = MutableStateFlow(
100+
listOf(
101+
ACCOUNT_PROFILE_RED_1,
102+
ACCOUNT_PROFILE_RED_2,
103+
ACCOUNT_PROFILE_GREEN_1,
104+
ACCOUNT_PROFILE_GREEN_2,
105+
ACCOUNT_PROFILE_BLUE_1,
106+
),
107+
)
108+
val testSubject = AccountColorPicker(
109+
repository = FakeAccountProfileRepository(profiles),
110+
accountColors = ACCOUNT_COLORS,
111+
)
112+
113+
// Act
114+
val result = testSubject.pickColor()
115+
116+
// Assert
117+
assertThat(result).isEqualTo(COLOR_BLUE)
118+
}
119+
120+
private companion object {
121+
const val COLOR_RED = 0xFF0000
122+
const val COLOR_GREEN = 0x00FF00
123+
const val COLOR_BLUE = 0x0000FF
124+
125+
val ACCOUNT_COLORS = persistentListOf(
126+
COLOR_RED,
127+
COLOR_GREEN,
128+
COLOR_BLUE,
129+
)
130+
131+
val ACCOUNT_PROFILE_RED_1 = AccountProfile(
132+
id = AccountIdFactory.create(),
133+
name = "Account Red 1",
134+
color = COLOR_RED,
135+
avatar = Avatar.Icon(name = "icon1"),
136+
)
137+
val ACCOUNT_PROFILE_RED_2 = AccountProfile(
138+
id = AccountIdFactory.create(),
139+
name = "Account Red 2",
140+
color = COLOR_RED,
141+
avatar = Avatar.Icon(name = "icon4"),
142+
)
143+
144+
val ACCOUNT_PROFILE_GREEN_1 = AccountProfile(
145+
id = AccountIdFactory.create(),
146+
name = "Account Green 1",
147+
color = COLOR_GREEN,
148+
avatar = Avatar.Icon(name = "icon2"),
149+
)
150+
151+
val ACCOUNT_PROFILE_GREEN_2 = AccountProfile(
152+
id = AccountIdFactory.create(),
153+
name = "Account Green 2",
154+
color = COLOR_GREEN,
155+
avatar = Avatar.Icon(name = "icon5"),
156+
)
157+
158+
val ACCOUNT_PROFILE_BLUE_1 = AccountProfile(
159+
id = AccountIdFactory.create(),
160+
name = "Account Blue 1",
161+
color = COLOR_BLUE,
162+
avatar = Avatar.Icon(name = "icon3"),
163+
)
164+
}
165+
}

0 commit comments

Comments
 (0)