feat(tor): restart confirmation dialog + key() app rebuild on Tor toggle

When user changes Tor settings, shows "Restart Required" confirmation
dialog. On confirm: saves prefs, triggers appRestartKey++ which forces
Compose to unmount/remount entire App tree.

Key behaviors:
- Window stays open, user stays logged in (AccountManager outside App)
- Column layout preserved (DeckState outside App)
- DisposableEffect stops old Tor daemon before new one starts
- TorSettingsSection mode change → confirmation dialog
- TorSettingsDialog Save → confirmation dialog
- Relays, subscriptions, caches all recreated fresh

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
nrobi144
2026-04-02 12:17:39 +03:00
parent 01fc5f3ac6
commit 2df621056e
3 changed files with 76 additions and 24 deletions
@@ -55,6 +55,7 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@@ -184,6 +185,7 @@ fun main() {
height = 800.dp,
position = WindowPosition.Aligned(Alignment.Center),
)
var appRestartKey by remember { mutableStateOf(0) }
var showComposeDialog by remember { mutableStateOf(false) }
var replyToNote by remember { mutableStateOf<com.vitorpamplona.quartz.nip01Core.core.Event?>(null) }
val deckScope = rememberCoroutineScope()
@@ -410,25 +412,28 @@ fun main() {
LocalAwtWindow provides window,
LocalIsImmersiveFullscreen provides immersiveFullscreenState,
) {
App(
layoutMode = layoutMode,
deckState = deckState,
accountManager = accountManager,
showComposeDialog = showComposeDialog,
showAddColumnDialog = showAddColumnDialog,
onShowComposeDialog = { showComposeDialog = true },
onShowReplyDialog = { event ->
replyToNote = event
showComposeDialog = true
},
onDismissComposeDialog = {
showComposeDialog = false
replyToNote = null
},
onDismissAddColumnDialog = { showAddColumnDialog = false },
onShowAddColumnDialog = { showAddColumnDialog = true },
replyToNote = replyToNote,
)
key(appRestartKey) {
App(
layoutMode = layoutMode,
deckState = deckState,
accountManager = accountManager,
showComposeDialog = showComposeDialog,
showAddColumnDialog = showAddColumnDialog,
onShowComposeDialog = { showComposeDialog = true },
onShowReplyDialog = { event ->
replyToNote = event
showComposeDialog = true
},
onDismissComposeDialog = {
showComposeDialog = false
replyToNote = null
},
onDismissAddColumnDialog = { showAddColumnDialog = false },
onShowAddColumnDialog = { showAddColumnDialog = true },
replyToNote = replyToNote,
onRestartApp = { appRestartKey++ },
)
}
}
}
}
@@ -447,6 +452,7 @@ fun App(
onDismissAddColumnDialog: () -> Unit,
onShowAddColumnDialog: () -> Unit,
replyToNote: com.vitorpamplona.quartz.nip01Core.core.Event?,
onRestartApp: () -> Unit = {},
) {
val localCache = remember { DesktopLocalCache() }
val accountState by accountManager.accountState.collectAsState()
@@ -468,6 +474,14 @@ fun App(
}
}
// Clean up Tor daemon on unmount (key() change triggers this)
DisposableEffect(torManager) {
onDispose {
torManager.stopSync()
activeTorManager = null
}
}
// Build TorRelayEvaluation for per-relay routing
val torRelayEvaluation =
remember(torSettings) {
@@ -606,6 +620,8 @@ fun App(
.save(newSettings)
torTypeFlow.value = newSettings.torType
externalPortFlow.value = newSettings.externalSocksPort
// Rebuild app to apply Tor changes
onRestartApp()
},
),
) {
@@ -71,6 +71,8 @@ fun TorSettingsDialog(
) {
var editSettings by remember { mutableStateOf(currentSettings) }
var showRestartConfirm by remember { mutableStateOf(false) }
DialogWindow(
onCloseRequest = onDismiss,
title = "Tor Settings",
@@ -196,10 +198,24 @@ fun TorSettingsDialog(
) {
TextButton(onClick = onDismiss) { Text("Cancel") }
Spacer(Modifier.width(8.dp))
TextButton(onClick = {
onSettingsChanged(editSettings)
onDismiss()
}) { Text("Save") }
TextButton(onClick = { showRestartConfirm = true }) { Text("Save") }
}
if (showRestartConfirm) {
androidx.compose.material3.AlertDialog(
onDismissRequest = { showRestartConfirm = false },
title = { Text("Restart Required") },
text = { Text("Tor changes require restarting. Proceed?") },
confirmButton = {
TextButton(onClick = {
onSettingsChanged(editSettings)
onDismiss()
}) { Text("Restart") }
},
dismissButton = {
TextButton(onClick = { showRestartConfirm = false }) { Text("Cancel") }
},
)
}
}
}
@@ -27,6 +27,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
@@ -58,6 +59,7 @@ fun TorSettingsSection(
modifier: Modifier = Modifier,
) {
var showDialog by remember { mutableStateOf(false) }
var pendingSettings by remember { mutableStateOf<TorSettings?>(null) }
Column(modifier = modifier) {
Row(
@@ -94,7 +96,7 @@ fun TorSettingsSection(
TorType.entries.forEachIndexed { index, torType ->
SegmentedButton(
shape = SegmentedButtonDefaults.itemShape(index = index, count = TorType.entries.size),
onClick = { onSettingsChanged(currentSettings.copy(torType = torType)) },
onClick = { pendingSettings = currentSettings.copy(torType = torType) },
selected = currentSettings.torType == torType,
) {
Text(torType.name.lowercase().replaceFirstChar { it.uppercase() })
@@ -136,4 +138,22 @@ fun TorSettingsSection(
onDismiss = { showDialog = false },
)
}
// Restart confirmation dialog
pendingSettings?.let { settings ->
AlertDialog(
onDismissRequest = { pendingSettings = null },
title = { Text("Restart Required") },
text = { Text("Changing Tor mode requires restarting. Your session will be briefly interrupted.") },
confirmButton = {
TextButton(onClick = {
onSettingsChanged(settings)
pendingSettings = null
}) { Text("Restart") }
},
dismissButton = {
TextButton(onClick = { pendingSettings = null }) { Text("Cancel") }
},
)
}
}