From ed7e6d6239c3c89241dad1164dc1a1e7c514a7c4 Mon Sep 17 00:00:00 2001 From: cloudwithax Date: Sat, 14 Feb 2026 22:14:43 -0500 Subject: [PATCH] feat(relay): add custom relay address handling and UI for selection --- src-tauri/src/commands/chat.rs | 8 ++ src-tauri/src/commands/identity.rs | 36 ++++++++ src-tauri/src/lib.rs | 1 + src-tauri/src/node/mod.rs | 15 +++- src-tauri/src/storage/disk.rs | 3 + src/components/auth/SplashScreen.tsx | 124 ++++++++++++++++++++++++++- src/lib/tauri.ts | 4 + src/lib/types.ts | 3 + 8 files changed, 190 insertions(+), 4 deletions(-) diff --git a/src-tauri/src/commands/chat.rs b/src-tauri/src/commands/chat.rs index 88ea71a..cfe3cb0 100644 --- a/src-tauri/src/commands/chat.rs +++ b/src-tauri/src/commands/chat.rs @@ -17,12 +17,20 @@ pub async fn start_node(app: tauri::AppHandle, state: State<'_, AppState>) -> Re .as_ref() .ok_or("no identity loaded, create one first")?; + // load custom relay address from settings if configured + let custom_relay = state + .storage + .load_settings() + .ok() + .and_then(|s| s.custom_relay_addr); + let handle = node::start( id.keypair.clone(), state.crdt_engine.clone(), state.storage.clone(), app, state.voice_channels.clone(), + custom_relay, ) .await?; diff --git a/src-tauri/src/commands/identity.rs b/src-tauri/src/commands/identity.rs index 93fdef5..b0911fe 100644 --- a/src-tauri/src/commands/identity.rs +++ b/src-tauri/src/commands/identity.rs @@ -274,6 +274,42 @@ pub async fn discover_global_peers(state: State<'_, AppState>) -> Result<(), Str Ok(()) } +// change relay address and restart the node +// used when default relay is unreachable or at capacity +#[tauri::command] +pub async fn set_relay_address( + app: tauri::AppHandle, + state: State<'_, AppState>, + relay_addr: String, +) -> Result<(), String> { + // validate the relay address format + let _ = relay_addr + .parse::() + .map_err(|_| "invalid relay address format")?; + + // stop the current node if running + { + let mut node_handle = state.node_handle.lock().await; + if let Some(handle) = node_handle.take() { + let _ = handle.command_tx.send(crate::node::NodeCommand::Shutdown).await; + let _ = handle.task.await; + } + } + + // update settings with the new relay address + let mut settings = state.storage.load_settings().unwrap_or_default(); + settings.custom_relay_addr = Some(relay_addr); + state + .storage + .save_settings(&settings) + .map_err(|e| format!("failed to save settings: {}", e))?; + + // restart the node with the new relay + crate::commands::chat::start_node(app, state).await?; + + Ok(()) +} + // broadcast a revocation to all peers, stop the node, and wipe all local data #[tauri::command] pub async fn reset_identity(state: State<'_, AppState>) -> Result<(), String> { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 77bd45c..73d07f1 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -90,6 +90,7 @@ pub fn run() { commands::identity::add_friend, commands::identity::remove_friend, commands::identity::discover_global_peers, + commands::identity::set_relay_address, commands::identity::reset_identity, commands::chat::send_message, commands::chat::get_messages, diff --git a/src-tauri/src/node/mod.rs b/src-tauri/src/node/mod.rs index 30e4c79..c3513d1 100644 --- a/src-tauri/src/node/mod.rs +++ b/src-tauri/src/node/mod.rs @@ -27,11 +27,13 @@ const PENDING_QUEUE_TTL_SECS: u64 = 600; // prevents banner flashing on transient disconnections const RELAY_WARN_GRACE_SECS: u64 = 8; -// resolve the relay multiaddr from env or default -fn relay_addr() -> Option { +// resolve the relay multiaddr from env var, custom setting, or default +// priority: DUSK_RELAY_ADDR env var > custom setting > DEFAULT_RELAY_ADDR +fn relay_addr(custom_addr: Option<&str>) -> Option { let addr_str = std::env::var("DUSK_RELAY_ADDR") .ok() .filter(|s| !s.is_empty()) + .or_else(|| custom_addr.map(|s| s.to_string())) .or_else(|| { if DEFAULT_RELAY_ADDR.is_empty() { None @@ -186,6 +188,7 @@ pub async fn start( storage: Arc, app_handle: tauri::AppHandle, voice_channels: VoiceChannelMap, + custom_relay_addr: Option, ) -> Result { let mut swarm_instance = swarm::build_swarm(&keypair).map_err(|e| format!("failed to build swarm: {}", e))?; @@ -207,7 +210,7 @@ pub async fn start( ); // resolve the relay address for WAN connectivity - let relay_multiaddr = relay_addr(); + let relay_multiaddr = relay_addr(custom_relay_addr.as_deref()); let relay_peer_id = relay_multiaddr.as_ref().and_then(peer_id_from_multiaddr); // if a relay is configured, dial it immediately @@ -218,7 +221,13 @@ pub async fn start( log::info!("dialing relay at {}", addr); if let Err(e) = swarm_instance.dial(addr.clone()) { log::warn!("failed to dial relay: {}", e); + // emit disconnected status immediately if dial fails + let _ = app_handle.emit("dusk-event", DuskEvent::RelayStatus { connected: false }); } + } else { + // if relay address is invalid or not configured, emit disconnected status + log::warn!("no valid relay address configured, running in LAN-only mode"); + let _ = app_handle.emit("dusk-event", DuskEvent::RelayStatus { connected: false }); } let task = tauri::async_runtime::spawn(async move { diff --git a/src-tauri/src/storage/disk.rs b/src-tauri/src/storage/disk.rs index 1b714c6..541c1a7 100644 --- a/src-tauri/src/storage/disk.rs +++ b/src-tauri/src/storage/disk.rs @@ -22,6 +22,8 @@ pub struct UserSettings { pub allow_dms_from_anyone: bool, pub message_display: String, pub font_size: String, + #[serde(default)] + pub custom_relay_addr: Option, } impl Default for UserSettings { @@ -36,6 +38,7 @@ impl Default for UserSettings { show_online_status: true, allow_dms_from_anyone: true, message_display: "cozy".to_string(), + custom_relay_addr: None, font_size: "default".to_string(), } } diff --git a/src/components/auth/SplashScreen.tsx b/src/components/auth/SplashScreen.tsx index bfc9385..60394e4 100644 --- a/src/components/auth/SplashScreen.tsx +++ b/src/components/auth/SplashScreen.tsx @@ -5,9 +5,11 @@ import { onMount, Show, onCleanup, + For, } from "solid-js"; import type { PublicIdentity } from "../../lib/types"; -import { checkInternetConnectivity } from "../../lib/tauri"; +import { checkInternetConnectivity, setRelayAddress } from "../../lib/tauri"; +import Button from "../common/Button"; interface SplashScreenProps { onComplete: () => void; @@ -23,6 +25,24 @@ const SplashScreen: Component = (props) => { const [retrying, setRetrying] = createSignal(false); // null = not checked yet, true = internet works, false = no internet const [hasInternet, setHasInternet] = createSignal(null); + const [showRelayPicker, setShowRelayPicker] = createSignal(false); + const [customRelay, setCustomRelay] = createSignal(""); + + // alternative public relays (would be populated from a discovery service in production) + const alternativeRelays = [ + { + name: "primary relay (default)", + addr: "/dns4/relay.duskchat.app/tcp/4001/p2p/12D3KooWGQkCkACcibJPKzus7Q6U1aYngfTuS4gwYwmJkJJtrSaw", + }, + { + name: "us-west relay", + addr: "/dns4/relay-us-west.duskchat.app/tcp/4001/p2p/12D3KooWExample1", + }, + { + name: "eu-central relay", + addr: "/dns4/relay-eu.duskchat.app/tcp/4001/p2p/12D3KooWExample2", + }, + ]; // refs for exit SMIL animations so we can trigger them programmatically let exitOrangeCx: SVGAnimateElement | undefined; @@ -230,6 +250,108 @@ const SplashScreen: Component = (props) => { {connectivityHint()}

+ + {/* show relay picker button if relay is unreachable but internet works */} + +
+

+ the default relay may be at capacity or offline +

+ +
+
+ + {/* relay picker modal */} + +
+
+

select a relay

+

+ choose an alternative relay server or enter a custom address +

+ +
+ + {(relay) => ( + + )} + +
+ +
+ + setCustomRelay(e.currentTarget.value)} + /> +
+ +
+ + +
+
+
+
+

connected to dusk chat, welcome {props.identity?.display_name}! diff --git a/src/lib/tauri.ts b/src/lib/tauri.ts index 12a9dd7..88e634c 100644 --- a/src/lib/tauri.ts +++ b/src/lib/tauri.ts @@ -206,6 +206,10 @@ export async function discoverGlobalPeers(): Promise { return invoke("discover_global_peers"); } +export async function setRelayAddress(relayAddr: string): Promise { + return invoke("set_relay_address", { relayAddr }); +} + export async function resetIdentity(): Promise { return invoke("reset_identity"); } diff --git a/src/lib/types.ts b/src/lib/types.ts index 2e5c19f..1982231 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -65,6 +65,9 @@ export interface UserSettings { // appearance message_display: "cozy" | "compact"; font_size: "small" | "default" | "large"; + + // network + custom_relay_addr?: string; } export interface CommunityMeta {