Merge main into feat-162-advanced-settings-hierarchy
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user