Merge main into feat-162-advanced-settings-hierarchy

This commit is contained in:
Jason Ross
2026-07-02 19:44:31 -05:00
committed by GitHub
9 changed files with 165 additions and 1 deletions
@@ -57,6 +57,14 @@ data class AppSettings(
*/
val retentionCount: Int = 0,
val retentionMonths: Int = 0,
/**
* The user-chosen default account (issue #163): the account [org.libremail.ui.compose.ComposeViewModel]
* uses when compose opens without an explicit account context (e.g. the unified-inbox FAB, or a
* `mailto:`/share intent with no account hint). Null means no default is set. A value here is not
* guaranteed to still name an existing account (see [SettingsRepository.clearDefaultAccountId]) —
* treat it as a hint to validate against the current account list, never as a trusted id.
*/
val defaultAccountId: String? = null,
)
private object Keys {
@@ -71,6 +79,7 @@ private object Keys {
val FETCH_POLICY = stringPreferencesKey("fetch_policy")
val RETENTION_COUNT = intPreferencesKey("retention_count")
val RETENTION_MONTHS = intPreferencesKey("retention_months")
val DEFAULT_ACCOUNT_ID = stringPreferencesKey("default_account_id")
val BATTERY_PROMPT_HANDLED = booleanPreferencesKey("battery_prompt_handled")
val CONTACTS_PROMPT_HANDLED = booleanPreferencesKey("contacts_prompt_handled")
val CONTACTS_PERMISSION_REQUESTED = booleanPreferencesKey("contacts_permission_requested")
@@ -96,6 +105,7 @@ internal fun Preferences.toAppSettings(): AppSettings = AppSettings(
?: FetchPolicy.WIFI_ONLY,
retentionCount = this[Keys.RETENTION_COUNT] ?: 0,
retentionMonths = this[Keys.RETENTION_MONTHS] ?: 0,
defaultAccountId = this[Keys.DEFAULT_ACCOUNT_ID],
)
@Singleton
@@ -175,6 +185,29 @@ class SettingsRepository @Inject constructor(@ApplicationContext private val con
context.settingsDataStore.edit { it[Keys.RETENTION_MONTHS] = value.coerceAtLeast(0) }
}
/**
* Sets (or, when [value] is null, clears) the user-chosen default account (#163). Used directly by
* the per-account "set as default" toggle; deleting an account should go through
* [clearDefaultAccountId] instead so it can't clobber a different account's default.
*/
suspend fun setDefaultAccountId(value: String?) {
context.settingsDataStore.edit {
if (value != null) it[Keys.DEFAULT_ACCOUNT_ID] = value else it.remove(Keys.DEFAULT_ACCOUNT_ID)
}
}
/**
* Clears the default-account preference, but only if it currently points at [accountId]. Called
* when that account is deleted, so a stale id is never left behind — and so deleting some other
* (non-default) account never disturbs an unrelated default. No-op if [accountId] isn't the current
* default.
*/
suspend fun clearDefaultAccountId(accountId: String) {
context.settingsDataStore.edit {
if (it[Keys.DEFAULT_ACCOUNT_ID] == accountId) it.remove(Keys.DEFAULT_ACCOUNT_ID)
}
}
private suspend fun put(key: Preferences.Key<Boolean>, value: Boolean) {
context.settingsDataStore.edit { it[key] = value }
}
@@ -20,6 +20,7 @@ import org.libremail.contacts.ContactSuggestion
import org.libremail.contacts.ContactsRepository
import org.libremail.data.SignatureBlock
import org.libremail.data.settings.AccountSettingsRepository
import org.libremail.data.settings.SettingsRepository
import org.libremail.data.settings.SignatureRepository
import org.libremail.domain.model.Account
import org.libremail.domain.model.Draft
@@ -62,6 +63,7 @@ class ComposeViewModel @Inject constructor(
private val contactsRepository: ContactsRepository,
private val accountSettingsRepository: AccountSettingsRepository,
private val signatureRepository: SignatureRepository,
private val settingsRepository: SettingsRepository,
) : ViewModel() {
private val draftId: String? =
@@ -120,7 +122,15 @@ class ComposeViewModel @Inject constructor(
// signature. Reply/forward drafts already carry theirs, so they take the draft branch above.
viewModelScope.launch {
val available = accountRepository.observeAccounts().first { it.isNotEmpty() }
val effectiveId = _state.value.fromAccountId ?: available.first().id
// The persisted default (#163) only counts if it still names an account that exists.
// Deleting the default account normally clears this via
// SettingsRepository.clearDefaultAccountId, but a stale id could still reach here (e.g.
// a Backup restore onto a device that never had the account) — validate rather than
// trust it, so it just falls through to the incidental "first account alphabetically"
// behavior instead of crashing or composing from a nonexistent account.
val defaultAccountId = settingsRepository.settings.first().defaultAccountId
val validDefaultAccountId = defaultAccountId?.takeIf { id -> available.any { it.id == id } }
val effectiveId = _state.value.fromAccountId ?: validDefaultAccountId ?: available.first().id
applySignature(effectiveId)
}
}
@@ -39,6 +39,7 @@ fun AccountSettingsScreen(
val settings by viewModel.settings.collectAsStateWithLifecycle()
val signatureCount by viewModel.signatureCount.collectAsStateWithLifecycle()
val defaultSignatureName by viewModel.defaultSignatureName.collectAsStateWithLifecycle()
val isDefaultAccount by viewModel.isDefaultAccount.collectAsStateWithLifecycle()
val context = LocalContext.current
val fallbackTitle = stringResource(R.string.settings_account_title)
@@ -63,6 +64,14 @@ fun AccountSettingsScreen(
.padding(padding)
.verticalScroll(rememberScrollState()),
) {
SwitchRow(
title = stringResource(R.string.settings_account_set_default),
checked = isDefaultAccount,
onCheckedChange = viewModel::setDefaultAccount,
subtitle = stringResource(R.string.settings_account_set_default_summary),
)
HorizontalDivider()
SectionHeader(stringResource(R.string.settings_signature))
SwitchRow(
title = stringResource(R.string.settings_signature_enable),
@@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.libremail.data.settings.AccountSettingsRepository
import org.libremail.data.settings.SettingsRepository
import org.libremail.data.settings.SignatureRepository
import org.libremail.data.sync.SyncScheduler
import org.libremail.domain.model.Account
@@ -27,6 +28,7 @@ class AccountSettingsViewModel @Inject constructor(
private val accountSettingsRepository: AccountSettingsRepository,
signatureRepository: SignatureRepository,
private val syncScheduler: SyncScheduler,
private val settingsRepository: SettingsRepository,
) : ViewModel() {
private val accountId: String =
@@ -39,6 +41,11 @@ class AccountSettingsViewModel @Inject constructor(
val settings: StateFlow<AccountSettings> = accountSettingsRepository.observe(accountId)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), AccountSettings(accountId))
/** Whether this account is the user-chosen default (issue #163) — see [SettingsRepository]. */
val isDefaultAccount: StateFlow<Boolean> = settingsRepository.settings
.map { it.defaultAccountId == accountId }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), false)
private val signaturesFlow = signatureRepository.observeForAccount(accountId)
val signatureCount: StateFlow<Int> = signaturesFlow
@@ -78,9 +85,27 @@ class AccountSettingsViewModel @Inject constructor(
}
}
/**
* Makes this account the default (used by compose's from-account fallback, #163), or clears the
* default when turned off. Only one account is default at a time — persisting this account's id
* implicitly un-defaults whichever one held it before.
*/
fun setDefaultAccount(isDefault: Boolean) {
viewModelScope.launch {
if (isDefault) {
settingsRepository.setDefaultAccountId(accountId)
} else {
settingsRepository.clearDefaultAccountId(accountId)
}
}
}
fun removeAccount(onRemoved: () -> Unit) {
viewModelScope.launch {
accountRepository.deleteAccount(accountId)
// Don't strand the preference on a deleted account; only clears it if this account was
// actually the default (see SettingsRepository.clearDefaultAccountId).
settingsRepository.clearDefaultAccountId(accountId)
onRemoved()
}
}
+2
View File
@@ -253,6 +253,8 @@
<!-- Per-account settings -->
<string name="settings_account_title">Account</string>
<string name="settings_account_set_default">Default account</string>
<string name="settings_account_set_default_summary">Use this account for new messages when none is specified</string>
<string name="settings_signature">Signature</string>
<string name="settings_signature_enable">Append signature automatically</string>
<string name="settings_signature_hint">Your signature</string>