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:
@@ -55,6 +55,7 @@ import androidx.compose.runtime.DisposableEffect
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.key
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
@@ -184,6 +185,7 @@ fun main() {
|
|||||||
height = 800.dp,
|
height = 800.dp,
|
||||||
position = WindowPosition.Aligned(Alignment.Center),
|
position = WindowPosition.Aligned(Alignment.Center),
|
||||||
)
|
)
|
||||||
|
var appRestartKey by remember { mutableStateOf(0) }
|
||||||
var showComposeDialog by remember { mutableStateOf(false) }
|
var showComposeDialog by remember { mutableStateOf(false) }
|
||||||
var replyToNote by remember { mutableStateOf<com.vitorpamplona.quartz.nip01Core.core.Event?>(null) }
|
var replyToNote by remember { mutableStateOf<com.vitorpamplona.quartz.nip01Core.core.Event?>(null) }
|
||||||
val deckScope = rememberCoroutineScope()
|
val deckScope = rememberCoroutineScope()
|
||||||
@@ -410,25 +412,28 @@ fun main() {
|
|||||||
LocalAwtWindow provides window,
|
LocalAwtWindow provides window,
|
||||||
LocalIsImmersiveFullscreen provides immersiveFullscreenState,
|
LocalIsImmersiveFullscreen provides immersiveFullscreenState,
|
||||||
) {
|
) {
|
||||||
App(
|
key(appRestartKey) {
|
||||||
layoutMode = layoutMode,
|
App(
|
||||||
deckState = deckState,
|
layoutMode = layoutMode,
|
||||||
accountManager = accountManager,
|
deckState = deckState,
|
||||||
showComposeDialog = showComposeDialog,
|
accountManager = accountManager,
|
||||||
showAddColumnDialog = showAddColumnDialog,
|
showComposeDialog = showComposeDialog,
|
||||||
onShowComposeDialog = { showComposeDialog = true },
|
showAddColumnDialog = showAddColumnDialog,
|
||||||
onShowReplyDialog = { event ->
|
onShowComposeDialog = { showComposeDialog = true },
|
||||||
replyToNote = event
|
onShowReplyDialog = { event ->
|
||||||
showComposeDialog = true
|
replyToNote = event
|
||||||
},
|
showComposeDialog = true
|
||||||
onDismissComposeDialog = {
|
},
|
||||||
showComposeDialog = false
|
onDismissComposeDialog = {
|
||||||
replyToNote = null
|
showComposeDialog = false
|
||||||
},
|
replyToNote = null
|
||||||
onDismissAddColumnDialog = { showAddColumnDialog = false },
|
},
|
||||||
onShowAddColumnDialog = { showAddColumnDialog = true },
|
onDismissAddColumnDialog = { showAddColumnDialog = false },
|
||||||
replyToNote = replyToNote,
|
onShowAddColumnDialog = { showAddColumnDialog = true },
|
||||||
)
|
replyToNote = replyToNote,
|
||||||
|
onRestartApp = { appRestartKey++ },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -447,6 +452,7 @@ fun App(
|
|||||||
onDismissAddColumnDialog: () -> Unit,
|
onDismissAddColumnDialog: () -> Unit,
|
||||||
onShowAddColumnDialog: () -> Unit,
|
onShowAddColumnDialog: () -> Unit,
|
||||||
replyToNote: com.vitorpamplona.quartz.nip01Core.core.Event?,
|
replyToNote: com.vitorpamplona.quartz.nip01Core.core.Event?,
|
||||||
|
onRestartApp: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val localCache = remember { DesktopLocalCache() }
|
val localCache = remember { DesktopLocalCache() }
|
||||||
val accountState by accountManager.accountState.collectAsState()
|
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
|
// Build TorRelayEvaluation for per-relay routing
|
||||||
val torRelayEvaluation =
|
val torRelayEvaluation =
|
||||||
remember(torSettings) {
|
remember(torSettings) {
|
||||||
@@ -606,6 +620,8 @@ fun App(
|
|||||||
.save(newSettings)
|
.save(newSettings)
|
||||||
torTypeFlow.value = newSettings.torType
|
torTypeFlow.value = newSettings.torType
|
||||||
externalPortFlow.value = newSettings.externalSocksPort
|
externalPortFlow.value = newSettings.externalSocksPort
|
||||||
|
// Rebuild app to apply Tor changes
|
||||||
|
onRestartApp()
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
|
|||||||
+20
-4
@@ -71,6 +71,8 @@ fun TorSettingsDialog(
|
|||||||
) {
|
) {
|
||||||
var editSettings by remember { mutableStateOf(currentSettings) }
|
var editSettings by remember { mutableStateOf(currentSettings) }
|
||||||
|
|
||||||
|
var showRestartConfirm by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
DialogWindow(
|
DialogWindow(
|
||||||
onCloseRequest = onDismiss,
|
onCloseRequest = onDismiss,
|
||||||
title = "Tor Settings",
|
title = "Tor Settings",
|
||||||
@@ -196,10 +198,24 @@ fun TorSettingsDialog(
|
|||||||
) {
|
) {
|
||||||
TextButton(onClick = onDismiss) { Text("Cancel") }
|
TextButton(onClick = onDismiss) { Text("Cancel") }
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
TextButton(onClick = {
|
TextButton(onClick = { showRestartConfirm = true }) { Text("Save") }
|
||||||
onSettingsChanged(editSettings)
|
}
|
||||||
onDismiss()
|
|
||||||
}) { 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") }
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-1
@@ -27,6 +27,7 @@ import androidx.compose.foundation.layout.Spacer
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SegmentedButton
|
import androidx.compose.material3.SegmentedButton
|
||||||
import androidx.compose.material3.SegmentedButtonDefaults
|
import androidx.compose.material3.SegmentedButtonDefaults
|
||||||
@@ -58,6 +59,7 @@ fun TorSettingsSection(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
var showDialog by remember { mutableStateOf(false) }
|
var showDialog by remember { mutableStateOf(false) }
|
||||||
|
var pendingSettings by remember { mutableStateOf<TorSettings?>(null) }
|
||||||
|
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
Row(
|
Row(
|
||||||
@@ -94,7 +96,7 @@ fun TorSettingsSection(
|
|||||||
TorType.entries.forEachIndexed { index, torType ->
|
TorType.entries.forEachIndexed { index, torType ->
|
||||||
SegmentedButton(
|
SegmentedButton(
|
||||||
shape = SegmentedButtonDefaults.itemShape(index = index, count = TorType.entries.size),
|
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,
|
selected = currentSettings.torType == torType,
|
||||||
) {
|
) {
|
||||||
Text(torType.name.lowercase().replaceFirstChar { it.uppercase() })
|
Text(torType.name.lowercase().replaceFirstChar { it.uppercase() })
|
||||||
@@ -136,4 +138,22 @@ fun TorSettingsSection(
|
|||||||
onDismiss = { showDialog = false },
|
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") }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user