diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index edbc1fd..57eee0b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -61,11 +61,13 @@ fn specta_builder() -> tauri_specta::Builder { profile::get_profile, profile::update_profile, keypair::get_public_key, + network::list_statuses, ]) .events(tauri_specta::collect_events![ friends::FriendsChanged, remotes::RemotesChanged, profile::ProfileChanged, + network::NetworkStatusChanged, ]) } diff --git a/src-tauri/src/network/mod.rs b/src-tauri/src/network/mod.rs index 3600936..10a82fb 100644 --- a/src-tauri/src/network/mod.rs +++ b/src-tauri/src/network/mod.rs @@ -1,9 +1,12 @@ use std::collections::HashMap; -use std::sync::Mutex; +use std::sync::{Arc, Mutex}; use std::time::Duration; use futures_util::{SinkExt, StreamExt}; -use tauri::{AppHandle, Manager}; +use serde::{Deserialize, Serialize}; +use specta::Type; +use tauri::{AppHandle, Manager, State}; +use tauri_specta::Event; use tokio::sync::mpsc; use tokio_tungstenite::tungstenite::Message; use wyd_common::{ClientMessage, Profile, ServerMessage, message_bytes, register_bytes}; @@ -12,8 +15,34 @@ use crate::db::AppDatabase; use crate::keypair::AppKeypair; use crate::remotes::{self, Remote}; +type Statuses = Arc>>; + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub enum ConnectionState { + Connecting, + Connected, + Disconnected, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct ConnectionStatus { + pub remote_id: String, + pub address: String, + pub name: Option, + pub state: ConnectionState, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] +#[serde(rename_all = "camelCase")] +pub struct NetworkStatusChanged { + pub statuses: Vec, +} + pub struct Network { senders: Mutex>>, + statuses: Statuses, } impl Network { @@ -35,34 +64,59 @@ pub async fn init(handle: &AppHandle) -> Result<(), Box> let profile = crate::profile::get(&database, keypair.public_key()).await?; let remotes = remotes::all(&database).await?; let mut senders = HashMap::new(); + let statuses = Statuses::default(); for remote in remotes { let (sender, receiver) = mpsc::channel(32); senders.insert(remote.id.clone(), sender); - tauri::async_runtime::spawn(run(remote, profile.clone(), keypair.clone(), receiver)); + set(&statuses, &remote, ConnectionState::Connecting); + tauri::async_runtime::spawn(run( + handle.clone(), + statuses.clone(), + remote, + profile.clone(), + keypair.clone(), + receiver, + )); } handle.manage(Network { senders: Mutex::new(senders), + statuses, }); Ok(()) } async fn run( + handle: AppHandle, + statuses: Statuses, remote: Remote, profile: crate::user::User, keypair: AppKeypair, mut outgoing: mpsc::Receiver, ) { loop { - if let Err(error) = connect(&remote, &profile, &keypair, &mut outgoing).await { + changed(&handle, &statuses, &remote, ConnectionState::Connecting); + if let Err(error) = connect( + &handle, + &statuses, + &remote, + &profile, + &keypair, + &mut outgoing, + ) + .await + { eprintln!("remote {} disconnected: {error}", remote.id); } + changed(&handle, &statuses, &remote, ConnectionState::Disconnected); tokio::time::sleep(Duration::from_secs(5)).await; } } async fn connect( + handle: &AppHandle, + statuses: &Statuses, remote: &Remote, profile: &crate::user::User, keypair: &AppKeypair, @@ -93,6 +147,7 @@ async fn connect( if !matches!(recv(&mut reader).await?, ServerMessage::Registered) { return Err("server rejected registration".into()); } + changed(handle, statuses, remote, ConnectionState::Connected); loop { tokio::select! { @@ -112,6 +167,53 @@ async fn connect( } } +#[tauri::command] +#[specta::specta] +pub fn list_statuses( + handle: AppHandle, + network: State<'_, Network>, +) -> Result, String> { + let statuses = snapshot(&network.statuses)?; + NetworkStatusChanged { + statuses: statuses.clone(), + } + .emit(&handle) + .map_err(|error| error.to_string())?; + Ok(statuses) +} + +fn changed(handle: &AppHandle, statuses: &Statuses, remote: &Remote, state: ConnectionState) { + set(statuses, remote, state); + if let Ok(statuses) = snapshot(statuses) { + let _ = NetworkStatusChanged { statuses }.emit(handle); + } +} + +fn set(statuses: &Statuses, remote: &Remote, state: ConnectionState) { + if let Ok(mut statuses) = statuses.lock() { + statuses.insert( + remote.id.clone(), + ConnectionStatus { + remote_id: remote.id.clone(), + address: remote.address.clone(), + name: remote.name.clone(), + state, + }, + ); + } +} + +fn snapshot(statuses: &Statuses) -> Result, String> { + let mut statuses: Vec<_> = statuses + .lock() + .map_err(|error| error.to_string())? + .values() + .cloned() + .collect(); + statuses.sort_by(|a, b| a.remote_id.cmp(&b.remote_id)); + Ok(statuses) +} + async fn send( writer: &mut S, message: &ClientMessage, diff --git a/src/lib/bindings.ts b/src/lib/bindings.ts index ca1d4a1..446d277 100644 --- a/src/lib/bindings.ts +++ b/src/lib/bindings.ts @@ -40,6 +40,9 @@ async updateProfile(displayName: string) : Promise { }, async getPublicKey() : Promise { return await TAURI_INVOKE("get_public_key"); +}, +async listStatuses() : Promise { + return await TAURI_INVOKE("list_statuses"); } } @@ -48,10 +51,12 @@ async getPublicKey() : Promise { export const events = __makeEvents__<{ friendsChanged: FriendsChanged, +networkStatusChanged: NetworkStatusChanged, profileChanged: ProfileChanged, remotesChanged: RemotesChanged }>({ friendsChanged: "friends-changed", +networkStatusChanged: "network-status-changed", profileChanged: "profile-changed", remotesChanged: "remotes-changed" }) @@ -62,7 +67,10 @@ remotesChanged: "remotes-changed" /** user-defined types **/ +export type ConnectionState = "connecting" | "connected" | "disconnected" +export type ConnectionStatus = { remoteId: string; address: string; name: string | null; state: ConnectionState } export type FriendsChanged = { friends: User[] } +export type NetworkStatusChanged = { statuses: ConnectionStatus[] } export type ProfileChanged = { profile: User } export type Remote = { id: string; address: string; name: string | null; port: number | null } export type RemoteInput = { address: string; name: string | null; port: number | null } diff --git a/src/lib/components/connection-status.svelte b/src/lib/components/connection-status.svelte new file mode 100644 index 0000000..659e692 --- /dev/null +++ b/src/lib/components/connection-status.svelte @@ -0,0 +1,47 @@ + + +
+

Connections

+ + {#if error} +

{error}

+ {:else if statuses.length === 0} +

No remote connections configured.

+ {:else} +
    + {#each statuses as status (status.remoteId)} +
  • + {status.name ?? status.address}: {status.state} + {#if status.name} + — {status.address} + {/if} +
  • + {/each} +
+ {/if} +
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 9ec2efe..9dc496d 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,5 +1,6 @@ @@ -14,4 +15,7 @@
+
+ +