fix(tor): default to Internal + Full Privacy, clickable shield, sticky dialog buttons
UX improvements based on manual testing feedback: - Default TorType changed from OFF to INTERNAL (Tor on by default) - Default preset changed to Full Privacy (all routing via Tor) - Shield icon always visible in sidebar (not hidden when Off) - Shield icon clickable — opens Settings - TorSettingsDialog Cancel/Save buttons sticky at bottom (don't scroll away) - HorizontalDivider above buttons for visual separation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+11
-11
@@ -37,19 +37,19 @@ object DesktopTorPreferences : ITorSettingsPersistence {
|
||||
|
||||
override fun load(): TorSettings =
|
||||
TorSettings(
|
||||
torType = TorType.entries.firstOrNull { it.name == prefs.get("tor_type", TorType.OFF.name) } ?: TorType.OFF,
|
||||
torType = TorType.entries.firstOrNull { it.name == prefs.get("tor_type", TorType.INTERNAL.name) } ?: TorType.INTERNAL,
|
||||
externalSocksPort = prefs.getInt("tor_external_port", 9050),
|
||||
onionRelaysViaTor = prefs.getBoolean("tor_onion_relays", true),
|
||||
dmRelaysViaTor = prefs.getBoolean("tor_dm_relays", false),
|
||||
newRelaysViaTor = prefs.getBoolean("tor_new_relays", false),
|
||||
trustedRelaysViaTor = prefs.getBoolean("tor_trusted_relays", false),
|
||||
urlPreviewsViaTor = prefs.getBoolean("tor_url_previews", false),
|
||||
profilePicsViaTor = prefs.getBoolean("tor_profile_pics", false),
|
||||
imagesViaTor = prefs.getBoolean("tor_images", false),
|
||||
videosViaTor = prefs.getBoolean("tor_videos", false),
|
||||
moneyOperationsViaTor = prefs.getBoolean("tor_money", false),
|
||||
nip05VerificationsViaTor = prefs.getBoolean("tor_nip05", false),
|
||||
mediaUploadsViaTor = prefs.getBoolean("tor_media_uploads", false),
|
||||
dmRelaysViaTor = prefs.getBoolean("tor_dm_relays", true),
|
||||
newRelaysViaTor = prefs.getBoolean("tor_new_relays", true),
|
||||
trustedRelaysViaTor = prefs.getBoolean("tor_trusted_relays", true),
|
||||
urlPreviewsViaTor = prefs.getBoolean("tor_url_previews", true),
|
||||
profilePicsViaTor = prefs.getBoolean("tor_profile_pics", true),
|
||||
imagesViaTor = prefs.getBoolean("tor_images", true),
|
||||
videosViaTor = prefs.getBoolean("tor_videos", true),
|
||||
moneyOperationsViaTor = prefs.getBoolean("tor_money", true),
|
||||
nip05VerificationsViaTor = prefs.getBoolean("tor_nip05", true),
|
||||
mediaUploadsViaTor = prefs.getBoolean("tor_media_uploads", true),
|
||||
)
|
||||
|
||||
override fun save(settings: TorSettings) {
|
||||
|
||||
+3
-5
@@ -90,12 +90,10 @@ fun DeckSidebar(
|
||||
lastPingTimeSec = lastPingTimeSec,
|
||||
)
|
||||
|
||||
if (torStatus !is TorServiceStatus.Off) {
|
||||
Spacer(Modifier.size(4.dp))
|
||||
TorStatusIndicator(status = torStatus)
|
||||
}
|
||||
Spacer(Modifier.size(4.dp))
|
||||
TorStatusIndicator(status = torStatus, onClick = onOpenSettings)
|
||||
|
||||
Spacer(Modifier.size(8.dp))
|
||||
Spacer(Modifier.size(4.dp))
|
||||
|
||||
IconButton(onClick = onOpenSettings) {
|
||||
Icon(
|
||||
|
||||
+105
-100
@@ -77,114 +77,119 @@ fun TorSettingsDialog(
|
||||
state = rememberDialogState(size = DpSize(480.dp, 640.dp)),
|
||||
) {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(24.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
// Status
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
TorStatusIndicator(status = torStatus)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
when (torStatus) {
|
||||
is TorServiceStatus.Off -> "Tor is off"
|
||||
is TorServiceStatus.Connecting -> "Connecting to Tor..."
|
||||
is TorServiceStatus.Active -> "Connected via Tor"
|
||||
is TorServiceStatus.Error -> "Error: ${torStatus.message}"
|
||||
},
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
}
|
||||
Column(modifier = Modifier.padding(24.dp)) {
|
||||
// Scrollable content
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
// Status
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
TorStatusIndicator(status = torStatus)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
when (torStatus) {
|
||||
is TorServiceStatus.Off -> "Tor is off"
|
||||
is TorServiceStatus.Connecting -> "Connecting to Tor..."
|
||||
is TorServiceStatus.Active -> "Connected via Tor"
|
||||
is TorServiceStatus.Error -> "Error: ${torStatus.message}"
|
||||
},
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
// Mode selector
|
||||
Text("Mode", style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
|
||||
TorType.entries.forEachIndexed { index, torType ->
|
||||
SegmentedButton(
|
||||
shape = SegmentedButtonDefaults.itemShape(index, TorType.entries.size),
|
||||
onClick = { editSettings = editSettings.copy(torType = torType) },
|
||||
selected = editSettings.torType == torType,
|
||||
) {
|
||||
Text(torType.name.lowercase().replaceFirstChar { it.uppercase() })
|
||||
// Mode selector
|
||||
Text("Mode", style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
|
||||
TorType.entries.forEachIndexed { index, torType ->
|
||||
SegmentedButton(
|
||||
shape = SegmentedButtonDefaults.itemShape(index, TorType.entries.size),
|
||||
onClick = { editSettings = editSettings.copy(torType = torType) },
|
||||
selected = editSettings.torType == torType,
|
||||
) {
|
||||
Text(torType.name.lowercase().replaceFirstChar { it.uppercase() })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (editSettings.torType == TorType.EXTERNAL) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = editSettings.externalSocksPort.toString(),
|
||||
onValueChange = { text ->
|
||||
text.toIntOrNull()?.let { port ->
|
||||
if (port in 1..65535) {
|
||||
editSettings = editSettings.copy(externalSocksPort = port)
|
||||
if (editSettings.torType == TorType.EXTERNAL) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = editSettings.externalSocksPort.toString(),
|
||||
onValueChange = { text ->
|
||||
text.toIntOrNull()?.let { port ->
|
||||
if (port in 1..65535) {
|
||||
editSettings = editSettings.copy(externalSocksPort = port)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
label = { Text("SOCKS Port") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.width(150.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
label = { Text("SOCKS Port") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.width(150.dp),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Spacer(Modifier.height(16.dp))
|
||||
HorizontalDivider()
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
// Presets
|
||||
Text("Preset", style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
val currentPreset = whichPreset(editSettings)
|
||||
PresetRow("Only When Needed", TorPresetType.ONLY_WHEN_NEEDED, currentPreset) {
|
||||
editSettings = torOnlyWhenNeededPreset.copy(torType = editSettings.torType, externalSocksPort = editSettings.externalSocksPort)
|
||||
}
|
||||
PresetRow("Default", TorPresetType.DEFAULT, currentPreset) {
|
||||
editSettings = torDefaultPreset.copy(torType = editSettings.torType, externalSocksPort = editSettings.externalSocksPort)
|
||||
}
|
||||
PresetRow("Small Payloads", TorPresetType.SMALL_PAYLOADS, currentPreset) {
|
||||
editSettings = torSmallPayloadsPreset.copy(torType = editSettings.torType, externalSocksPort = editSettings.externalSocksPort)
|
||||
}
|
||||
PresetRow("Full Privacy", TorPresetType.FULL_PRIVACY, currentPreset) {
|
||||
editSettings = torFullyPrivate.copy(torType = editSettings.torType, externalSocksPort = editSettings.externalSocksPort)
|
||||
}
|
||||
PresetRow("Custom", TorPresetType.CUSTOM, currentPreset) {}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
HorizontalDivider()
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
// Relay Routing
|
||||
Text("Relay Routing", style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
ToggleRow(".onion relays via Tor", editSettings.onionRelaysViaTor) { editSettings = editSettings.copy(onionRelaysViaTor = it) }
|
||||
ToggleRow("DM relays via Tor", editSettings.dmRelaysViaTor) { editSettings = editSettings.copy(dmRelaysViaTor = it) }
|
||||
ToggleRow("Trusted relays via Tor", editSettings.trustedRelaysViaTor) { editSettings = editSettings.copy(trustedRelaysViaTor = it) }
|
||||
ToggleRow("New/unknown relays via Tor", editSettings.newRelaysViaTor) { editSettings = editSettings.copy(newRelaysViaTor = it) }
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
HorizontalDivider()
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
// Content Routing
|
||||
Text("Content Routing", style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
ToggleRow("URL previews via Tor", editSettings.urlPreviewsViaTor) { editSettings = editSettings.copy(urlPreviewsViaTor = it) }
|
||||
ToggleRow("Profile pictures via Tor", editSettings.profilePicsViaTor) { editSettings = editSettings.copy(profilePicsViaTor = it) }
|
||||
ToggleRow("Images via Tor", editSettings.imagesViaTor) { editSettings = editSettings.copy(imagesViaTor = it) }
|
||||
ToggleRow("Videos via Tor", editSettings.videosViaTor) { editSettings = editSettings.copy(videosViaTor = it) }
|
||||
ToggleRow("NIP-05 verifications via Tor", editSettings.nip05VerificationsViaTor) { editSettings = editSettings.copy(nip05VerificationsViaTor = it) }
|
||||
ToggleRow("Money operations via Tor", editSettings.moneyOperationsViaTor) { editSettings = editSettings.copy(moneyOperationsViaTor = it) }
|
||||
ToggleRow("Media uploads via Tor", editSettings.mediaUploadsViaTor) { editSettings = editSettings.copy(mediaUploadsViaTor = it) }
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
} // end scrollable content
|
||||
|
||||
// Sticky bottom buttons — always visible
|
||||
HorizontalDivider()
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
// Presets
|
||||
Text("Preset", style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
val currentPreset = whichPreset(editSettings)
|
||||
PresetRow("Only When Needed", TorPresetType.ONLY_WHEN_NEEDED, currentPreset) {
|
||||
editSettings = torOnlyWhenNeededPreset.copy(torType = editSettings.torType, externalSocksPort = editSettings.externalSocksPort)
|
||||
}
|
||||
PresetRow("Default", TorPresetType.DEFAULT, currentPreset) {
|
||||
editSettings = torDefaultPreset.copy(torType = editSettings.torType, externalSocksPort = editSettings.externalSocksPort)
|
||||
}
|
||||
PresetRow("Small Payloads", TorPresetType.SMALL_PAYLOADS, currentPreset) {
|
||||
editSettings = torSmallPayloadsPreset.copy(torType = editSettings.torType, externalSocksPort = editSettings.externalSocksPort)
|
||||
}
|
||||
PresetRow("Full Privacy", TorPresetType.FULL_PRIVACY, currentPreset) {
|
||||
editSettings = torFullyPrivate.copy(torType = editSettings.torType, externalSocksPort = editSettings.externalSocksPort)
|
||||
}
|
||||
PresetRow("Custom", TorPresetType.CUSTOM, currentPreset) {}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
HorizontalDivider()
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
// Relay Routing
|
||||
Text("Relay Routing", style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
ToggleRow(".onion relays via Tor", editSettings.onionRelaysViaTor) { editSettings = editSettings.copy(onionRelaysViaTor = it) }
|
||||
ToggleRow("DM relays via Tor", editSettings.dmRelaysViaTor) { editSettings = editSettings.copy(dmRelaysViaTor = it) }
|
||||
ToggleRow("Trusted relays via Tor", editSettings.trustedRelaysViaTor) { editSettings = editSettings.copy(trustedRelaysViaTor = it) }
|
||||
ToggleRow("New/unknown relays via Tor", editSettings.newRelaysViaTor) { editSettings = editSettings.copy(newRelaysViaTor = it) }
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
HorizontalDivider()
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
// Content Routing
|
||||
Text("Content Routing", style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
ToggleRow("URL previews via Tor", editSettings.urlPreviewsViaTor) { editSettings = editSettings.copy(urlPreviewsViaTor = it) }
|
||||
ToggleRow("Profile pictures via Tor", editSettings.profilePicsViaTor) { editSettings = editSettings.copy(profilePicsViaTor = it) }
|
||||
ToggleRow("Images via Tor", editSettings.imagesViaTor) { editSettings = editSettings.copy(imagesViaTor = it) }
|
||||
ToggleRow("Videos via Tor", editSettings.videosViaTor) { editSettings = editSettings.copy(videosViaTor = it) }
|
||||
ToggleRow("NIP-05 verifications via Tor", editSettings.nip05VerificationsViaTor) { editSettings = editSettings.copy(nip05VerificationsViaTor = it) }
|
||||
ToggleRow("Money operations via Tor", editSettings.moneyOperationsViaTor) { editSettings = editSettings.copy(moneyOperationsViaTor = it) }
|
||||
ToggleRow("Media uploads via Tor", editSettings.mediaUploadsViaTor) { editSettings = editSettings.copy(mediaUploadsViaTor = it) }
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
// Buttons
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
|
||||
+20
-7
@@ -30,6 +30,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Shield
|
||||
import androidx.compose.material.icons.outlined.Shield
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
@@ -43,12 +44,13 @@ import com.vitorpamplona.amethyst.commons.tor.TorServiceStatus
|
||||
|
||||
/**
|
||||
* Small shield icon showing Tor connection status in the sidebar footer.
|
||||
* Tooltip shows status text (no port number for security).
|
||||
* Clickable to open Tor settings. Tooltip shows status text.
|
||||
*/
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun TorStatusIndicator(
|
||||
status: TorServiceStatus,
|
||||
onClick: (() -> Unit)? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val (icon, tint, tooltip) =
|
||||
@@ -86,11 +88,22 @@ fun TorStatusIndicator(
|
||||
},
|
||||
tooltipPlacement = TooltipPlacement.CursorPoint(alignment = Alignment.BottomEnd, offset = DpOffset(0.dp, 16.dp)),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = tooltip,
|
||||
tint = tint,
|
||||
modifier = modifier.size(20.dp),
|
||||
)
|
||||
if (onClick != null) {
|
||||
IconButton(onClick = onClick, modifier = modifier.size(28.dp)) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = tooltip,
|
||||
tint = tint,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = tooltip,
|
||||
tint = tint,
|
||||
modifier = modifier.size(20.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user