feat(relay): add custom relay address handling and UI for selection
This commit is contained in:
parent
b4f75cd995
commit
ed7e6d6239
|
|
@ -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?;
|
||||
|
||||
|
|
|
|||
|
|
@ -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::<libp2p::Multiaddr>()
|
||||
.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> {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<libp2p::Multiaddr> {
|
||||
// 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<libp2p::Multiaddr> {
|
||||
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<crate::storage::DiskStorage>,
|
||||
app_handle: tauri::AppHandle,
|
||||
voice_channels: VoiceChannelMap,
|
||||
custom_relay_addr: Option<String>,
|
||||
) -> Result<NodeHandle, String> {
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<SplashScreenProps> = (props) => {
|
|||
const [retrying, setRetrying] = createSignal(false);
|
||||
// null = not checked yet, true = internet works, false = no internet
|
||||
const [hasInternet, setHasInternet] = createSignal<boolean | null>(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<SplashScreenProps> = (props) => {
|
|||
{connectivityHint()}
|
||||
</p>
|
||||
</Show>
|
||||
|
||||
{/* show relay picker button if relay is unreachable but internet works */}
|
||||
<Show when={retrying() && hasInternet()}>
|
||||
<div class="flex flex-col items-center gap-3 mt-4">
|
||||
<p class="text-white/50 text-xs font-sans">
|
||||
the default relay may be at capacity or offline
|
||||
</p>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => setShowRelayPicker(true)}
|
||||
>
|
||||
choose a different relay
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* relay picker modal */}
|
||||
<Show when={showRelayPicker()}>
|
||||
<div class="fixed inset-0 z-10000 bg-black/90 flex items-center justify-center">
|
||||
<div class="bg-gray-900 border-2 border-white/20 p-6 max-w-md w-full mx-4">
|
||||
<h2 class="text-white text-lg font-sans mb-4">select a relay</h2>
|
||||
<p class="text-white/60 text-sm font-sans mb-6">
|
||||
choose an alternative relay server or enter a custom address
|
||||
</p>
|
||||
|
||||
<div class="space-y-2 mb-6">
|
||||
<For each={alternativeRelays}>
|
||||
{(relay) => (
|
||||
<button
|
||||
class="w-full text-left px-4 py-3 bg-gray-800 hover:bg-gray-700 border border-white/10 hover:border-accent transition-colors"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await setRelayAddress(relay.addr);
|
||||
setShowRelayPicker(false);
|
||||
// restart the connection cycle
|
||||
setRetrying(false);
|
||||
setHasInternet(null);
|
||||
startCycle();
|
||||
} catch (e) {
|
||||
console.error("failed to switch relay:", e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="text-white text-sm font-sans">{relay.name}</div>
|
||||
<div class="text-white/40 text-xs font-mono mt-1 truncate">
|
||||
{relay.addr}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="text-white/60 text-xs font-sans mb-2 block">
|
||||
or enter custom relay address
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="w-full bg-gray-800 border border-white/10 px-3 py-2 text-white text-sm font-mono focus:outline-none focus:border-accent"
|
||||
placeholder="/dns4/relay.example.com/tcp/4001/p2p/12D3..."
|
||||
value={customRelay()}
|
||||
onInput={(e) => setCustomRelay(e.currentTarget.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => setShowRelayPicker(false)}
|
||||
class="flex-1"
|
||||
>
|
||||
cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
disabled={!customRelay().trim()}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await setRelayAddress(customRelay());
|
||||
setShowRelayPicker(false);
|
||||
setCustomRelay("");
|
||||
// restart the connection cycle
|
||||
setRetrying(false);
|
||||
setHasInternet(null);
|
||||
startCycle();
|
||||
} catch (e) {
|
||||
console.error("failed to switch to custom relay:", e);
|
||||
alert(`Invalid relay address: ${e}`);
|
||||
}
|
||||
}}
|
||||
class="flex-1"
|
||||
>
|
||||
connect
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={showWelcome() && props.identity}>
|
||||
<p class="text-white/60 text-sm font-sans">
|
||||
connected to dusk chat, welcome {props.identity?.display_name}!
|
||||
|
|
|
|||
|
|
@ -206,6 +206,10 @@ export async function discoverGlobalPeers(): Promise<void> {
|
|||
return invoke("discover_global_peers");
|
||||
}
|
||||
|
||||
export async function setRelayAddress(relayAddr: string): Promise<void> {
|
||||
return invoke("set_relay_address", { relayAddr });
|
||||
}
|
||||
|
||||
export async function resetIdentity(): Promise<void> {
|
||||
return invoke("reset_identity");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue