diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/tor/DesktopTorPreferences.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/tor/DesktopTorPreferences.kt index b08499bd1..489384dc6 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/tor/DesktopTorPreferences.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/tor/DesktopTorPreferences.kt @@ -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) { diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/DeckSidebar.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/DeckSidebar.kt index 1173b6912..32f470b9f 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/DeckSidebar.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/DeckSidebar.kt @@ -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( diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/tor/TorSettingsDialog.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/tor/TorSettingsDialog.kt index f1faf37d8..76f99a4c0 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/tor/TorSettingsDialog.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/tor/TorSettingsDialog.kt @@ -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, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/tor/TorStatusIndicator.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/tor/TorStatusIndicator.kt index 20530b1d7..3eb57188b 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/tor/TorStatusIndicator.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/tor/TorStatusIndicator.kt @@ -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), + ) + } } }