client server connection UI
This commit is contained in:
@@ -61,11 +61,13 @@ fn specta_builder() -> tauri_specta::Builder<tauri::Wry> {
|
|||||||
profile::get_profile,
|
profile::get_profile,
|
||||||
profile::update_profile,
|
profile::update_profile,
|
||||||
keypair::get_public_key,
|
keypair::get_public_key,
|
||||||
|
network::list_statuses,
|
||||||
])
|
])
|
||||||
.events(tauri_specta::collect_events![
|
.events(tauri_specta::collect_events![
|
||||||
friends::FriendsChanged,
|
friends::FriendsChanged,
|
||||||
remotes::RemotesChanged,
|
remotes::RemotesChanged,
|
||||||
profile::ProfileChanged,
|
profile::ProfileChanged,
|
||||||
|
network::NetworkStatusChanged,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Mutex;
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use futures_util::{SinkExt, StreamExt};
|
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::sync::mpsc;
|
||||||
use tokio_tungstenite::tungstenite::Message;
|
use tokio_tungstenite::tungstenite::Message;
|
||||||
use wyd_common::{ClientMessage, Profile, ServerMessage, message_bytes, register_bytes};
|
use wyd_common::{ClientMessage, Profile, ServerMessage, message_bytes, register_bytes};
|
||||||
@@ -12,8 +15,34 @@ use crate::db::AppDatabase;
|
|||||||
use crate::keypair::AppKeypair;
|
use crate::keypair::AppKeypair;
|
||||||
use crate::remotes::{self, Remote};
|
use crate::remotes::{self, Remote};
|
||||||
|
|
||||||
|
type Statuses = Arc<Mutex<HashMap<String, ConnectionStatus>>>;
|
||||||
|
|
||||||
|
#[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<String>,
|
||||||
|
pub state: ConnectionState,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct NetworkStatusChanged {
|
||||||
|
pub statuses: Vec<ConnectionStatus>,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Network {
|
pub struct Network {
|
||||||
senders: Mutex<HashMap<String, mpsc::Sender<String>>>,
|
senders: Mutex<HashMap<String, mpsc::Sender<String>>>,
|
||||||
|
statuses: Statuses,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Network {
|
impl Network {
|
||||||
@@ -35,34 +64,59 @@ pub async fn init(handle: &AppHandle) -> Result<(), Box<dyn std::error::Error>>
|
|||||||
let profile = crate::profile::get(&database, keypair.public_key()).await?;
|
let profile = crate::profile::get(&database, keypair.public_key()).await?;
|
||||||
let remotes = remotes::all(&database).await?;
|
let remotes = remotes::all(&database).await?;
|
||||||
let mut senders = HashMap::new();
|
let mut senders = HashMap::new();
|
||||||
|
let statuses = Statuses::default();
|
||||||
|
|
||||||
for remote in remotes {
|
for remote in remotes {
|
||||||
let (sender, receiver) = mpsc::channel(32);
|
let (sender, receiver) = mpsc::channel(32);
|
||||||
senders.insert(remote.id.clone(), sender);
|
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 {
|
handle.manage(Network {
|
||||||
senders: Mutex::new(senders),
|
senders: Mutex::new(senders),
|
||||||
|
statuses,
|
||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run(
|
async fn run(
|
||||||
|
handle: AppHandle,
|
||||||
|
statuses: Statuses,
|
||||||
remote: Remote,
|
remote: Remote,
|
||||||
profile: crate::user::User,
|
profile: crate::user::User,
|
||||||
keypair: AppKeypair,
|
keypair: AppKeypair,
|
||||||
mut outgoing: mpsc::Receiver<String>,
|
mut outgoing: mpsc::Receiver<String>,
|
||||||
) {
|
) {
|
||||||
loop {
|
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);
|
eprintln!("remote {} disconnected: {error}", remote.id);
|
||||||
}
|
}
|
||||||
|
changed(&handle, &statuses, &remote, ConnectionState::Disconnected);
|
||||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn connect(
|
async fn connect(
|
||||||
|
handle: &AppHandle,
|
||||||
|
statuses: &Statuses,
|
||||||
remote: &Remote,
|
remote: &Remote,
|
||||||
profile: &crate::user::User,
|
profile: &crate::user::User,
|
||||||
keypair: &AppKeypair,
|
keypair: &AppKeypair,
|
||||||
@@ -93,6 +147,7 @@ async fn connect(
|
|||||||
if !matches!(recv(&mut reader).await?, ServerMessage::Registered) {
|
if !matches!(recv(&mut reader).await?, ServerMessage::Registered) {
|
||||||
return Err("server rejected registration".into());
|
return Err("server rejected registration".into());
|
||||||
}
|
}
|
||||||
|
changed(handle, statuses, remote, ConnectionState::Connected);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
@@ -112,6 +167,53 @@ async fn connect(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub fn list_statuses(
|
||||||
|
handle: AppHandle,
|
||||||
|
network: State<'_, Network>,
|
||||||
|
) -> Result<Vec<ConnectionStatus>, 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<Vec<ConnectionStatus>, 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<S>(
|
async fn send<S>(
|
||||||
writer: &mut S,
|
writer: &mut S,
|
||||||
message: &ClientMessage,
|
message: &ClientMessage,
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ async updateProfile(displayName: string) : Promise<User> {
|
|||||||
},
|
},
|
||||||
async getPublicKey() : Promise<string> {
|
async getPublicKey() : Promise<string> {
|
||||||
return await TAURI_INVOKE("get_public_key");
|
return await TAURI_INVOKE("get_public_key");
|
||||||
|
},
|
||||||
|
async listStatuses() : Promise<ConnectionStatus[]> {
|
||||||
|
return await TAURI_INVOKE("list_statuses");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,10 +51,12 @@ async getPublicKey() : Promise<string> {
|
|||||||
|
|
||||||
export const events = __makeEvents__<{
|
export const events = __makeEvents__<{
|
||||||
friendsChanged: FriendsChanged,
|
friendsChanged: FriendsChanged,
|
||||||
|
networkStatusChanged: NetworkStatusChanged,
|
||||||
profileChanged: ProfileChanged,
|
profileChanged: ProfileChanged,
|
||||||
remotesChanged: RemotesChanged
|
remotesChanged: RemotesChanged
|
||||||
}>({
|
}>({
|
||||||
friendsChanged: "friends-changed",
|
friendsChanged: "friends-changed",
|
||||||
|
networkStatusChanged: "network-status-changed",
|
||||||
profileChanged: "profile-changed",
|
profileChanged: "profile-changed",
|
||||||
remotesChanged: "remotes-changed"
|
remotesChanged: "remotes-changed"
|
||||||
})
|
})
|
||||||
@@ -62,7 +67,10 @@ remotesChanged: "remotes-changed"
|
|||||||
|
|
||||||
/** user-defined types **/
|
/** 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 FriendsChanged = { friends: User[] }
|
||||||
|
export type NetworkStatusChanged = { statuses: ConnectionStatus[] }
|
||||||
export type ProfileChanged = { profile: User }
|
export type ProfileChanged = { profile: User }
|
||||||
export type Remote = { id: string; address: string; name: string | null; port: number | null }
|
export type Remote = { id: string; address: string; name: string | null; port: number | null }
|
||||||
export type RemoteInput = { address: string; name: string | null; port: number | null }
|
export type RemoteInput = { address: string; name: string | null; port: number | null }
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { commands, events, type ConnectionStatus } from "$lib/bindings";
|
||||||
|
|
||||||
|
let statuses: ConnectionStatus[] = [];
|
||||||
|
let error = "";
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const unlisten = events.networkStatusChanged.listen((event) => {
|
||||||
|
statuses = event.payload.statuses;
|
||||||
|
});
|
||||||
|
|
||||||
|
commands
|
||||||
|
.listStatuses()
|
||||||
|
.then((current) => {
|
||||||
|
statuses = current;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = String(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unlisten.then((stop) => stop());
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h1>Connections</h1>
|
||||||
|
|
||||||
|
{#if error}
|
||||||
|
<p aria-live="polite">{error}</p>
|
||||||
|
{:else if statuses.length === 0}
|
||||||
|
<p>No remote connections configured.</p>
|
||||||
|
{:else}
|
||||||
|
<ul aria-live="polite">
|
||||||
|
{#each statuses as status (status.remoteId)}
|
||||||
|
<li>
|
||||||
|
<strong>{status.name ?? status.address}</strong>: {status.state}
|
||||||
|
{#if status.name}
|
||||||
|
<span> — {status.address}</span>
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import FriendsList from "$lib/components/friends-list.svelte";
|
import FriendsList from "$lib/components/friends-list.svelte";
|
||||||
|
import ConnectionStatus from "$lib/components/connection-status.svelte";
|
||||||
import Profile from "$lib/components/profile.svelte";
|
import Profile from "$lib/components/profile.svelte";
|
||||||
import RemotesList from "$lib/components/remotes-list.svelte";
|
import RemotesList from "$lib/components/remotes-list.svelte";
|
||||||
</script>
|
</script>
|
||||||
@@ -14,4 +15,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<RemotesList />
|
<RemotesList />
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<ConnectionStatus />
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
Reference in New Issue
Block a user