diff --git a/src-tauri/src/commands/identity.rs b/src-tauri/src/commands/identity.rs index 80f9962..1639062 100644 --- a/src-tauri/src/commands/identity.rs +++ b/src-tauri/src/commands/identity.rs @@ -254,12 +254,15 @@ pub async fn search_directory( query: String, ) -> Result, String> { ipc_log!("search_directory", { + let query_trimmed = query.trim().to_string(); + + // local search first let entries = state .storage .load_directory() .map_err(|e| format!("failed to load directory: {}", e))?; - let query_lower = query.to_lowercase(); + let query_lower = query_trimmed.to_lowercase(); let mut results: Vec = entries .into_values() .filter(|entry| { @@ -267,8 +270,63 @@ pub async fn search_directory( || entry.peer_id.to_lowercase().contains(&query_lower) }) .collect(); - results.sort_by(|a, b| b.last_seen.cmp(&a.last_seen)); + + // relay fallback when local results are sparse + if results.len() < 5 && !query_trimmed.is_empty() { + let node_handle = state.node_handle.lock().await; + if let Some(ref handle) = *node_handle { + let (tx, rx) = tokio::sync::oneshot::channel(); + let _ = handle + .command_tx + .send(crate::node::NodeCommand::DirectorySearch { + query: query_trimmed.clone(), + reply: tx, + }) + .await; + drop(node_handle); + + // wait up to 5 seconds for relay response + if let Ok(Ok(Ok(relay_entries))) = + tokio::time::timeout(std::time::Duration::from_secs(5), rx).await + { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as u64; + + for entry in relay_entries { + // upsert as stub — empty bio/public_key means never directly connected + let stub = DirectoryEntry { + peer_id: entry.peer_id.clone(), + display_name: entry.display_name, + bio: String::new(), + public_key: String::new(), + last_seen: entry.last_seen.saturating_mul(1000).max(now - 86_400_000), + is_friend: false, + }; + // preserve existing local data if we already know this peer + let _ = state.storage.save_directory_entry_if_new(&stub); + } + + // re-run local search to get merged results + let entries2 = state + .storage + .load_directory() + .unwrap_or_default(); + let mut results2: Vec = entries2 + .into_values() + .filter(|entry| { + entry.display_name.to_lowercase().contains(&query_lower) + || entry.peer_id.to_lowercase().contains(&query_lower) + }) + .collect(); + results2.sort_by(|a, b| b.last_seen.cmp(&a.last_seen)); + return Ok(results2); + } + } + } + Ok(results) }) } @@ -329,6 +387,33 @@ pub async fn discover_global_peers(state: State<'_, AppState>) -> Result<(), Str Ok(()) } +// toggle relay discoverability at runtime and sync the setting to disk +#[tauri::command] +pub async fn set_relay_discoverable( + state: State<'_, AppState>, + enabled: bool, +) -> Result<(), String> { + ipc_log!("set_relay_discoverable", { + // persist setting + let mut settings = state.storage.load_settings().unwrap_or_default(); + settings.relay_discoverable = enabled; + state + .storage + .save_settings(&settings) + .map_err(|e| format!("failed to save settings: {}", e))?; + + // notify running node + let node_handle = state.node_handle.lock().await; + if let Some(ref handle) = *node_handle { + let _ = handle + .command_tx + .send(crate::node::NodeCommand::SetRelayDiscoverable { enabled }) + .await; + } + Ok(()) + }) +} + // change relay address and restart the node // used when default relay is unreachable or at capacity #[tauri::command] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 564de14..88e8bdb 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -114,6 +114,7 @@ pub fn run() { commands::identity::add_friend, commands::identity::remove_friend, commands::identity::discover_global_peers, + commands::identity::set_relay_discoverable, commands::identity::set_relay_address, commands::identity::reset_identity, commands::identity::cache_avatar_icon, diff --git a/src-tauri/src/storage/disk.rs b/src-tauri/src/storage/disk.rs index 626336a..c047196 100644 --- a/src-tauri/src/storage/disk.rs +++ b/src-tauri/src/storage/disk.rs @@ -744,6 +744,27 @@ impl DiskStorage { Ok(()) } + // save a directory entry only if the peer is not already known + // preserves existing data (bio, public_key) when upserting relay stubs + pub fn save_directory_entry_if_new(&self, entry: &DirectoryEntry) -> Result<(), io::Error> { + let conn = self.open_conn()?; + conn.execute( + "INSERT OR IGNORE INTO directory_entries ( + peer_id, display_name, bio, public_key, last_seen, is_friend + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", + params![ + entry.peer_id, + entry.display_name, + entry.bio, + entry.public_key, + entry.last_seen as i64, + if entry.is_friend { 1_i64 } else { 0_i64 } + ], + ) + .map_err(sqlite_to_io_error)?; + Ok(()) + } + // load the entire peer directory pub fn load_directory(&self) -> Result, io::Error> { let conn = self.open_conn()?;