app: the stable-pseudonym switch was live at a level that pseudonymizes nothing

At `full` the anonymizer returns the document unchanged, so the salt has nothing
to act on - but the switch was enabled and looked like it did something. A
control that silently does nothing is the same class of fault as the preview
button and the archived-level label: the screen implying more than is true.

Shown disabled with the reason rather than hidden. The setting is still stored
and applies the moment the level changes, so making it vanish would hide state
that is still there; and a settings screen whose controls appear and disappear
as you touch other controls is harder to trust, not easier. The label dims with
the switch so "not active right now" reads at a glance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mrambossek
2026-08-01 16:16:36 +02:00
co-authored by Claude Fable 5
parent c19f382640
commit 57a5ef8796
@@ -16,6 +16,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChip
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
@@ -130,12 +131,21 @@ fun SettingsScreen(
} }
Text(privacyExplanation(privacy), style = MaterialTheme.typography.bodySmall) Text(privacyExplanation(privacy), style = MaterialTheme.typography.bodySmall)
// At FULL nothing is pseudonymized, so a salt has nothing to act on. Shown
// disabled rather than hidden: the setting is still stored and still applies the
// moment the level changes, and a control that vanishes hides that fact.
Toggle( Toggle(
label = "Stable pseudonyms across runs", label = "Stable pseudonyms across runs",
detail = "Lets you compare uploaded runs over time (same SSID reads the same " + detail = if (privacy == PrivacyLevel.FULL) {
"each time). It also links your uploads together, so leave it off on a " + "Not used at this level — nothing is pseudonymized, so there is nothing " +
"server you don't run yourself.", "to keep stable. Choose balanced or strict to use this."
checked = stableSalt, } else {
"Lets you compare uploaded runs over time (same SSID reads the same " +
"each time). It also links your uploads together, so leave it off on " +
"a server you don't run yourself."
},
checked = stableSalt && privacy != PrivacyLevel.FULL,
enabled = privacy != PrivacyLevel.FULL,
) { stableSalt = it; settings.stableSalt = it } ) { stableSalt = it; settings.stableSalt = it }
TextButton(onClick = onPreviewUpload) { Text("Preview what an upload would send") } TextButton(onClick = onPreviewUpload) { Text("Preview what an upload would send") }
@@ -237,13 +247,22 @@ private fun privacyExplanation(level: PrivacyLevel): String = when (level) {
} }
@Composable @Composable
private fun Toggle(label: String, detail: String, checked: Boolean, onChange: (Boolean) -> Unit) { private fun Toggle(
label: String,
detail: String,
checked: Boolean,
enabled: Boolean = true,
onChange: (Boolean) -> Unit,
) {
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.Top) { Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.Top) {
Column(Modifier.weight(1f)) { Column(Modifier.weight(1f)) {
Text(label, style = MaterialTheme.typography.bodyMedium) // Dimmed together with the switch, so "this does nothing right now" reads at a glance
Text(detail, style = MaterialTheme.typography.bodySmall) // instead of only on close inspection.
val alpha = if (enabled) 1f else 0.5f
Text(label, style = MaterialTheme.typography.bodyMedium, color = LocalContentColor.current.copy(alpha = alpha))
Text(detail, style = MaterialTheme.typography.bodySmall, color = LocalContentColor.current.copy(alpha = alpha))
} }
Switch(checked = checked, onCheckedChange = onChange) Switch(checked = checked, onCheckedChange = onChange, enabled = enabled)
} }
} }