Repo created
This commit is contained in:
parent
75dc487a7a
commit
39c29d175b
6317 changed files with 388324 additions and 2 deletions
12
feature/account/settings/api/build.gradle.kts
Normal file
12
feature/account/settings/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
plugins {
|
||||
id(ThunderbirdPlugins.Library.androidCompose)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "net.thunderbird.feature.account.settings.api"
|
||||
resourcePrefix = "account_settings_api_"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core.ui.compose.navigation)
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package net.thunderbird.feature.account.settings.api
|
||||
|
||||
import app.k9mail.core.ui.compose.navigation.Navigation
|
||||
|
||||
interface AccountSettingsNavigation : Navigation<AccountSettingsRoute>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package net.thunderbird.feature.account.settings.api
|
||||
|
||||
import app.k9mail.core.ui.compose.navigation.Route
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
sealed interface AccountSettingsRoute : Route {
|
||||
|
||||
@Serializable
|
||||
data class GeneralSettings(val accountId: String) : AccountSettingsRoute {
|
||||
override val basePath: String = BASE_PATH
|
||||
|
||||
override fun route(): String = "$basePath/$accountId"
|
||||
|
||||
companion object {
|
||||
const val BASE_PATH = "$ACCOUNT_SETTINGS_BASE_PATH/general"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ACCOUNT_SETTINGS_BASE_PATH = "app://account/settings"
|
||||
}
|
||||
}
|
||||
25
feature/account/settings/impl/build.gradle.kts
Normal file
25
feature/account/settings/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
plugins {
|
||||
id(ThunderbirdPlugins.Library.androidCompose)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "net.thunderbird.feature.account.settings"
|
||||
resourcePrefix = "account_settings_"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.feature.account.settings.api)
|
||||
implementation(projects.feature.account.core)
|
||||
implementation(projects.feature.account.avatar.impl)
|
||||
|
||||
implementation(projects.core.outcome)
|
||||
|
||||
implementation(projects.core.logging.implLegacy)
|
||||
implementation(projects.core.ui.compose.designsystem)
|
||||
implementation(projects.core.ui.compose.navigation)
|
||||
implementation(projects.core.ui.compose.preference)
|
||||
implementation(projects.core.ui.legacy.theme2.common)
|
||||
|
||||
testImplementation(projects.core.logging.testing)
|
||||
testImplementation(projects.core.ui.compose.testing)
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
package net.thunderbird.feature.account.settings.impl.ui.fake
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.card.CardElevated
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.icon.Icons
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodyLarge
|
||||
import app.k9mail.core.ui.compose.theme2.MainTheme
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import net.thunderbird.core.ui.compose.preference.api.PreferenceDisplay
|
||||
import net.thunderbird.core.ui.compose.preference.api.PreferenceSetting
|
||||
|
||||
object FakePreferenceData {
|
||||
|
||||
val textPreference = PreferenceSetting.Text(
|
||||
id = "text",
|
||||
icon = { Icons.Outlined.Info },
|
||||
title = { "Title" },
|
||||
description = { "Description" },
|
||||
value = "Value",
|
||||
)
|
||||
|
||||
val colorPreference = PreferenceSetting.Color(
|
||||
id = "color",
|
||||
icon = { Icons.Outlined.Info },
|
||||
title = { "Title" },
|
||||
description = { "Description" },
|
||||
value = 0xFFFF0000.toInt(),
|
||||
colors = persistentListOf(
|
||||
0xFFFF0000.toInt(),
|
||||
0xFF00FF00.toInt(),
|
||||
0xFF0000FF.toInt(),
|
||||
),
|
||||
)
|
||||
|
||||
val customPreference = PreferenceDisplay.Custom(
|
||||
id = "custom",
|
||||
customUi = { modifier ->
|
||||
CardElevated(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(MainTheme.spacings.double),
|
||||
) {
|
||||
TextBodyLarge(
|
||||
text = "Custom UI",
|
||||
modifier = Modifier
|
||||
.padding(MainTheme.spacings.default)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
val sectionDivider = PreferenceDisplay.SectionDivider(
|
||||
id = "section_divider",
|
||||
)
|
||||
|
||||
val sectionHeader = PreferenceDisplay.SectionHeader(
|
||||
id = "section_header",
|
||||
title = { "Section Title" },
|
||||
color = { Color.Black },
|
||||
)
|
||||
|
||||
val preferences = persistentListOf(
|
||||
textPreference,
|
||||
colorPreference,
|
||||
customPreference,
|
||||
sectionHeader,
|
||||
sectionDivider,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package net.thunderbird.feature.account.settings.impl.ui.general
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import app.k9mail.core.ui.compose.designsystem.PreviewWithTheme
|
||||
import net.thunderbird.feature.account.settings.impl.ui.fake.FakePreferenceData
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun GeneralSettingsContentPreview() {
|
||||
PreviewWithTheme {
|
||||
GeneralSettingsContent(
|
||||
state = GeneralSettingsContract.State(
|
||||
subtitle = "Subtitle",
|
||||
preferences = FakePreferenceData.preferences,
|
||||
),
|
||||
onEvent = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package net.thunderbird.feature.account.settings.impl.ui.general.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import app.k9mail.core.ui.compose.designsystem.PreviewWithThemes
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun GeneralSettingsProfileViewPreview() {
|
||||
PreviewWithThemes {
|
||||
GeneralSettingsProfileView(
|
||||
name = "Name",
|
||||
email = "demo@example.com",
|
||||
color = Color.Green,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
internal fun GeneralSettingsProfileViewWithLongTextPreview() {
|
||||
PreviewWithThemes {
|
||||
GeneralSettingsProfileView(
|
||||
name = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut " +
|
||||
"labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris " +
|
||||
"nisi ut aliquip ex ea commodo consequat.",
|
||||
email = "verylongemailaddress@exampledomainwithaverylongname.com",
|
||||
color = Color.Green,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package net.thunderbird.feature.account.settings
|
||||
|
||||
import net.thunderbird.feature.account.settings.api.AccountSettingsNavigation
|
||||
import net.thunderbird.feature.account.settings.impl.DefaultAccountSettingsNavigation
|
||||
import net.thunderbird.feature.account.settings.impl.domain.AccountSettingsDomainContract.ResourceProvider
|
||||
import net.thunderbird.feature.account.settings.impl.domain.AccountSettingsDomainContract.UseCase
|
||||
import net.thunderbird.feature.account.settings.impl.domain.usecase.GetAccountName
|
||||
import net.thunderbird.feature.account.settings.impl.domain.usecase.GetGeneralPreferences
|
||||
import net.thunderbird.feature.account.settings.impl.domain.usecase.UpdateGeneralPreferences
|
||||
import net.thunderbird.feature.account.settings.impl.ui.general.GeneralResourceProvider
|
||||
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.dsl.module
|
||||
|
||||
val featureAccountSettingsModule = module {
|
||||
single<AccountSettingsNavigation> { DefaultAccountSettingsNavigation() }
|
||||
|
||||
factory<ResourceProvider.GeneralResourceProvider> {
|
||||
GeneralResourceProvider(
|
||||
context = androidContext(),
|
||||
)
|
||||
}
|
||||
|
||||
factory<UseCase.GetAccountName> {
|
||||
GetAccountName(
|
||||
repository = get(),
|
||||
)
|
||||
}
|
||||
|
||||
factory<UseCase.GetGeneralPreferences> {
|
||||
GetGeneralPreferences(
|
||||
repository = get(),
|
||||
resourceProvider = get(),
|
||||
)
|
||||
}
|
||||
|
||||
factory<UseCase.UpdateGeneralPreferences> {
|
||||
UpdateGeneralPreferences(
|
||||
repository = get(),
|
||||
)
|
||||
}
|
||||
|
||||
viewModel { params ->
|
||||
GeneralSettingsViewModel(
|
||||
accountId = params.get(),
|
||||
getAccountName = get(),
|
||||
getGeneralPreferences = get(),
|
||||
updateGeneralPreferences = get(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package net.thunderbird.feature.account.settings.impl
|
||||
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.toRoute
|
||||
import app.k9mail.core.ui.compose.navigation.deepLinkComposable
|
||||
import net.thunderbird.feature.account.AccountIdFactory
|
||||
import net.thunderbird.feature.account.settings.api.AccountSettingsNavigation
|
||||
import net.thunderbird.feature.account.settings.api.AccountSettingsRoute
|
||||
import net.thunderbird.feature.account.settings.impl.ui.general.GeneralSettingsScreen
|
||||
|
||||
internal class DefaultAccountSettingsNavigation : AccountSettingsNavigation {
|
||||
|
||||
override fun registerRoutes(
|
||||
navGraphBuilder: NavGraphBuilder,
|
||||
onBack: () -> Unit,
|
||||
onFinish: (AccountSettingsRoute) -> Unit,
|
||||
) {
|
||||
with(navGraphBuilder) {
|
||||
deepLinkComposable<AccountSettingsRoute.GeneralSettings>(
|
||||
basePath = AccountSettingsRoute.GeneralSettings.Companion.BASE_PATH,
|
||||
) { backStackEntry ->
|
||||
val generalSettingsRoute = backStackEntry.toRoute<AccountSettingsRoute.GeneralSettings>()
|
||||
val accountId = AccountIdFactory.of(generalSettingsRoute.accountId)
|
||||
|
||||
GeneralSettingsScreen(
|
||||
accountId = accountId,
|
||||
onBack = onBack,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package net.thunderbird.feature.account.settings.impl.domain
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import net.thunderbird.core.outcome.Outcome
|
||||
import net.thunderbird.core.ui.compose.preference.api.Preference
|
||||
import net.thunderbird.core.ui.compose.preference.api.PreferenceSetting
|
||||
import net.thunderbird.feature.account.AccountId
|
||||
import net.thunderbird.feature.account.settings.impl.domain.AccountSettingsDomainContract.SettingsError
|
||||
|
||||
internal typealias AccountNameOutcome = Outcome<String, SettingsError>
|
||||
internal typealias AccountSettingsOutcome = Outcome<ImmutableList<Preference>, SettingsError>
|
||||
|
||||
internal interface AccountSettingsDomainContract {
|
||||
|
||||
interface UseCase {
|
||||
|
||||
fun interface GetAccountName {
|
||||
operator fun invoke(accountId: AccountId): Flow<AccountNameOutcome>
|
||||
}
|
||||
|
||||
fun interface GetGeneralPreferences {
|
||||
operator fun invoke(accountId: AccountId): Flow<AccountSettingsOutcome>
|
||||
}
|
||||
|
||||
fun interface UpdateGeneralPreferences {
|
||||
suspend operator fun invoke(
|
||||
accountId: AccountId,
|
||||
preference: PreferenceSetting<*>,
|
||||
): Outcome<Unit, SettingsError>
|
||||
}
|
||||
}
|
||||
|
||||
interface ResourceProvider {
|
||||
interface GeneralResourceProvider {
|
||||
fun profileUi(
|
||||
name: String,
|
||||
color: Int,
|
||||
): @Composable (Modifier) -> Unit
|
||||
|
||||
val nameTitle: () -> String
|
||||
val nameDescription: () -> String?
|
||||
val nameIcon: () -> ImageVector?
|
||||
|
||||
val colorTitle: () -> String
|
||||
val colorDescription: () -> String?
|
||||
val colorIcon: () -> ImageVector?
|
||||
val colors: ImmutableList<Int>
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface SettingsError {
|
||||
data class NotFound(
|
||||
val message: String,
|
||||
) : SettingsError
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package net.thunderbird.feature.account.settings.impl.domain.entity
|
||||
|
||||
import net.thunderbird.feature.account.AccountId
|
||||
|
||||
internal enum class GeneralPreference {
|
||||
PROFILE,
|
||||
NAME,
|
||||
COLOR,
|
||||
}
|
||||
|
||||
internal fun GeneralPreference.generateId(accountId: AccountId): String {
|
||||
return "${accountId.asRaw()}-general-${this.name.lowercase()}"
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package net.thunderbird.feature.account.settings.impl.domain.usecase
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import net.thunderbird.core.outcome.Outcome
|
||||
import net.thunderbird.feature.account.AccountId
|
||||
import net.thunderbird.feature.account.profile.AccountProfileRepository
|
||||
import net.thunderbird.feature.account.settings.impl.domain.AccountNameOutcome
|
||||
import net.thunderbird.feature.account.settings.impl.domain.AccountSettingsDomainContract
|
||||
import net.thunderbird.feature.account.settings.impl.domain.AccountSettingsDomainContract.UseCase
|
||||
|
||||
internal class GetAccountName(
|
||||
private val repository: AccountProfileRepository,
|
||||
) : UseCase.GetAccountName {
|
||||
|
||||
override fun invoke(accountId: AccountId): Flow<AccountNameOutcome> {
|
||||
return repository.getById(accountId).map { profile ->
|
||||
if (profile != null) {
|
||||
Outcome.success(profile.name)
|
||||
} else {
|
||||
Outcome.failure(
|
||||
AccountSettingsDomainContract.SettingsError.NotFound(
|
||||
message = "Account profile not found for accountId: ${accountId.asRaw()}",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
package net.thunderbird.feature.account.settings.impl.domain.usecase
|
||||
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import net.thunderbird.core.outcome.Outcome
|
||||
import net.thunderbird.core.ui.compose.preference.api.Preference
|
||||
import net.thunderbird.core.ui.compose.preference.api.PreferenceDisplay
|
||||
import net.thunderbird.core.ui.compose.preference.api.PreferenceSetting
|
||||
import net.thunderbird.feature.account.AccountId
|
||||
import net.thunderbird.feature.account.profile.AccountProfile
|
||||
import net.thunderbird.feature.account.profile.AccountProfileRepository
|
||||
import net.thunderbird.feature.account.settings.impl.domain.AccountSettingsDomainContract.ResourceProvider
|
||||
import net.thunderbird.feature.account.settings.impl.domain.AccountSettingsDomainContract.SettingsError
|
||||
import net.thunderbird.feature.account.settings.impl.domain.AccountSettingsDomainContract.UseCase
|
||||
import net.thunderbird.feature.account.settings.impl.domain.AccountSettingsOutcome
|
||||
import net.thunderbird.feature.account.settings.impl.domain.entity.GeneralPreference
|
||||
import net.thunderbird.feature.account.settings.impl.domain.entity.generateId
|
||||
|
||||
internal class GetGeneralPreferences(
|
||||
private val repository: AccountProfileRepository,
|
||||
private val resourceProvider: ResourceProvider.GeneralResourceProvider,
|
||||
) : UseCase.GetGeneralPreferences {
|
||||
override fun invoke(accountId: AccountId): Flow<AccountSettingsOutcome> {
|
||||
return repository.getById(accountId).map { profile ->
|
||||
if (profile != null) {
|
||||
Outcome.success(generatePreferences(accountId, profile))
|
||||
} else {
|
||||
Outcome.failure(
|
||||
SettingsError.NotFound(
|
||||
message = "Account profile not found for accountId: ${accountId.asRaw()}",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun generatePreferences(accountId: AccountId, profile: AccountProfile): ImmutableList<Preference> {
|
||||
return persistentListOf(
|
||||
PreferenceDisplay.Custom(
|
||||
id = GeneralPreference.PROFILE.generateId(accountId),
|
||||
customUi = resourceProvider.profileUi(
|
||||
name = profile.name,
|
||||
color = profile.color,
|
||||
),
|
||||
),
|
||||
PreferenceSetting.Text(
|
||||
id = GeneralPreference.NAME.generateId(accountId),
|
||||
title = resourceProvider.nameTitle,
|
||||
description = resourceProvider.nameDescription,
|
||||
icon = resourceProvider.nameIcon,
|
||||
value = profile.name,
|
||||
),
|
||||
PreferenceSetting.Color(
|
||||
id = GeneralPreference.COLOR.generateId(accountId),
|
||||
title = resourceProvider.colorTitle,
|
||||
description = resourceProvider.colorDescription,
|
||||
icon = resourceProvider.colorIcon,
|
||||
value = profile.color,
|
||||
colors = resourceProvider.colors,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package net.thunderbird.feature.account.settings.impl.domain.usecase
|
||||
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import net.thunderbird.core.outcome.Outcome
|
||||
import net.thunderbird.core.ui.compose.preference.api.PreferenceSetting
|
||||
import net.thunderbird.feature.account.AccountId
|
||||
import net.thunderbird.feature.account.profile.AccountProfile
|
||||
import net.thunderbird.feature.account.profile.AccountProfileRepository
|
||||
import net.thunderbird.feature.account.settings.impl.domain.AccountSettingsDomainContract.SettingsError
|
||||
import net.thunderbird.feature.account.settings.impl.domain.AccountSettingsDomainContract.UseCase
|
||||
import net.thunderbird.feature.account.settings.impl.domain.entity.GeneralPreference
|
||||
import net.thunderbird.feature.account.settings.impl.domain.entity.generateId
|
||||
|
||||
internal class UpdateGeneralPreferences(
|
||||
private val repository: AccountProfileRepository,
|
||||
) : UseCase.UpdateGeneralPreferences {
|
||||
override suspend fun invoke(
|
||||
accountId: AccountId,
|
||||
preference: PreferenceSetting<*>,
|
||||
): Outcome<Unit, SettingsError> {
|
||||
return when (preference.id) {
|
||||
GeneralPreference.NAME.generateId(accountId) -> {
|
||||
updateAccountProfile(accountId) {
|
||||
copy(name = preference.value as String)
|
||||
}
|
||||
}
|
||||
|
||||
GeneralPreference.COLOR.generateId(accountId) -> {
|
||||
updateAccountProfile(accountId) {
|
||||
copy(color = preference.value as Int)
|
||||
}
|
||||
}
|
||||
|
||||
else -> Outcome.failure(
|
||||
SettingsError.NotFound(
|
||||
message = "Unknown preference id: ${preference.id}",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateAccountProfile(
|
||||
accountId: AccountId,
|
||||
update: AccountProfile.() -> AccountProfile,
|
||||
): Outcome<Unit, SettingsError> {
|
||||
val accountProfile = repository.getById(accountId).firstOrNull()
|
||||
?: return Outcome.failure(
|
||||
SettingsError.NotFound(
|
||||
message = "Account profile not found for accountId: $accountId",
|
||||
),
|
||||
)
|
||||
val updatedAccountProfile = update(accountProfile)
|
||||
|
||||
repository.update(updatedAccountProfile)
|
||||
|
||||
return Outcome.success(Unit)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package net.thunderbird.feature.account.settings.impl.ui.general
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Composable
|
||||
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,
|
||||
) : ResourceProvider.GeneralResourceProvider {
|
||||
|
||||
override fun profileUi(
|
||||
name: String,
|
||||
color: Int,
|
||||
): @Composable ((Modifier) -> Unit) = { modifier ->
|
||||
GeneralSettingsProfileView(
|
||||
name = name,
|
||||
email = null,
|
||||
color = Color(color),
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
override val nameTitle: () -> String = {
|
||||
context.getString(R.string.account_settings_general_name_title)
|
||||
}
|
||||
override val nameDescription: () -> String? = {
|
||||
context.getString(R.string.account_settings_general_name_description)
|
||||
}
|
||||
override val nameIcon: () -> ImageVector? = { null }
|
||||
|
||||
override val colorTitle: () -> String = {
|
||||
context.getString(R.string.account_settings_general_color_title)
|
||||
}
|
||||
override val colorDescription: () -> String? = {
|
||||
context.getString(R.string.account_settings_general_color_description)
|
||||
}
|
||||
override val colorIcon: () -> ImageVector? = { null }
|
||||
override val colors: ImmutableList<Int> = context.resources.getIntArray(ThunderbirdCommonR.array.account_colors)
|
||||
.toList().toImmutableList()
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package net.thunderbird.feature.account.settings.impl.ui.general
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import net.thunderbird.core.ui.compose.preference.ui.PreferenceView
|
||||
import net.thunderbird.feature.account.settings.R
|
||||
import net.thunderbird.feature.account.settings.impl.ui.general.GeneralSettingsContract.Event
|
||||
import net.thunderbird.feature.account.settings.impl.ui.general.GeneralSettingsContract.State
|
||||
|
||||
@Composable
|
||||
internal fun GeneralSettingsContent(
|
||||
state: State,
|
||||
onEvent: (Event) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
PreferenceView(
|
||||
title = stringResource(R.string.account_settings_general_title),
|
||||
subtitle = state.subtitle,
|
||||
preferences = state.preferences,
|
||||
onPreferenceChange = { preference ->
|
||||
onEvent(Event.OnPreferenceSettingChange(preference))
|
||||
},
|
||||
onBack = { onEvent(Event.OnBackPressed) },
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package net.thunderbird.feature.account.settings.impl.ui.general
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import app.k9mail.core.ui.compose.common.mvi.UnidirectionalViewModel
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import net.thunderbird.core.ui.compose.preference.api.Preference
|
||||
import net.thunderbird.core.ui.compose.preference.api.PreferenceSetting
|
||||
|
||||
internal interface GeneralSettingsContract {
|
||||
|
||||
interface ViewModel : UnidirectionalViewModel<State, Event, Effect>
|
||||
|
||||
@Stable
|
||||
data class State(
|
||||
val subtitle: String? = null,
|
||||
val preferences: ImmutableList<Preference> = persistentListOf<Preference>(),
|
||||
)
|
||||
|
||||
sealed interface Event {
|
||||
data class OnPreferenceSettingChange(
|
||||
val preference: PreferenceSetting<*>,
|
||||
) : Event
|
||||
|
||||
data object OnBackPressed : Event
|
||||
}
|
||||
|
||||
sealed interface Effect {
|
||||
object NavigateBack : Effect
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package net.thunderbird.feature.account.settings.impl.ui.general
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import app.k9mail.core.ui.compose.common.mvi.observe
|
||||
import net.thunderbird.feature.account.AccountId
|
||||
import net.thunderbird.feature.account.settings.impl.ui.general.GeneralSettingsContract.Effect
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
|
||||
@Composable
|
||||
internal fun GeneralSettingsScreen(
|
||||
accountId: AccountId,
|
||||
onBack: () -> Unit,
|
||||
viewModel: GeneralSettingsContract.ViewModel = koinViewModel<GeneralSettingsViewModel> {
|
||||
parametersOf(accountId)
|
||||
},
|
||||
) {
|
||||
val (state, dispatch) = viewModel.observe { effect ->
|
||||
when (effect) {
|
||||
is Effect.NavigateBack -> onBack()
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler(onBack = onBack)
|
||||
|
||||
GeneralSettingsContent(
|
||||
state = state.value,
|
||||
onEvent = { dispatch(it) },
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
package net.thunderbird.feature.account.settings.impl.ui.general
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import net.thunderbird.core.logging.legacy.Log
|
||||
import net.thunderbird.core.outcome.handle
|
||||
import net.thunderbird.core.ui.compose.preference.api.PreferenceSetting
|
||||
import net.thunderbird.feature.account.AccountId
|
||||
import net.thunderbird.feature.account.settings.impl.domain.AccountSettingsDomainContract.SettingsError
|
||||
import net.thunderbird.feature.account.settings.impl.domain.AccountSettingsDomainContract.UseCase
|
||||
import net.thunderbird.feature.account.settings.impl.ui.general.GeneralSettingsContract.Effect
|
||||
import net.thunderbird.feature.account.settings.impl.ui.general.GeneralSettingsContract.Event
|
||||
import net.thunderbird.feature.account.settings.impl.ui.general.GeneralSettingsContract.State
|
||||
|
||||
internal class GeneralSettingsViewModel(
|
||||
private val accountId: AccountId,
|
||||
private val getAccountName: UseCase.GetAccountName,
|
||||
private val getGeneralPreferences: UseCase.GetGeneralPreferences,
|
||||
private val updateGeneralPreferences: UseCase.UpdateGeneralPreferences,
|
||||
initialState: State = State(),
|
||||
) : BaseViewModel<State, Event, Effect>(initialState), GeneralSettingsContract.ViewModel {
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
getAccountName(accountId).collect { outcome ->
|
||||
outcome.handle(
|
||||
onSuccess = { accountName ->
|
||||
updateState { state ->
|
||||
state.copy(
|
||||
subtitle = accountName,
|
||||
)
|
||||
}
|
||||
},
|
||||
onFailure = { handleError(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
getGeneralPreferences(accountId).collect { outcome ->
|
||||
outcome.handle(
|
||||
onSuccess = { preferences ->
|
||||
updateState { state ->
|
||||
state.copy(
|
||||
preferences = preferences,
|
||||
)
|
||||
}
|
||||
},
|
||||
onFailure = { handleError(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun event(event: Event) {
|
||||
when (event) {
|
||||
is Event.OnPreferenceSettingChange -> updatePreference(event.preference)
|
||||
is Event.OnBackPressed -> emitEffect(Effect.NavigateBack)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePreference(preference: PreferenceSetting<*>) {
|
||||
viewModelScope.launch {
|
||||
updateGeneralPreferences(accountId, preference)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleError(error: SettingsError) {
|
||||
when (error) {
|
||||
is SettingsError.NotFound -> Log.w(error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
package net.thunderbird.feature.account.settings.impl.ui.general.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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 androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.card.CardElevated
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodyLarge
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.text.TextHeadlineSmall
|
||||
import app.k9mail.core.ui.compose.theme2.MainTheme
|
||||
import net.thunderbird.feature.account.avatar.ui.AvatarOutlined
|
||||
import net.thunderbird.feature.account.avatar.ui.AvatarSize
|
||||
|
||||
@Composable
|
||||
internal fun GeneralSettingsProfileView(
|
||||
name: String,
|
||||
email: String?,
|
||||
color: Color,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.padding(MainTheme.spacings.double),
|
||||
contentAlignment = Alignment.TopCenter,
|
||||
) {
|
||||
ProfileCard(
|
||||
name = name,
|
||||
email = email,
|
||||
modifier = Modifier
|
||||
.padding(top = MainTheme.spacings.quadruple)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
AvatarOutlined(
|
||||
color = color,
|
||||
name = name,
|
||||
size = AvatarSize.LARGE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProfileCard(
|
||||
name: String,
|
||||
email: String?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
CardElevated(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = MainTheme.spacings.oneHalf,
|
||||
vertical = MainTheme.spacings.triple,
|
||||
),
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(MainTheme.spacings.triple))
|
||||
TextHeadlineSmall(
|
||||
text = name,
|
||||
color = MainTheme.colors.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
email?.let {
|
||||
Spacer(modifier = Modifier.height(MainTheme.spacings.default))
|
||||
TextBodyLarge(
|
||||
text = it,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
|
||||
<string name="account_settings_general_title">General Settings</string>
|
||||
|
||||
<string name="account_settings_general_name_title">Account name</string>
|
||||
<string name="account_settings_general_name_description">The name associated with your account.</string>
|
||||
|
||||
<string name="account_settings_general_color_title">Account color</string>
|
||||
<string name="account_settings_general_color_description">The accent color of this account used in folders and account lists.</string>
|
||||
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package net.thunderbird.feature.account.settings.impl
|
||||
|
||||
import kotlin.test.Test
|
||||
import net.thunderbird.feature.account.AccountId
|
||||
import net.thunderbird.feature.account.settings.featureAccountSettingsModule
|
||||
import net.thunderbird.feature.account.settings.impl.ui.general.GeneralSettingsContract
|
||||
import org.koin.core.annotation.KoinExperimentalAPI
|
||||
import org.koin.test.verify.verify
|
||||
|
||||
internal class AccountSettingsModuleKtTest {
|
||||
|
||||
@OptIn(KoinExperimentalAPI::class)
|
||||
@Test
|
||||
fun `should hava a valid di module`() {
|
||||
featureAccountSettingsModule.verify(
|
||||
extraTypes = listOf(
|
||||
AccountId::class,
|
||||
GeneralSettingsContract.State::class,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package net.thunderbird.feature.account.settings.impl.domain.usecase
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import net.thunderbird.feature.account.AccountId
|
||||
import net.thunderbird.feature.account.profile.AccountProfile
|
||||
import net.thunderbird.feature.account.profile.AccountProfileRepository
|
||||
|
||||
internal class FakeAccountProfileRepository(
|
||||
initialAccountProfile: AccountProfile? = null,
|
||||
) : AccountProfileRepository {
|
||||
|
||||
private val accountProfileState = MutableStateFlow<AccountProfile?>(initialAccountProfile)
|
||||
private val accountProfile: StateFlow<AccountProfile?> = accountProfileState
|
||||
|
||||
override fun getById(accountId: AccountId): Flow<AccountProfile?> {
|
||||
return accountProfile
|
||||
}
|
||||
|
||||
override suspend fun update(accountProfile: AccountProfile) {
|
||||
accountProfileState.update {
|
||||
accountProfile
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package net.thunderbird.feature.account.settings.impl.domain.usecase
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import net.thunderbird.feature.account.settings.impl.domain.AccountSettingsDomainContract.ResourceProvider
|
||||
|
||||
internal class FakeGeneralResourceProvider : ResourceProvider.GeneralResourceProvider {
|
||||
override fun profileUi(
|
||||
name: String,
|
||||
color: Int,
|
||||
): @Composable ((Modifier) -> Unit) = { }
|
||||
|
||||
override val nameTitle: () -> String = { "Name" }
|
||||
override val nameDescription: () -> String? = { null }
|
||||
override val nameIcon: () -> ImageVector? = { null }
|
||||
|
||||
override val colorTitle: () -> String = { "Color" }
|
||||
override val colorDescription: () -> String? = { null }
|
||||
override val colorIcon: () -> ImageVector? = { null }
|
||||
override val colors: ImmutableList<Int> = persistentListOf(0xFF0000, 0x00FF00, 0x0000FF)
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package net.thunderbird.feature.account.settings.impl.domain.usecase
|
||||
|
||||
import app.cash.turbine.test
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isInstanceOf
|
||||
import kotlin.test.Test
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.thunderbird.core.outcome.Outcome
|
||||
import net.thunderbird.feature.account.AccountIdFactory
|
||||
import net.thunderbird.feature.account.profile.AccountAvatar
|
||||
import net.thunderbird.feature.account.profile.AccountProfile
|
||||
import net.thunderbird.feature.account.settings.impl.domain.AccountSettingsDomainContract.SettingsError
|
||||
import net.thunderbird.feature.account.settings.impl.domain.AccountSettingsDomainContract.UseCase
|
||||
|
||||
class GetAccountNameTest {
|
||||
|
||||
@Test
|
||||
fun `should emit account name when account profile present`() = runTest {
|
||||
// Arrange
|
||||
val accountId = AccountIdFactory.create()
|
||||
val accountProfile = AccountProfile(
|
||||
id = accountId,
|
||||
name = "Test Account",
|
||||
color = 0xFF0000,
|
||||
avatar = AccountAvatar.Icon(name = "star"),
|
||||
)
|
||||
val testSubject = createTestSubject(accountProfile)
|
||||
|
||||
// Act & Assert
|
||||
testSubject(accountId).test {
|
||||
val outcome = awaitItem()
|
||||
assertThat(outcome).isInstanceOf(Outcome.Success::class)
|
||||
|
||||
val success = outcome as Outcome.Success
|
||||
assertThat(success.data).isEqualTo(accountProfile.name)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should emit NotFound when account profile not present`() = runTest {
|
||||
// Arrange
|
||||
val accountId = AccountIdFactory.create()
|
||||
val testSubject = createTestSubject()
|
||||
|
||||
// Act & Assert
|
||||
testSubject(accountId).test {
|
||||
val outcome = awaitItem()
|
||||
assertThat(outcome).isInstanceOf(Outcome.Failure::class)
|
||||
|
||||
val failure = outcome as Outcome.Failure
|
||||
assertThat(failure.error).isInstanceOf(SettingsError.NotFound::class)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createTestSubject(
|
||||
accountProfile: AccountProfile? = null,
|
||||
): UseCase.GetAccountName {
|
||||
return GetAccountName(
|
||||
repository = FakeAccountProfileRepository(accountProfile),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package net.thunderbird.feature.account.settings.impl.domain.usecase
|
||||
|
||||
import app.cash.turbine.test
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isInstanceOf
|
||||
import kotlin.test.Test
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.thunderbird.core.outcome.Outcome
|
||||
import net.thunderbird.core.ui.compose.preference.api.PreferenceDisplay
|
||||
import net.thunderbird.core.ui.compose.preference.api.PreferenceSetting
|
||||
import net.thunderbird.feature.account.AccountIdFactory
|
||||
import net.thunderbird.feature.account.profile.AccountAvatar
|
||||
import net.thunderbird.feature.account.profile.AccountProfile
|
||||
import net.thunderbird.feature.account.settings.impl.domain.AccountSettingsDomainContract.ResourceProvider
|
||||
import net.thunderbird.feature.account.settings.impl.domain.AccountSettingsDomainContract.SettingsError
|
||||
import net.thunderbird.feature.account.settings.impl.domain.AccountSettingsDomainContract.UseCase
|
||||
|
||||
internal class GetGeneralPreferencesTest {
|
||||
|
||||
@Test
|
||||
fun `should emit preferences when account profile present`() = runTest {
|
||||
// Arrange
|
||||
val accountId = AccountIdFactory.create()
|
||||
val accountProfile = AccountProfile(
|
||||
id = accountId,
|
||||
name = "Test Account",
|
||||
color = 0xFF0000,
|
||||
avatar = AccountAvatar.Icon(name = "star"),
|
||||
)
|
||||
val resourceProvider = FakeGeneralResourceProvider()
|
||||
val testSubject = createTestSubject(accountProfile)
|
||||
|
||||
// Act & Assert
|
||||
testSubject(accountId).test {
|
||||
val outcome = awaitItem()
|
||||
assertThat(outcome).isInstanceOf(Outcome.Success::class)
|
||||
|
||||
val success = outcome as Outcome.Success
|
||||
assertThat(success.data).isEqualTo(
|
||||
persistentListOf(
|
||||
PreferenceDisplay.Custom(
|
||||
id = "${accountId.asRaw()}-general-profile",
|
||||
customUi = resourceProvider.profileUi(
|
||||
name = accountProfile.name,
|
||||
color = accountProfile.color,
|
||||
),
|
||||
),
|
||||
PreferenceSetting.Text(
|
||||
id = "${accountId.asRaw()}-general-name",
|
||||
title = resourceProvider.nameTitle,
|
||||
description = resourceProvider.nameDescription,
|
||||
icon = resourceProvider.nameIcon,
|
||||
value = accountProfile.name,
|
||||
),
|
||||
PreferenceSetting.Color(
|
||||
id = "${accountId.asRaw()}-general-color",
|
||||
title = resourceProvider.colorTitle,
|
||||
description = resourceProvider.colorDescription,
|
||||
icon = resourceProvider.colorIcon,
|
||||
value = accountProfile.color,
|
||||
colors = resourceProvider.colors,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should emit NotFound when account profile not found`() = runTest {
|
||||
// Arrange
|
||||
val accountId = AccountIdFactory.create()
|
||||
val testSubject = createTestSubject()
|
||||
|
||||
// Act & Assert
|
||||
testSubject(accountId).test {
|
||||
assertThat(awaitItem()).isEqualTo(
|
||||
Outcome.failure(
|
||||
SettingsError.NotFound(
|
||||
message = "Account profile not found for accountId: ${accountId.asRaw()}",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createTestSubject(
|
||||
accountProfile: AccountProfile? = null,
|
||||
resourceProvider: ResourceProvider.GeneralResourceProvider = FakeGeneralResourceProvider(),
|
||||
): UseCase.GetGeneralPreferences {
|
||||
return GetGeneralPreferences(
|
||||
repository = FakeAccountProfileRepository(accountProfile),
|
||||
resourceProvider = resourceProvider,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
package net.thunderbird.feature.account.settings.impl.domain.usecase
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isInstanceOf
|
||||
import kotlin.test.Test
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.thunderbird.core.outcome.Outcome
|
||||
import net.thunderbird.core.ui.compose.preference.api.PreferenceSetting
|
||||
import net.thunderbird.feature.account.AccountIdFactory
|
||||
import net.thunderbird.feature.account.profile.AccountAvatar
|
||||
import net.thunderbird.feature.account.profile.AccountProfile
|
||||
import net.thunderbird.feature.account.settings.impl.domain.AccountSettingsDomainContract.SettingsError
|
||||
import net.thunderbird.feature.account.settings.impl.domain.entity.GeneralPreference
|
||||
import net.thunderbird.feature.account.settings.impl.domain.entity.generateId
|
||||
|
||||
class UpdateGeneralPreferencesTest {
|
||||
|
||||
@Test
|
||||
fun `should update account profile`() = runTest {
|
||||
// Arrange
|
||||
val accountId = AccountIdFactory.create()
|
||||
val accountProfile = AccountProfile(
|
||||
id = accountId,
|
||||
name = "Test Account",
|
||||
color = 0xFF0000,
|
||||
avatar = AccountAvatar.Icon(name = "star"),
|
||||
)
|
||||
val newName = "Updated Account Name"
|
||||
val preference = PreferenceSetting.Text(
|
||||
id = GeneralPreference.NAME.generateId(accountId),
|
||||
title = { "Name" },
|
||||
description = { "Account name" },
|
||||
icon = { null },
|
||||
value = newName,
|
||||
)
|
||||
val repository = FakeAccountProfileRepository(
|
||||
initialAccountProfile = accountProfile,
|
||||
)
|
||||
val testSubject = UpdateGeneralPreferences(repository)
|
||||
|
||||
// Act
|
||||
val result = testSubject(accountId, preference)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isInstanceOf(Outcome.Success::class)
|
||||
assertThat(repository.getById(accountId).firstOrNull()).isEqualTo(
|
||||
accountProfile.copy(name = newName),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should update account profile for all general settings`() = runTest {
|
||||
// Arrange
|
||||
val accountId = AccountIdFactory.create()
|
||||
val accountProfile = AccountProfile(
|
||||
id = accountId,
|
||||
name = "Test Account",
|
||||
color = 0xFF0000,
|
||||
avatar = AccountAvatar.Icon(name = "star"),
|
||||
)
|
||||
val newName = "Updated Account Name"
|
||||
val newColor = 0x00FF00
|
||||
val preferences = listOf(
|
||||
PreferenceSetting.Text(
|
||||
id = GeneralPreference.NAME.generateId(accountId),
|
||||
title = { "Name" },
|
||||
description = { "Account name" },
|
||||
icon = { null },
|
||||
value = newName,
|
||||
),
|
||||
PreferenceSetting.Color(
|
||||
id = GeneralPreference.COLOR.generateId(accountId),
|
||||
title = { "Color" },
|
||||
description = { "Account color" },
|
||||
icon = { null },
|
||||
value = newColor,
|
||||
colors = persistentListOf(0xFF0000, 0x00FF00, 0x0000FF),
|
||||
),
|
||||
)
|
||||
val repository = FakeAccountProfileRepository(
|
||||
initialAccountProfile = accountProfile,
|
||||
)
|
||||
val testSubject = UpdateGeneralPreferences(repository)
|
||||
|
||||
// Act
|
||||
preferences.forEach { preference ->
|
||||
testSubject(accountId, preference)
|
||||
}
|
||||
|
||||
// Assert
|
||||
assertThat(repository.getById(accountId).firstOrNull()).isEqualTo(
|
||||
accountProfile.copy(
|
||||
name = newName,
|
||||
color = newColor,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should emit NotFound when account profile not found`() = runTest {
|
||||
// Arrange
|
||||
val accountId = AccountIdFactory.create()
|
||||
val preference = PreferenceSetting.Text(
|
||||
id = GeneralPreference.NAME.generateId(accountId),
|
||||
title = { "Name" },
|
||||
description = { "Account name" },
|
||||
icon = { null },
|
||||
value = "Updated Account Name",
|
||||
)
|
||||
val repository = FakeAccountProfileRepository()
|
||||
val testSubject = UpdateGeneralPreferences(repository)
|
||||
|
||||
// Act
|
||||
val result = testSubject(accountId, preference)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isInstanceOf(Outcome.Failure::class)
|
||||
assertThat((result as Outcome.Failure).error).isInstanceOf(SettingsError.NotFound::class)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package net.thunderbird.feature.account.settings.impl.ui.general
|
||||
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import net.thunderbird.core.ui.compose.preference.api.Preference
|
||||
import net.thunderbird.core.ui.compose.preference.api.PreferenceSetting
|
||||
|
||||
internal object FakeData {
|
||||
|
||||
val preferences: ImmutableList<Preference> = persistentListOf(
|
||||
PreferenceSetting.Text(
|
||||
id = "test_id",
|
||||
title = { "Title" },
|
||||
description = { "Description" },
|
||||
icon = { null },
|
||||
value = "Test",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package net.thunderbird.feature.account.settings.impl.ui.general
|
||||
|
||||
import app.k9mail.core.ui.compose.testing.BaseFakeViewModel
|
||||
import net.thunderbird.feature.account.settings.impl.ui.general.GeneralSettingsContract.Effect
|
||||
import net.thunderbird.feature.account.settings.impl.ui.general.GeneralSettingsContract.Event
|
||||
import net.thunderbird.feature.account.settings.impl.ui.general.GeneralSettingsContract.State
|
||||
import net.thunderbird.feature.account.settings.impl.ui.general.GeneralSettingsContract.ViewModel
|
||||
|
||||
internal class FakeGeneralSettingsViewModel(
|
||||
initialState: State = State(),
|
||||
) : BaseFakeViewModel<State, Event, Effect>(initialState), ViewModel
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package net.thunderbird.feature.account.settings.impl.ui.general
|
||||
|
||||
import app.k9mail.core.ui.compose.testing.ComposeTest
|
||||
import app.k9mail.core.ui.compose.testing.pressBack
|
||||
import app.k9mail.core.ui.compose.testing.setContentWithTheme
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import kotlin.test.Test
|
||||
import net.thunderbird.feature.account.AccountIdFactory
|
||||
import net.thunderbird.feature.account.settings.impl.ui.general.GeneralSettingsContract.Effect
|
||||
import net.thunderbird.feature.account.settings.impl.ui.general.GeneralSettingsContract.State
|
||||
|
||||
internal class GeneralSettingsScreenKtTest : ComposeTest() {
|
||||
|
||||
@Test
|
||||
fun `should call onBack when back button is pressed`() {
|
||||
val initialState = State()
|
||||
val accountId = AccountIdFactory.create()
|
||||
val viewModel = FakeGeneralSettingsViewModel(initialState)
|
||||
var onBackCounter = 0
|
||||
|
||||
setContentWithTheme {
|
||||
GeneralSettingsScreen(
|
||||
accountId = accountId,
|
||||
onBack = { onBackCounter++ },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
|
||||
assertThat(onBackCounter).isEqualTo(0)
|
||||
|
||||
pressBack()
|
||||
|
||||
assertThat(onBackCounter).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should call onBack when navigate back effect received`() {
|
||||
val initialState = State()
|
||||
val accountId = AccountIdFactory.create()
|
||||
val viewModel = FakeGeneralSettingsViewModel(initialState)
|
||||
var onBackCounter = 0
|
||||
|
||||
setContentWithTheme {
|
||||
GeneralSettingsScreen(
|
||||
accountId = accountId,
|
||||
onBack = { onBackCounter++ },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
|
||||
assertThat(onBackCounter).isEqualTo(0)
|
||||
|
||||
viewModel.effect(Effect.NavigateBack)
|
||||
|
||||
assertThat(onBackCounter).isEqualTo(1)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package net.thunderbird.feature.account.settings.impl.ui.general
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import kotlin.test.Test
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import net.thunderbird.feature.account.settings.impl.ui.general.GeneralSettingsContract.State
|
||||
|
||||
internal class GeneralSettingsStateTest {
|
||||
|
||||
@Test
|
||||
fun `should set default values`() {
|
||||
// Arrange
|
||||
val state = State()
|
||||
|
||||
// Assert
|
||||
assertThat(state).isEqualTo(
|
||||
State(
|
||||
subtitle = null,
|
||||
preferences = persistentListOf(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
package net.thunderbird.feature.account.settings.impl.ui.general
|
||||
|
||||
import app.k9mail.core.ui.compose.testing.mvi.MviContext
|
||||
import app.k9mail.core.ui.compose.testing.mvi.MviTurbines
|
||||
import app.k9mail.core.ui.compose.testing.mvi.runMviTest
|
||||
import app.k9mail.core.ui.compose.testing.mvi.turbinesWithInitialStateCheck
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import kotlin.test.Test
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import net.thunderbird.core.logging.legacy.Log
|
||||
import net.thunderbird.core.logging.testing.TestLogger
|
||||
import net.thunderbird.core.outcome.Outcome
|
||||
import net.thunderbird.core.testing.coroutines.MainDispatcherRule
|
||||
import net.thunderbird.core.ui.compose.preference.api.Preference
|
||||
import net.thunderbird.core.ui.compose.preference.api.PreferenceSetting
|
||||
import net.thunderbird.feature.account.AccountId
|
||||
import net.thunderbird.feature.account.AccountIdFactory
|
||||
import net.thunderbird.feature.account.settings.impl.ui.general.GeneralSettingsContract.Effect
|
||||
import net.thunderbird.feature.account.settings.impl.ui.general.GeneralSettingsContract.State
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
|
||||
class GeneralSettingsViewModelTest {
|
||||
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule(StandardTestDispatcher())
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
Log.logger = TestLogger()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should load account name`() = runMviTest {
|
||||
val accountId = AccountIdFactory.create()
|
||||
val initialState = State(
|
||||
subtitle = null,
|
||||
preferences = persistentListOf(),
|
||||
)
|
||||
|
||||
generalSettingsRobot(accountId, initialState, persistentListOf()) {
|
||||
verifyAccountNameLoaded()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should load general settings`() = runMviTest {
|
||||
val accountId = AccountIdFactory.create()
|
||||
val initialState = State(
|
||||
subtitle = "Subtitle",
|
||||
preferences = persistentListOf(),
|
||||
)
|
||||
val preferences = FakeData.preferences
|
||||
|
||||
generalSettingsRobot(accountId, initialState, preferences) {
|
||||
verifyGeneralSettingsLoaded(preferences)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should navigate back when back is pressed`() = runMviTest {
|
||||
val accountId = AccountIdFactory.create()
|
||||
val initialState = State(
|
||||
subtitle = "Subtitle",
|
||||
preferences = persistentListOf(),
|
||||
)
|
||||
val preferences = FakeData.preferences
|
||||
|
||||
generalSettingsRobot(accountId, initialState, preferences) {
|
||||
verifyGeneralSettingsLoaded(preferences)
|
||||
pressBack()
|
||||
verifyBackNavigation()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should update preference when changed`() = runMviTest {
|
||||
val accountId = AccountIdFactory.create()
|
||||
val initialState = State(
|
||||
subtitle = "Subtitle",
|
||||
preferences = persistentListOf(),
|
||||
)
|
||||
val preferences = FakeData.preferences
|
||||
|
||||
generalSettingsRobot(accountId, initialState, preferences) {
|
||||
verifyGeneralSettingsLoaded(preferences)
|
||||
val updatedPreference = (preferences.first() as PreferenceSetting.Text).copy(
|
||||
title = { "Updated Title" },
|
||||
description = { "Updated Description" },
|
||||
)
|
||||
updatePreference(updatedPreference)
|
||||
|
||||
verifyPreferenceUpdated(updatedPreference)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun MviContext.generalSettingsRobot(
|
||||
accountId: AccountId,
|
||||
initialState: State,
|
||||
preferences: ImmutableList<Preference>,
|
||||
interaction: suspend GeneralSettingsRobot.() -> Unit,
|
||||
) = GeneralSettingsRobot(this, accountId, initialState, preferences).apply {
|
||||
initialize()
|
||||
interaction()
|
||||
}
|
||||
|
||||
private class GeneralSettingsRobot(
|
||||
private val mviContext: MviContext,
|
||||
private val accountId: AccountId,
|
||||
private val initialState: State = State(),
|
||||
private val preferences: ImmutableList<Preference>,
|
||||
) {
|
||||
private lateinit var preferencesState: MutableStateFlow<ImmutableList<Preference>>
|
||||
private lateinit var turbines: MviTurbines<State, Effect>
|
||||
|
||||
private val viewModel: GeneralSettingsContract.ViewModel by lazy {
|
||||
GeneralSettingsViewModel(
|
||||
accountId = accountId,
|
||||
getAccountName = {
|
||||
flowOf(Outcome.success("Subtitle"))
|
||||
},
|
||||
getGeneralPreferences = {
|
||||
preferencesState.map {
|
||||
println("Loading preferences: $it")
|
||||
Outcome.success(it)
|
||||
}
|
||||
},
|
||||
updateGeneralPreferences = { _, preference ->
|
||||
preferencesState.value = preferencesState.value.map { existingPreference ->
|
||||
if (existingPreference is PreferenceSetting<*> && existingPreference.id == preference.id) {
|
||||
println("Updating preference: ${preference.id}")
|
||||
println("Old preference: $existingPreference")
|
||||
println("New preference: $preference")
|
||||
preference
|
||||
} else {
|
||||
existingPreference
|
||||
}
|
||||
}.toImmutableList()
|
||||
Outcome.success(Unit)
|
||||
},
|
||||
initialState = initialState,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun initialize() {
|
||||
preferencesState = MutableStateFlow(preferences)
|
||||
|
||||
turbines = mviContext.turbinesWithInitialStateCheck(
|
||||
initialState = initialState,
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun verifyAccountNameLoaded() {
|
||||
assertThat(turbines.awaitStateItem()).isEqualTo(
|
||||
initialState.copy(
|
||||
subtitle = "Subtitle",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun verifyGeneralSettingsLoaded(preferences: ImmutableList<Preference>) {
|
||||
assertThat(turbines.awaitStateItem()).isEqualTo(
|
||||
initialState.copy(
|
||||
preferences = preferences,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun pressBack() {
|
||||
viewModel.event(GeneralSettingsContract.Event.OnBackPressed)
|
||||
}
|
||||
|
||||
suspend fun verifyBackNavigation() {
|
||||
assertThat(turbines.awaitEffectItem()).isEqualTo(
|
||||
Effect.NavigateBack,
|
||||
)
|
||||
}
|
||||
|
||||
fun updatePreference(preference: PreferenceSetting<*>) {
|
||||
viewModel.event(GeneralSettingsContract.Event.OnPreferenceSettingChange(preference))
|
||||
}
|
||||
|
||||
suspend fun verifyPreferenceUpdated(preference: PreferenceSetting<*>) {
|
||||
val updatedPreference = turbines.awaitStateItem().preferences
|
||||
.filterIsInstance<PreferenceSetting<*>>()
|
||||
.find { it.id == preference.id }
|
||||
|
||||
assertThat(updatedPreference).isEqualTo(preference)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue