First-run chooser, node shutdown on switch, file picker, export path fix

- First-run: show Start Fresh / Import chooser only when single auto-created
  identity with no profile (not on every boot without a display name).
- Identity switch: shut down old node's endpoint before starting new one.
  Fixes lockup after multiple switches (zombie background tasks).
- File picker: native Browse buttons on export (folder) and import (ZIP file)
  via tauri-plugin-dialog.
- Export path: resolve relative paths against home dir (was using process cwd).
- Lightbox close: only close on overlay/image click, not inner form content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Scott Reimers 2026-04-06 01:58:02 -04:00
parent a349d33422
commit ec731fdb4b
6 changed files with 171 additions and 15 deletions

View file

@ -257,11 +257,10 @@ impl IdentityManager {
anyhow::bail!("Identity {} not found", &target_hex[..12]);
}
// Shutdown current node if running
// Shutdown current node before switching
if let Some(ref node) = self.active_node {
info!(old = hex::encode(node.node_id), new = target_hex, "Switching identity");
// Node::shutdown would go here once implemented
// For now, dropping the Arc triggers cleanup when last reference is released
info!(old = hex::encode(node.node_id), new = target_hex, "Switching identity — shutting down old node");
node.network.shutdown_ref().await;
}
self.active_node = None;
self.active_id = None;

View file

@ -2335,6 +2335,14 @@ impl Network {
Ok(())
}
/// Shutdown via Arc reference — closes the endpoint, causing all background tasks to exit.
pub async fn shutdown_ref(&self) {
if let Some(ref mapping) = self.upnp_mapping {
crate::upnp::remove_upnp_mapping(mapping.external_addr.port()).await;
}
self.endpoint.close().await;
}
/// Propagate an engagement diff to all downstream holders of a post (CDN tree).
/// Excludes the sender to avoid loops.
pub async fn propagate_engagement_diff(

View file

@ -24,4 +24,5 @@ base64 = "0.22"
dirs = "5"
open = "5"
tauri-plugin-notification = "2"
tauri-plugin-dialog = "2"
notify-rust = "4"

View file

@ -1643,6 +1643,25 @@ struct AddressInfoDto {
status: String, // "Public", "NAT (easy)", "NAT (hard)", "UPnP", "LAN", "Server"
}
#[tauri::command]
async fn pick_file(app: tauri::AppHandle, title: String, filter_name: Option<String>, filter_ext: Option<Vec<String>>) -> Result<Option<String>, String> {
use tauri_plugin_dialog::DialogExt;
let mut builder = app.dialog().file().set_title(&title);
if let (Some(name), Some(exts)) = (filter_name, filter_ext) {
let ext_refs: Vec<&str> = exts.iter().map(|s| s.as_str()).collect();
builder = builder.add_filter(&name, &ext_refs);
}
let path = builder.blocking_pick_file();
Ok(path.map(|p| p.to_string()))
}
#[tauri::command]
async fn pick_folder(app: tauri::AppHandle, title: String) -> Result<Option<String>, String> {
use tauri_plugin_dialog::DialogExt;
let path = app.dialog().file().set_title(&title).blocking_pick_folder();
Ok(path.map(|p| p.to_string()))
}
#[tauri::command]
async fn get_our_info(state: State<'_, AppNode>) -> Result<OurInfoDto, String> {
let node = get_node(&state).await;
@ -2276,13 +2295,21 @@ async fn export_data(
"everything" => itsgoin_core::export::ExportScope::Everything,
_ => return Err("Invalid scope".to_string()),
};
// Resolve relative paths against user's home directory
let resolved_dir = if std::path::Path::new(&output_dir).is_relative() {
dirs::home_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join(&output_dir)
} else {
std::path::PathBuf::from(&output_dir)
};
let result = itsgoin_core::export::export_data(
&node.data_dir,
&node.storage,
&node.blob_store,
&node.node_id,
export_scope,
std::path::Path::new(&output_dir),
&resolved_dir,
).await.map_err(|e| e.to_string())?;
let paths: Vec<String> = result.paths.iter()
@ -2372,6 +2399,7 @@ pub fn run() {
info!("Starting Tauri app");
tauri::Builder::default()
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_dialog::init())
.setup(move |app| {
// Desktop: store data next to the AppImage/executable so each copy
// gets its own identity. Mobile: use the standard app data dir.
@ -2407,15 +2435,17 @@ pub fn run() {
itsgoin_core::types::DeviceProfile::Desktop
};
let (node, identity_mgr) = tauri::async_runtime::block_on(async {
let mgr = IdentityManager::open(&data_dir, None, profile).await?;
let mut mgr = IdentityManager::open(&data_dir, None, profile).await?;
// If no identity exists yet, create one
let n = if let Some(node) = mgr.active_node() {
Arc::clone(node)
} else {
// No identities at all — this is a fresh install after migration
// The IdentityManager should have either migrated or the user needs to create one
anyhow::bail!("No identity available — create one first")
// No identity — create a temporary one so the app can start.
// Frontend detects this via get_node_info and shows the first-run chooser.
info!("No identity found — creating initial identity for first-run setup");
let node_id = mgr.create_identity("My Identity")?;
mgr.switch_identity(&node_id).await?;
Arc::clone(mgr.active_node().expect("just created"))
};
// Start background networking
@ -2502,6 +2532,8 @@ pub fn run() {
sync_from_peer,
get_network_summary,
get_our_info,
pick_file,
pick_folder,
get_activity_log,
trigger_rebalance,
request_referrals,