diff --git a/commonsUI/src/commonMain/composeResources/values/strings.xml b/commonsUI/src/commonMain/composeResources/values/strings.xml index 8a227b5f8f..c98506f31e 100644 --- a/commonsUI/src/commonMain/composeResources/values/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values/strings.xml @@ -1865,8 +1865,7 @@ Keep waiting Connecting to Tor… Tor error: %1$s - Amethyst sends your connections through Tor to hide your IP address. Starting Tor can take a minute and needs internet access. - Tor settings + Amethyst uses Tor to hide your IP address from relays. Starting Tor can take a minute and needs internet access. Tap a category to open Android notification settings for it — sound, importance, badges and Do Not Disturb live there. On Silent diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt index b4cd746979..88859f79c6 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt @@ -347,14 +347,11 @@ fun main(args: Array) { val feedSearchActiveState = remember { mutableStateOf(false) } // Tor state at Window level — survives key() app rebuild - var torSettings by remember { - mutableStateOf( - com.vitorpamplona.amethyst.desktop.tor.DesktopTorPreferences - .load(), - ) - } + val torSettings = remember { DesktopTorPreferences.load() } val torTypeFlow = remember { kotlinx.coroutines.flow.MutableStateFlow(torSettings.torType) } val externalPortFlow = remember { kotlinx.coroutines.flow.MutableStateFlow(torSettings.externalSocksPort) } + // "Use regular connection" on the Tor splash: Tor off for this run only, never saved. + val torSessionBypassFlow = remember { kotlinx.coroutines.flow.MutableStateFlow(false) } val windowScope = rememberCoroutineScope() val torManager = remember { @@ -739,6 +736,7 @@ fun main(args: Array) { torManager = torManager, torTypeFlow = torTypeFlow, externalPortFlow = externalPortFlow, + torSessionBypassFlow = torSessionBypassFlow, initialTorSettings = torSettings, onNavigateToScreen = { navigateToScreen = it }, ) @@ -775,6 +773,7 @@ fun App( torManager: com.vitorpamplona.amethyst.commons.tor.ITorManager, torTypeFlow: kotlinx.coroutines.flow.MutableStateFlow, externalPortFlow: kotlinx.coroutines.flow.MutableStateFlow, + torSessionBypassFlow: kotlinx.coroutines.flow.MutableStateFlow, initialTorSettings: com.vitorpamplona.amethyst.commons.tor.TorSettings, onNavigateToScreen: ((DeckColumnType) -> Unit) -> Unit = {}, testOverrides: LaunchTestOverrides? = null, @@ -831,6 +830,7 @@ fun App( torManager = torManager, torTypeFlow = torTypeFlow, externalPortFlow = externalPortFlow, + torSessionBypassFlow = torSessionBypassFlow, initialTorSettings = initialTorSettings, onNavigateToScreen = onNavigateToScreen, testOverrides = testOverrides, @@ -866,6 +866,7 @@ private fun AppInner( torManager: com.vitorpamplona.amethyst.commons.tor.ITorManager, torTypeFlow: kotlinx.coroutines.flow.MutableStateFlow, externalPortFlow: kotlinx.coroutines.flow.MutableStateFlow, + torSessionBypassFlow: kotlinx.coroutines.flow.MutableStateFlow, initialTorSettings: com.vitorpamplona.amethyst.commons.tor.TorSettings, onNavigateToScreen: ((DeckColumnType) -> Unit) -> Unit, testOverrides: LaunchTestOverrides?, @@ -878,28 +879,26 @@ private fun AppInner( } // Always reload from prefs — after key() rebuild, prefs have the latest saved settings. - // The mode comes from the window-level torTypeFlow instead, so a session-only "continue - // without Tor" (which never touches prefs) survives the rebuild. // Tests can short-circuit the prefs read via `testOverrides.torSettingsOverride` so the // Tor splash gate (below) does not block them behind a real kmp-tor runtime. + // torSettings is the user's saved choice (what the settings UI shows and saves); + // effectiveTorSettings is what this session actually routes by. var torSettings by remember { - mutableStateOf( - testOverrides?.torSettingsOverride - ?: DesktopTorPreferences.load().copy(torType = torTypeFlow.value), - ) + mutableStateOf(testOverrides?.torSettingsOverride ?: DesktopTorPreferences.load()) } + val torSessionBypass by torSessionBypassFlow.collectAsState() + val effectiveTorSettings = if (torSessionBypass) torSettings.copy(torType = TorType.OFF) else torSettings // Gate: block EVERYTHING until Tor proxy is ready (when Tor expected) // This must be before any OkHttpClient/Coil/relay creation val torStatus by torManager.status.collectAsState() - val isTorExpected = torSettings.torType != TorType.OFF + val isTorExpected = effectiveTorSettings.torType != TorType.OFF if (isTorExpected && torStatus !is TorServiceStatus.Active) { var showTorSettings by remember { mutableStateOf(false) } TorConnectingSplash( status = torStatus, onContinueWithoutTor = { - // Session only: prefs keep the user's Tor choice for the next launch. - torSettings = torSettings.copy(torType = TorType.OFF) + torSessionBypassFlow.value = true torTypeFlow.value = TorType.OFF }, onOpenTorSettings = { showTorSettings = true }, @@ -909,11 +908,12 @@ private fun AppInner( currentSettings = torSettings, torStatus = torStatus, onSettingsChanged = { newSettings -> + // No onRestartApp(): nothing below the gate has been built yet, and a + // rebuild would only restart the splash (and its escape timer). torSettings = newSettings DesktopTorPreferences.save(newSettings) torTypeFlow.value = newSettings.torType externalPortFlow.value = newSettings.externalSocksPort - onRestartApp() }, onDismiss = { showTorSettings = false }, ) @@ -977,15 +977,15 @@ private fun AppInner( // Build TorRelayEvaluation for per-relay routing val torRelayEvaluation = - remember(torSettings) { + remember(effectiveTorSettings) { com.vitorpamplona.amethyst.commons.tor.TorRelayEvaluation( torSettings = com.vitorpamplona.amethyst.commons.tor.TorRelaySettings( - torType = torSettings.torType, - onionRelaysViaTor = torSettings.onionRelaysViaTor, - dmRelaysViaTor = torSettings.dmRelaysViaTor, - newRelaysViaTor = torSettings.newRelaysViaTor, - trustedRelaysViaTor = torSettings.trustedRelaysViaTor, + torType = effectiveTorSettings.torType, + onionRelaysViaTor = effectiveTorSettings.onionRelaysViaTor, + dmRelaysViaTor = effectiveTorSettings.dmRelaysViaTor, + newRelaysViaTor = effectiveTorSettings.newRelaysViaTor, + trustedRelaysViaTor = effectiveTorSettings.trustedRelaysViaTor, ), // TODO: populate from account relay lists classification = @@ -1491,9 +1491,11 @@ private fun AppInner( status = currentTorStatus, settings = torSettings, onSettingsChanged = { newSettings -> + // Saving re-applies the saved choice, ending a + // session bypass from the splash. + torSessionBypassFlow.value = false torSettings = newSettings - com.vitorpamplona.amethyst.desktop.tor.DesktopTorPreferences - .save(newSettings) + DesktopTorPreferences.save(newSettings) torTypeFlow.value = newSettings.torType externalPortFlow.value = newSettings.externalSocksPort // Rebuild app to apply Tor changes diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/tor/TorConnectingSplash.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/tor/TorConnectingSplash.kt index 44313c4817..d6b93b9b90 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/tor/TorConnectingSplash.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/tor/TorConnectingSplash.kt @@ -47,11 +47,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.app_logo +import com.vitorpamplona.amethyst.commons.resources.connect_via_tor2 import com.vitorpamplona.amethyst.commons.resources.tor_continue_without_for_session import com.vitorpamplona.amethyst.commons.resources.tor_splash_connecting import com.vitorpamplona.amethyst.commons.resources.tor_splash_error import com.vitorpamplona.amethyst.commons.resources.tor_splash_explainer -import com.vitorpamplona.amethyst.commons.resources.tor_splash_settings import com.vitorpamplona.amethyst.commons.tor.TorServiceStatus import com.vitorpamplona.amethyst.desktop.platform.IconResources import kotlinx.coroutines.delay @@ -112,7 +113,7 @@ fun TorConnectingSplash( Spacer(Modifier.height(24.dp)) Icon( painter = IconResources.rawBitmapPainter, - contentDescription = "Amethyst", + contentDescription = stringResource(Res.string.app_logo), modifier = Modifier.size(96.dp), tint = MaterialTheme.colorScheme.primary, ) @@ -120,7 +121,7 @@ fun TorConnectingSplash( Spacer(Modifier.height(24.dp)) Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { OutlinedButton(onClick = onOpenTorSettings) { - Text(stringResource(Res.string.tor_splash_settings)) + Text(stringResource(Res.string.connect_via_tor2)) } Button(onClick = onContinueWithoutTor) { Text(stringResource(Res.string.tor_continue_without_for_session)) diff --git a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/ui/AppStateMachineTest.kt b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/ui/AppStateMachineTest.kt index cd98c7ea9c..8db43c0fd2 100644 --- a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/ui/AppStateMachineTest.kt +++ b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/ui/AppStateMachineTest.kt @@ -129,6 +129,7 @@ class AppStateMachineTest { torManager = torManager, torTypeFlow = MutableStateFlow(TorType.OFF), externalPortFlow = MutableStateFlow(9050), + torSessionBypassFlow = MutableStateFlow(false), initialTorSettings = OFF_TOR_SETTINGS, testOverrides = LaunchTestOverrides( @@ -190,6 +191,7 @@ class AppStateMachineTest { torManager = torManager, torTypeFlow = MutableStateFlow(TorType.OFF), externalPortFlow = MutableStateFlow(9050), + torSessionBypassFlow = MutableStateFlow(false), initialTorSettings = OFF_TOR_SETTINGS, testOverrides = LaunchTestOverrides( @@ -266,6 +268,7 @@ class AppStateMachineTest { torManager = torManager, torTypeFlow = MutableStateFlow(TorType.OFF), externalPortFlow = MutableStateFlow(9050), + torSessionBypassFlow = MutableStateFlow(false), initialTorSettings = OFF_TOR_SETTINGS, testOverrides = LaunchTestOverrides( @@ -342,6 +345,7 @@ class AppStateMachineTest { torManager = torManager, torTypeFlow = MutableStateFlow(TorType.OFF), externalPortFlow = MutableStateFlow(9050), + torSessionBypassFlow = MutableStateFlow(false), initialTorSettings = OFF_TOR_SETTINGS, testOverrides = LaunchTestOverrides(