diff --git a/app-common/build.gradle.kts b/app-common/build.gradle.kts index 373e8e396a0..d2685ffbbbf 100644 --- a/app-common/build.gradle.kts +++ b/app-common/build.gradle.kts @@ -55,6 +55,8 @@ dependencies { implementation(libs.androidx.work.runtime) implementation(libs.androidx.lifecycle.process) + implementation(libs.kotlinx.collections.immutable) testImplementation(projects.feature.account.fake) + testImplementation(projects.core.testing) } diff --git a/app-common/src/main/kotlin/net/thunderbird/app/common/account/AccountColorPicker.kt b/app-common/src/main/kotlin/net/thunderbird/app/common/account/AccountColorPicker.kt index eb9793c8c92..f92cd6fa5d3 100644 --- a/app-common/src/main/kotlin/net/thunderbird/app/common/account/AccountColorPicker.kt +++ b/app-common/src/main/kotlin/net/thunderbird/app/common/account/AccountColorPicker.kt @@ -1,27 +1,26 @@ package net.thunderbird.app.common.account -import android.content.res.Resources -import app.k9mail.core.ui.legacy.theme2.common.R -import net.thunderbird.core.android.account.LegacyAccountDtoManager +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.flow.first +import net.thunderbird.feature.account.profile.AccountProfileRepository internal class AccountColorPicker( - private val accountManager: LegacyAccountDtoManager, - private val resources: Resources, + private val repository: AccountProfileRepository, + private val accountColors: ImmutableList, ) { - fun pickColor(): Int { - val accounts = accountManager.getAccounts() - val usedAccountColors = accounts.map { it.chipColor }.toSet() - val accountColors = resources.getIntArray(R.array.account_colors).toList() + suspend fun pickColor(): Int { + val profiles = repository.getAll().first() + val usedCounts = profiles.groupingBy { it.color }.eachCount() - val availableColors = accountColors - usedAccountColors - if (availableColors.isEmpty()) { - return accountColors.random() + val minCount = accountColors.minOf { usedCounts[it] ?: 0 } + val candidates = accountColors.filter { + (usedCounts[it] ?: 0) == minCount } - val defaultAccountColors = resources.getIntArray(R.array.default_account_colors) - return availableColors.shuffled().minByOrNull { color -> - val index = defaultAccountColors.indexOf(color) - if (index != -1) index else defaultAccountColors.size - } ?: error("availableColors must not be empty") + return if (candidates.isNotEmpty()) { + candidates.shuffled().first() + } else { + accountColors.shuffled().first() + } } } diff --git a/app-common/src/main/kotlin/net/thunderbird/app/common/account/AppCommonAccountModule.kt b/app-common/src/main/kotlin/net/thunderbird/app/common/account/AppCommonAccountModule.kt index 610f2263df9..0e8d056b602 100644 --- a/app-common/src/main/kotlin/net/thunderbird/app/common/account/AppCommonAccountModule.kt +++ b/app-common/src/main/kotlin/net/thunderbird/app/common/account/AppCommonAccountModule.kt @@ -1,6 +1,8 @@ package net.thunderbird.app.common.account import app.k9mail.feature.account.setup.AccountSetupExternalContract +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList import net.thunderbird.app.common.account.data.DefaultAccountProfileLocalDataSource import net.thunderbird.app.common.account.data.DefaultLegacyAccountManager import net.thunderbird.core.android.account.AccountDefaultsProvider @@ -13,8 +15,11 @@ import net.thunderbird.feature.account.core.featureAccountCoreModule import net.thunderbird.feature.account.storage.legacy.featureAccountStorageLegacyModule import net.thunderbird.feature.mail.account.api.AccountManager import org.koin.android.ext.koin.androidApplication +import org.koin.android.ext.koin.androidContext +import org.koin.core.qualifier.named import org.koin.dsl.binds import org.koin.dsl.module +import app.k9mail.core.ui.legacy.theme2.common.R as ThemeCommonR internal val appCommonAccountModule = module { includes( @@ -43,10 +48,16 @@ internal val appCommonAccountModule = module { ) } + factory>(named("AccountColors")) { + androidContext().resources.getIntArray( + ThemeCommonR.array.account_colors, + ).toList().toImmutableList() + } + factory { AccountColorPicker( - accountManager = get(), - resources = get(), + repository = get(), + accountColors = get(named("AccountColors")), ) } diff --git a/app-common/src/main/kotlin/net/thunderbird/app/common/account/data/DefaultAccountProfileLocalDataSource.kt b/app-common/src/main/kotlin/net/thunderbird/app/common/account/data/DefaultAccountProfileLocalDataSource.kt index 03bc948d037..99794cfb138 100644 --- a/app-common/src/main/kotlin/net/thunderbird/app/common/account/data/DefaultAccountProfileLocalDataSource.kt +++ b/app-common/src/main/kotlin/net/thunderbird/app/common/account/data/DefaultAccountProfileLocalDataSource.kt @@ -14,6 +14,15 @@ internal class DefaultAccountProfileLocalDataSource( private val dataMapper: AccountProfileDataMapper, ) : AccountProfileLocalDataSource { + override fun getAll(): Flow> { + return accountManager.getAll() + .map { accounts -> + accounts.map { dto -> + dataMapper.toDomain(dto.profile) + } + } + } + override fun getById(accountId: AccountId): Flow { return accountManager.getById(accountId) .map { account -> diff --git a/app-common/src/test/kotlin/net/thunderbird/app/common/account/AccountColorPickerTest.kt b/app-common/src/test/kotlin/net/thunderbird/app/common/account/AccountColorPickerTest.kt new file mode 100644 index 00000000000..c16028daefe --- /dev/null +++ b/app-common/src/test/kotlin/net/thunderbird/app/common/account/AccountColorPickerTest.kt @@ -0,0 +1,165 @@ +package net.thunderbird.app.common.account + +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isOneOf +import kotlin.test.Test +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runTest +import net.thunderbird.app.common.account.data.FakeAccountProfileRepository +import net.thunderbird.feature.account.AccountIdFactory +import net.thunderbird.feature.account.profile.AccountAvatar +import net.thunderbird.feature.account.profile.AccountProfile + +class AccountColorPickerTest { + + @Test + fun `should pick random color when none used`() = runTest { + // Arrange + val profiles: MutableStateFlow> = MutableStateFlow(emptyList()) + val testSubject = AccountColorPicker( + repository = FakeAccountProfileRepository(profiles), + accountColors = ACCOUNT_COLORS, + ) + + // Act + val result = testSubject.pickColor() + + // Assert + assertThat(result).isOneOf(COLOR_RED, COLOR_GREEN, COLOR_BLUE) + } + + @Test + fun `should pick one of the available colors when some are used`() = runTest { + // Arrange + val profiles: MutableStateFlow> = MutableStateFlow( + listOf( + ACCOUNT_PROFILE_GREEN_1, + ), + ) + val testSubject = AccountColorPicker( + repository = FakeAccountProfileRepository(profiles), + accountColors = ACCOUNT_COLORS, + ) + + // Act + val result = testSubject.pickColor() + + // Assert + assertThat(result).isOneOf(COLOR_RED, COLOR_BLUE) + } + + @Test + fun `should pick last available color when others are used`() = runTest { + // Arrange + val profiles: MutableStateFlow> = MutableStateFlow( + listOf( + ACCOUNT_PROFILE_RED_1, + ACCOUNT_PROFILE_GREEN_1, + ), + ) + val testSubject = AccountColorPicker( + repository = FakeAccountProfileRepository(profiles), + accountColors = ACCOUNT_COLORS, + ) + + // Act + val result = testSubject.pickColor() + + // Assert + assertThat(result).isEqualTo(COLOR_BLUE) + } + + @Test + fun `should pick random color when all colors are used equally`() = runTest { + // Arrange + val profiles: MutableStateFlow> = MutableStateFlow( + listOf( + ACCOUNT_PROFILE_RED_1, + ACCOUNT_PROFILE_GREEN_1, + ACCOUNT_PROFILE_BLUE_1, + ), + ) + val testSubject = AccountColorPicker( + repository = FakeAccountProfileRepository(profiles), + accountColors = ACCOUNT_COLORS, + ) + + // Act + val result = testSubject.pickColor() + + // Assert + assertThat(result).isOneOf(COLOR_RED, COLOR_GREEN, COLOR_BLUE) + } + + @Test + fun `should pick from least used colors when colors are used multiple times`() = runTest { + // Arrange + val profiles: MutableStateFlow> = MutableStateFlow( + listOf( + ACCOUNT_PROFILE_RED_1, + ACCOUNT_PROFILE_RED_2, + ACCOUNT_PROFILE_GREEN_1, + ACCOUNT_PROFILE_GREEN_2, + ACCOUNT_PROFILE_BLUE_1, + ), + ) + val testSubject = AccountColorPicker( + repository = FakeAccountProfileRepository(profiles), + accountColors = ACCOUNT_COLORS, + ) + + // Act + val result = testSubject.pickColor() + + // Assert + assertThat(result).isEqualTo(COLOR_BLUE) + } + + private companion object { + const val COLOR_RED = 0xFF0000 + const val COLOR_GREEN = 0x00FF00 + const val COLOR_BLUE = 0x0000FF + + val ACCOUNT_COLORS = persistentListOf( + COLOR_RED, + COLOR_GREEN, + COLOR_BLUE, + ) + + val ACCOUNT_PROFILE_RED_1 = AccountProfile( + id = AccountIdFactory.create(), + name = "Account Red 1", + color = COLOR_RED, + avatar = AccountAvatar.Icon(name = "icon1"), + ) + val ACCOUNT_PROFILE_RED_2 = AccountProfile( + id = AccountIdFactory.create(), + name = "Account Red 2", + color = COLOR_RED, + avatar = AccountAvatar.Icon(name = "icon4"), + ) + + val ACCOUNT_PROFILE_GREEN_1 = AccountProfile( + id = AccountIdFactory.create(), + name = "Account Green 1", + color = COLOR_GREEN, + avatar = AccountAvatar.Icon(name = "icon2"), + ) + + val ACCOUNT_PROFILE_GREEN_2 = AccountProfile( + id = AccountIdFactory.create(), + name = "Account Green 2", + color = COLOR_GREEN, + avatar = AccountAvatar.Icon(name = "icon5"), + ) + + val ACCOUNT_PROFILE_BLUE_1 = AccountProfile( + id = AccountIdFactory.create(), + name = "Account Blue 1", + color = COLOR_BLUE, + avatar = AccountAvatar.Icon(name = "icon3"), + ) + } +} diff --git a/app-common/src/test/kotlin/net/thunderbird/app/common/account/data/DefaultAccountProfileLocalDataSourceTest.kt b/app-common/src/test/kotlin/net/thunderbird/app/common/account/data/DefaultAccountProfileLocalDataSourceTest.kt index 0f5d4af2519..4696e87c4f4 100644 --- a/app-common/src/test/kotlin/net/thunderbird/app/common/account/data/DefaultAccountProfileLocalDataSourceTest.kt +++ b/app-common/src/test/kotlin/net/thunderbird/app/common/account/data/DefaultAccountProfileLocalDataSourceTest.kt @@ -24,15 +24,47 @@ import org.junit.Test class DefaultAccountProfileLocalDataSourceTest { + @Test + fun `getAll should return all account profiles`() = runTest { + // Arrange + val accountId1 = AccountIdFactory.create() + val legacyAccount1 = createLegacyAccount(accountId1) + val accountProfile1 = createAccountProfile(accountId1) + + val accountId2 = AccountIdFactory.create() + val legacyAccount2 = createLegacyAccount(accountId2) + val accountProfile2 = createAccountProfile(accountId2) + + val testSubject = createTestSubject(listOf(legacyAccount1, legacyAccount2)) + + // Act / Assert + testSubject.getAll().test { + val profiles = awaitItem() + assertThat(profiles).isEqualTo(listOf(accountProfile1, accountProfile2)) + } + } + + @Test + fun `getAll should return empty list when no accounts found`() = runTest { + // Arrange + val testSubject = createTestSubject(emptyList()) + + // Act / Assert + testSubject.getAll().test { + val result = awaitItem() + assertThat(result).isEqualTo(emptyList()) + } + } + @Test fun `getById should return account profile`() = runTest { - // arrange + // Arrange val accountId = AccountIdFactory.create() val legacyAccount = createLegacyAccount(accountId) val accountProfile = createAccountProfile(accountId) - val testSubject = createTestSubject(legacyAccount) + val testSubject = createTestSubject(listOf(legacyAccount)) - // act & assert + // Act / Assert testSubject.getById(accountId).test { assertThat(awaitItem()).isEqualTo(accountProfile) } @@ -40,11 +72,11 @@ class DefaultAccountProfileLocalDataSourceTest { @Test fun `getById should return null when account is not found`() = runTest { - // arrange + // Arrange val accountId = AccountIdFactory.create() - val testSubject = createTestSubject(null) + val testSubject = createTestSubject(emptyList()) - // act & assert + // Act / Assert testSubject.getById(accountId).test { assertThat(awaitItem()).isEqualTo(null) } @@ -52,7 +84,7 @@ class DefaultAccountProfileLocalDataSourceTest { @Test fun `update should save account profile`() = runTest { - // arrange + // Arrange val accountId = AccountIdFactory.create() val legacyAccount = createLegacyAccount(accountId) val accountProfile = createAccountProfile(accountId) @@ -60,9 +92,9 @@ class DefaultAccountProfileLocalDataSourceTest { val updatedName = "updatedName" val updatedAccountProfile = accountProfile.copy(name = updatedName) - val testSubject = createTestSubject(legacyAccount) + val testSubject = createTestSubject(listOf(legacyAccount)) - // act & assert + // Act / Assert testSubject.getById(accountId).test { assertThat(awaitItem()).isEqualTo(accountProfile) @@ -139,15 +171,11 @@ class DefaultAccountProfileLocalDataSourceTest { } private fun createTestSubject( - legacyAccount: LegacyAccount?, + accounts: List, ): DefaultAccountProfileLocalDataSource { return DefaultAccountProfileLocalDataSource( accountManager = FakeLegacyAccountManager( - initialAccounts = if (legacyAccount != null) { - listOf(legacyAccount) - } else { - emptyList() - }, + initialAccounts = accounts, ), dataMapper = DefaultAccountProfileDataMapper( avatarMapper = DefaultAccountAvatarDataMapper(), diff --git a/app-common/src/test/kotlin/net/thunderbird/app/common/account/data/FakeAccountProfileRepository.kt b/app-common/src/test/kotlin/net/thunderbird/app/common/account/data/FakeAccountProfileRepository.kt new file mode 100644 index 00000000000..4042894ae2d --- /dev/null +++ b/app-common/src/test/kotlin/net/thunderbird/app/common/account/data/FakeAccountProfileRepository.kt @@ -0,0 +1,22 @@ +package net.thunderbird.app.common.account.data + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import net.thunderbird.feature.account.AccountId +import net.thunderbird.feature.account.profile.AccountProfile +import net.thunderbird.feature.account.profile.AccountProfileRepository + +class FakeAccountProfileRepository( + val profiles: MutableStateFlow> = MutableStateFlow(emptyList()), +) : AccountProfileRepository { + + override fun getAll(): Flow> = profiles + + override fun getById(accountId: AccountId): Flow { + TODO("Not yet implemented") + } + + override suspend fun update(accountProfile: AccountProfile) { + TODO("Not yet implemented") + } +} diff --git a/core/outcome/src/commonMain/kotlin/net/thunderbird/core/outcome/Outcome.kt b/core/outcome/src/commonMain/kotlin/net/thunderbird/core/outcome/Outcome.kt index 656da2364a8..4aaf1422650 100644 --- a/core/outcome/src/commonMain/kotlin/net/thunderbird/core/outcome/Outcome.kt +++ b/core/outcome/src/commonMain/kotlin/net/thunderbird/core/outcome/Outcome.kt @@ -1,20 +1,56 @@ package net.thunderbird.core.outcome +/** + * A sealed interface representing the outcome of an operation. + * + * @param SUCCESS The type of the value when the operation succeeds. + * @param FAILURE The type of the error when the operation fails. + */ sealed interface Outcome { + + /** + * A successful outcome with a value of type [SUCCESS]. + * + * @param data The value of the successful outcome. + */ data class Success(val data: SUCCESS) : Outcome + + /** + * A failed outcome with an error of type [FAILURE]. + * + * @param error The error of the failed outcome. + * @param cause The cause of the failed outcome. + */ data class Failure( val error: FAILURE, val cause: Any? = null, ) : Outcome + /** + * Whether the outcome is a success. + */ val isSuccess: Boolean get() = this is Success + /** + * Whether the outcome is a failure. + */ val isFailure: Boolean get() = this is Failure companion object { + /** + * Create a [Success] outcome with the given value. + * + * @param data The value of the successful outcome. + */ fun success(data: SUCCESS): Outcome = Success(data) + + /** + * Create a [Failure] outcome with the given error. + * + * @param error The error of the failed outcome. + */ fun failure(error: FAILURE): Outcome = Failure(error) } } diff --git a/core/testing/src/commonMain/kotlin/assertk/assertions/ListExtensions.kt b/core/testing/src/commonMain/kotlin/assertk/assertions/ListExtensions.kt index 2ab42b25287..4940a3f533f 100644 --- a/core/testing/src/commonMain/kotlin/assertk/assertions/ListExtensions.kt +++ b/core/testing/src/commonMain/kotlin/assertk/assertions/ListExtensions.kt @@ -4,6 +4,9 @@ import assertk.Assert import assertk.assertions.support.expected import assertk.assertions.support.show +/** + * Asserts that the list contains no duplicate elements. + */ fun Assert>.containsNoDuplicates() = given { actual -> val seen: MutableSet = mutableSetOf() val duplicates = actual.filter { !seen.add(it) } diff --git a/core/testing/src/commonMain/kotlin/assertk/assertions/ValueExtensions.kt b/core/testing/src/commonMain/kotlin/assertk/assertions/ValueExtensions.kt new file mode 100644 index 00000000000..5b0f9a3d65d --- /dev/null +++ b/core/testing/src/commonMain/kotlin/assertk/assertions/ValueExtensions.kt @@ -0,0 +1,21 @@ +package assertk.assertions + +import assertk.Assert +import assertk.assertions.support.expected +import assertk.assertions.support.show + +/** + * Asserts that the value is one of the expected values. + */ +fun Assert.isOneOf(vararg expectedValues: T) = given { actual -> + if (expectedValues.none { it == actual }) { + expected("to be one of ${show(expectedValues.toList())} but was ${show(actual)}") + } +} + +/** + * Asserts that the value is one of the expected values. + */ +inline fun Assert.isOneOf(expectedValues: Collection) { + isOneOf(*expectedValues.toTypedArray()) +} diff --git a/core/ui/legacy/theme2/common/src/main/res/values/account_colors.xml b/core/ui/legacy/theme2/common/src/main/res/values/account_colors.xml index 976b4ce3eb7..747546bfa67 100644 --- a/core/ui/legacy/theme2/common/src/main/res/values/account_colors.xml +++ b/core/ui/legacy/theme2/common/src/main/res/values/account_colors.xml @@ -21,10 +21,4 @@ @color/material_deep_purple_600 @color/material_blue_gray_700 - - - @color/material_blue_700 - @color/material_pink_500 - @color/material_amber_600 - diff --git a/feature/account/api/src/commonMain/kotlin/net/thunderbird/feature/account/profile/AccountProfileRepository.kt b/feature/account/api/src/commonMain/kotlin/net/thunderbird/feature/account/profile/AccountProfileRepository.kt index 4272db38739..e081899a144 100644 --- a/feature/account/api/src/commonMain/kotlin/net/thunderbird/feature/account/profile/AccountProfileRepository.kt +++ b/feature/account/api/src/commonMain/kotlin/net/thunderbird/feature/account/profile/AccountProfileRepository.kt @@ -3,9 +3,30 @@ package net.thunderbird.feature.account.profile import kotlinx.coroutines.flow.Flow import net.thunderbird.feature.account.AccountId +/** + * Repository interface for managing account profiles. + */ interface AccountProfileRepository { + /** + * Gets all account profiles as a flow. + * + * @return A flow emitting a list of all account profiles. + */ + fun getAll(): Flow> + + /** + * Gets an account profile by its ID as a flow. + * + * @param accountId The ID of the account. + * @return A flow emitting the account profile or null if not found. + */ fun getById(accountId: AccountId): Flow + /** + * Updates the given account profile. + * + * @param accountProfile The account profile to update. + */ suspend fun update(accountProfile: AccountProfile) } diff --git a/feature/account/core/build.gradle.kts b/feature/account/core/build.gradle.kts index 3ed1d376274..f96d2aee3fe 100644 --- a/feature/account/core/build.gradle.kts +++ b/feature/account/core/build.gradle.kts @@ -1,8 +1,15 @@ plugins { - id(ThunderbirdPlugins.Library.jvm) - alias(libs.plugins.android.lint) + id(ThunderbirdPlugins.Library.kmp) } -dependencies { - api(projects.feature.account.api) +android { + namespace = "net.thunderbird.feature.account.core" +} + +kotlin { + sourceSets { + commonMain.dependencies { + api(projects.feature.account.api) + } + } } diff --git a/feature/account/core/src/commonMain/kotlin/net/thunderbird/feature/account/core/AccountCoreExternalContract.kt b/feature/account/core/src/commonMain/kotlin/net/thunderbird/feature/account/core/AccountCoreExternalContract.kt new file mode 100644 index 00000000000..be8b5515634 --- /dev/null +++ b/feature/account/core/src/commonMain/kotlin/net/thunderbird/feature/account/core/AccountCoreExternalContract.kt @@ -0,0 +1,36 @@ +package net.thunderbird.feature.account.core + +import kotlinx.coroutines.flow.Flow +import net.thunderbird.feature.account.AccountId +import net.thunderbird.feature.account.profile.AccountProfile + +interface AccountCoreExternalContract { + + /** + * Local data source for account profiles. + */ + interface AccountProfileLocalDataSource { + + /** + * Gets all account profiles as a flow. + * + * @return A flow emitting a list of all account profiles. + */ + fun getAll(): Flow> + + /** + * Gets an account profile by its ID as a flow. + * + * @param accountId The ID of the account. + * @return A flow emitting the account profile or null if not found. + */ + fun getById(accountId: AccountId): Flow + + /** + * Updates the given account profile. + * + * @param accountProfile The account profile to update. + */ + suspend fun update(accountProfile: AccountProfile) + } +} diff --git a/feature/account/core/src/main/kotlin/net/thunderbird/feature/account/core/AccountCoreModule.kt b/feature/account/core/src/commonMain/kotlin/net/thunderbird/feature/account/core/AccountCoreModule.kt similarity index 100% rename from feature/account/core/src/main/kotlin/net/thunderbird/feature/account/core/AccountCoreModule.kt rename to feature/account/core/src/commonMain/kotlin/net/thunderbird/feature/account/core/AccountCoreModule.kt diff --git a/feature/account/core/src/main/kotlin/net/thunderbird/feature/account/core/data/DefaultAccountProfileRepository.kt b/feature/account/core/src/commonMain/kotlin/net/thunderbird/feature/account/core/data/DefaultAccountProfileRepository.kt similarity index 85% rename from feature/account/core/src/main/kotlin/net/thunderbird/feature/account/core/data/DefaultAccountProfileRepository.kt rename to feature/account/core/src/commonMain/kotlin/net/thunderbird/feature/account/core/data/DefaultAccountProfileRepository.kt index e4e67342c6b..8d89ba95f56 100644 --- a/feature/account/core/src/main/kotlin/net/thunderbird/feature/account/core/data/DefaultAccountProfileRepository.kt +++ b/feature/account/core/src/commonMain/kotlin/net/thunderbird/feature/account/core/data/DefaultAccountProfileRepository.kt @@ -11,6 +11,11 @@ class DefaultAccountProfileRepository( private val localDataSource: AccountProfileLocalDataSource, ) : AccountProfileRepository { + override fun getAll(): Flow> { + return localDataSource.getAll() + .distinctUntilChanged() + } + override fun getById(accountId: AccountId): Flow { return localDataSource.getById(accountId) .distinctUntilChanged() diff --git a/feature/account/core/src/commonTest/kotlin/net/thunderbird/feature/account/core/data/DefaultAccountProfileRepositoryTest.kt b/feature/account/core/src/commonTest/kotlin/net/thunderbird/feature/account/core/data/DefaultAccountProfileRepositoryTest.kt new file mode 100644 index 00000000000..363369c2f2e --- /dev/null +++ b/feature/account/core/src/commonTest/kotlin/net/thunderbird/feature/account/core/data/DefaultAccountProfileRepositoryTest.kt @@ -0,0 +1,116 @@ +package net.thunderbird.feature.account.core.data + +import app.cash.turbine.test +import assertk.assertThat +import assertk.assertions.isEqualTo +import kotlin.collections.emptyList +import kotlin.test.Test +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runTest +import net.thunderbird.feature.account.AccountIdFactory +import net.thunderbird.feature.account.profile.AccountAvatar +import net.thunderbird.feature.account.profile.AccountProfile + +class DefaultAccountProfileRepositoryTest { + + @Test + fun `getAll should return distinct values`() = runTest { + // Arrange + val profiles: MutableStateFlow> = MutableStateFlow(emptyList()) + val list1 = listOf(PROFILE_1) + val list2 = listOf(PROFILE_1, PROFILE_2) + val testSubject = DefaultAccountProfileRepository( + localDataSource = FakeAccountProfileDataSource( + profiles = profiles, + ), + ) + + // Act / Assert + testSubject.getAll().test { + val emptyResult = awaitItem() + assertThat(emptyResult).isEqualTo(emptyList()) + + profiles.value = list1 + val firstResult = awaitItem() + assertThat(firstResult).isEqualTo(list1) + + profiles.value = list1 + expectNoEvents() + + profiles.value = list2 + val secondResult = awaitItem() + assertThat(secondResult).isEqualTo(list2) + + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `getById should return distinct values`() = runTest { + // Arrange + val profiles: MutableStateFlow> = MutableStateFlow(emptyList()) + val list1 = listOf(PROFILE_1, PROFILE_2) + val testSubject = DefaultAccountProfileRepository( + localDataSource = FakeAccountProfileDataSource( + profiles = profiles, + ), + ) + + // Act / Assert + testSubject.getById(PROFILE_ID_2).test { + val nullResult = awaitItem() + assertThat(nullResult).isEqualTo(null) + + profiles.value = list1 + val firstResult = awaitItem() + assertThat(firstResult).isEqualTo(PROFILE_2) + + profiles.value = list1 + expectNoEvents() + + profiles.value = emptyList() + val secondResult = awaitItem() + assertThat(secondResult).isEqualTo(null) + + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `update should call local data source update`() = runTest { + // Arrange + var updatedProfile: AccountProfile? = null + val testSubject = DefaultAccountProfileRepository( + localDataSource = object : FakeAccountProfileDataSource() { + override suspend fun update(accountProfile: AccountProfile) { + updatedProfile = accountProfile + } + }, + ) + val profile = PROFILE_1.copy(name = "Updated Name") + + // Act + testSubject.update(profile) + + // Assert + assertThat(updatedProfile).isEqualTo(profile) + } + + private companion object { + val PROFILE_ID_1 = AccountIdFactory.create() + val PROFILE_ID_2 = AccountIdFactory.create() + + val PROFILE_1 = AccountProfile( + id = PROFILE_ID_1, + name = "Profile 1", + color = 0xFF0000, + avatar = AccountAvatar.Icon(name = "icon-1"), + ) + val PROFILE_2 = AccountProfile( + id = PROFILE_ID_2, + name = "Profile 2", + color = 0x00FF00, + avatar = AccountAvatar.Monogram(value = "AB"), + ) + } +} diff --git a/feature/account/core/src/commonTest/kotlin/net/thunderbird/feature/account/core/data/FakeAccountProfileDataSource.kt b/feature/account/core/src/commonTest/kotlin/net/thunderbird/feature/account/core/data/FakeAccountProfileDataSource.kt new file mode 100644 index 00000000000..34f0d253720 --- /dev/null +++ b/feature/account/core/src/commonTest/kotlin/net/thunderbird/feature/account/core/data/FakeAccountProfileDataSource.kt @@ -0,0 +1,25 @@ +package net.thunderbird.feature.account.core.data + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.map +import net.thunderbird.feature.account.AccountId +import net.thunderbird.feature.account.core.AccountCoreExternalContract +import net.thunderbird.feature.account.profile.AccountProfile + +open class FakeAccountProfileDataSource( + val profiles: MutableStateFlow> = MutableStateFlow(emptyList()), +) : AccountCoreExternalContract.AccountProfileLocalDataSource { + + override fun getAll(): Flow> = profiles + + override fun getById(accountId: AccountId): Flow { + return profiles.map { list -> + list.find { it.id == accountId } + } + } + + override suspend fun update(accountProfile: AccountProfile) { + TODO("Not yet implemented") + } +} diff --git a/feature/account/core/src/main/kotlin/net/thunderbird/feature/account/core/AccountCoreExternalContract.kt b/feature/account/core/src/main/kotlin/net/thunderbird/feature/account/core/AccountCoreExternalContract.kt deleted file mode 100644 index 207441ffdcd..00000000000 --- a/feature/account/core/src/main/kotlin/net/thunderbird/feature/account/core/AccountCoreExternalContract.kt +++ /dev/null @@ -1,14 +0,0 @@ -package net.thunderbird.feature.account.core - -import kotlinx.coroutines.flow.Flow -import net.thunderbird.feature.account.AccountId -import net.thunderbird.feature.account.profile.AccountProfile - -interface AccountCoreExternalContract { - - interface AccountProfileLocalDataSource { - fun getById(accountId: AccountId): Flow - - suspend fun update(accountProfile: AccountProfile) - } -} diff --git a/feature/account/settings/impl/build.gradle.kts b/feature/account/settings/impl/build.gradle.kts index 44002a750cc..cbf9eff838c 100644 --- a/feature/account/settings/impl/build.gradle.kts +++ b/feature/account/settings/impl/build.gradle.kts @@ -28,7 +28,6 @@ dependencies { implementation(projects.core.logging.implLegacy) implementation(projects.core.ui.compose.designsystem) implementation(projects.core.ui.compose.navigation) - implementation(projects.core.ui.legacy.theme2.common) debugImplementation(projects.core.ui.setting.implDialog) diff --git a/feature/account/settings/impl/src/main/kotlin/net/thunderbird/feature/account/settings/AccountSettingsModule.kt b/feature/account/settings/impl/src/main/kotlin/net/thunderbird/feature/account/settings/AccountSettingsModule.kt index 455b1915a48..3dccba6c3ce 100644 --- a/feature/account/settings/impl/src/main/kotlin/net/thunderbird/feature/account/settings/AccountSettingsModule.kt +++ b/feature/account/settings/impl/src/main/kotlin/net/thunderbird/feature/account/settings/AccountSettingsModule.kt @@ -11,6 +11,7 @@ import net.thunderbird.feature.account.settings.impl.ui.general.GeneralResourceP import net.thunderbird.feature.account.settings.impl.ui.general.GeneralSettingsViewModel import org.koin.android.ext.koin.androidContext import org.koin.core.module.dsl.viewModel +import org.koin.core.qualifier.named import org.koin.dsl.module val featureAccountSettingsModule = module { @@ -19,6 +20,7 @@ val featureAccountSettingsModule = module { factory { GeneralResourceProvider( context = androidContext(), + colors = get(named("AccountColors")), ) } diff --git a/feature/account/settings/impl/src/main/kotlin/net/thunderbird/feature/account/settings/impl/ui/general/GeneralResourceProvider.kt b/feature/account/settings/impl/src/main/kotlin/net/thunderbird/feature/account/settings/impl/ui/general/GeneralResourceProvider.kt index c2890c12228..e1ca931e1e6 100644 --- a/feature/account/settings/impl/src/main/kotlin/net/thunderbird/feature/account/settings/impl/ui/general/GeneralResourceProvider.kt +++ b/feature/account/settings/impl/src/main/kotlin/net/thunderbird/feature/account/settings/impl/ui/general/GeneralResourceProvider.kt @@ -6,14 +6,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList import net.thunderbird.feature.account.settings.R import net.thunderbird.feature.account.settings.impl.domain.AccountSettingsDomainContract.ResourceProvider import net.thunderbird.feature.account.settings.impl.ui.general.components.GeneralSettingsProfileView -import app.k9mail.core.ui.legacy.theme2.common.R as ThunderbirdCommonR internal class GeneralResourceProvider( private val context: Context, + override val colors: ImmutableList, ) : ResourceProvider.GeneralResourceProvider { override fun profileUi( @@ -59,6 +58,4 @@ internal class GeneralResourceProvider( context.getString(R.string.account_settings_general_color_description) } override val colorIcon: () -> ImageVector? = { null } - override val colors: ImmutableList = context.resources.getIntArray(ThunderbirdCommonR.array.account_colors) - .toList().toImmutableList() } diff --git a/feature/account/settings/impl/src/test/kotlin/net/thunderbird/feature/account/settings/impl/domain/usecase/FakeAccountProfileRepository.kt b/feature/account/settings/impl/src/test/kotlin/net/thunderbird/feature/account/settings/impl/domain/usecase/FakeAccountProfileRepository.kt index 245c122f997..3e2d472cd00 100644 --- a/feature/account/settings/impl/src/test/kotlin/net/thunderbird/feature/account/settings/impl/domain/usecase/FakeAccountProfileRepository.kt +++ b/feature/account/settings/impl/src/test/kotlin/net/thunderbird/feature/account/settings/impl/domain/usecase/FakeAccountProfileRepository.kt @@ -12,9 +12,13 @@ internal class FakeAccountProfileRepository( initialAccountProfile: AccountProfile? = null, ) : AccountProfileRepository { - private val accountProfileState = MutableStateFlow(initialAccountProfile) + private val accountProfileState = MutableStateFlow(initialAccountProfile) private val accountProfile: StateFlow = accountProfileState + override fun getAll(): Flow> { + TODO("Not yet implemented") + } + override fun getById(accountId: AccountId): Flow { return accountProfile }