From 3147c9ab9e8386a435ceead30e8e930bab16fcdd Mon Sep 17 00:00:00 2001 From: Sam Sartor Date: Sun, 29 Mar 2026 13:25:38 -0600 Subject: [PATCH] put model state in an arc --- gui/src/app.rs | 83 +++++++++++++++++++++++++----------- gui/src/imp/connect.rs | 5 ++- gui/src/imp/desktop.rs | 7 ++- gui/src/imp/mobile.rs | 5 ++- gui/src/imp/mod.rs | 6 ++- gui/src/imp/native_config.rs | 6 +-- gui/src/imp/stub.rs | 4 +- gui/src/imp/web.rs | 8 ++-- gui/src/lib.rs | 43 ++++++++++--------- 9 files changed, 103 insertions(+), 64 deletions(-) diff --git a/gui/src/app.rs b/gui/src/app.rs index 16ceaa6..fb76ad3 100644 --- a/gui/src/app.rs +++ b/gui/src/app.rs @@ -4,13 +4,18 @@ use dioxus::prelude::*; use mime_guess::Mime; use mumble_web2_common::{ProxyOverrides, ServerStatus}; use ordermap::OrderSet; -use std::collections::{HashMap, HashSet}; +use std::{ + collections::{HashMap, HashSet}, + fmt, + sync::Arc, +}; use crate::imp::{ConfigSystem, ConfigSystemInterface as _, Platform, PlatformInterface as _}; pub type ChannelId = u32; pub type UserId = u32; +#[derive(Debug)] pub enum ConnectionState { Disconnected, Connecting, @@ -54,7 +59,7 @@ pub enum Command { use Command::*; use ConnectionState::*; -#[derive(Default)] +#[derive(Default, Debug)] pub struct UserState { pub name: String, pub channel: ChannelId, @@ -79,13 +84,14 @@ impl UserState { } } +#[derive(Debug)] pub struct Chat { pub raw: String, pub dangerous_html: String, pub sender: Option, } -#[derive(Default)] +#[derive(Default, Debug)] pub struct ChannelState { pub name: String, pub children: OrderSet, @@ -111,7 +117,7 @@ impl ChannelState { } } -#[derive(Default)] +#[derive(Default, Debug)] pub struct ChannelsState { pub channels: HashMap, } @@ -198,7 +204,7 @@ impl ChannelsState { } } -#[derive(Default)] +#[derive(Default, Debug)] pub struct ServerState { pub channels_state: ChannelsState, pub users: HashMap, @@ -217,10 +223,16 @@ pub struct State { pub server: GlobalSignal, } -pub static STATE: State = State { - status: Signal::global(|| Disconnected), - server: Signal::global(|| Default::default()), -}; +impl fmt::Debug for State { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("State") + .field("status", &self.status.read()) + .field("server", &self.server.read()) + .finish() + } +} + +pub type SharedState = Arc; #[derive(Clone, Copy, PartialEq, Eq)] pub enum UserIcon { @@ -267,7 +279,8 @@ pub fn UserPill(name: String, icon: UserIcon, isself: bool) -> Element { #[component] pub fn User(id: UserId) -> Element { - let server = STATE.server.read(); + let state = use_context::(); + let server = state.server.read(); match server.users.get(&id) { Some(state) => rsx!(UserPill { name: state.name.clone(), @@ -285,7 +298,8 @@ pub fn User(id: UserId) -> Element { #[component] pub fn Channel(id: ChannelId) -> Element { let net: Coroutine = use_coroutine_handle(); - let server = STATE.server.read(); + let state = use_context::(); + let server = state.server.read(); let user = server.session.unwrap(); let Some(state) = server.channels_state.channels.get(&id) else { return rsx!("missing channel {id}"); @@ -354,7 +368,8 @@ pub fn Channel(id: ChannelId) -> Element { #[cfg(any(feature = "desktop", feature = "web"))] pub fn pick_and_send_file(net: &Coroutine) { - let channels = if let Some(user) = STATE.server.read().this_user() { + let state = use_context::(); + let channels = if let Some(user) = state.server.read().this_user() { vec![user.channel] } else { return; @@ -380,11 +395,14 @@ pub fn pick_and_send_file(net: &Coroutine) {} #[component] pub fn ChatView() -> Element { let net: Coroutine = use_coroutine_handle(); - let server = STATE.server.read(); + let state = use_context::(); + let server = state.server.read(); let mut draft = use_signal(|| "".to_string()); let mut do_send = move || { - if let Some(user) = STATE.server.read().this_user() { + let state = use_context::(); + let server = state.server.read(); + if let Some(user) = server.this_user() { net.send(SendChat { markdown: draft.write().split_off(0), channels: vec![user.channel], @@ -456,8 +474,9 @@ pub fn ChatView() -> Element { #[component] pub fn ControlView(overrides: Resource) -> Element { let net: Coroutine = use_coroutine_handle(); - let status = &STATE.status; - let server = STATE.server.read(); + let state = use_context::(); + let status = &state.status; + let server = state.server.read(); let Some(&UserState { deaf, self_deaf, @@ -645,9 +664,10 @@ pub fn ControlView(overrides: Resource) -> Element { } #[component] -pub fn ServerView(overrides: Resource, user_config: ConfigSystem) -> Element { +pub fn ServerView(overrides: Resource) -> Element { let net: Coroutine = use_coroutine_handle(); - let server = STATE.server.read(); + let state = use_context::(); + let server = state.server.read(); let Some(&UserState { deaf, self_deaf, @@ -683,7 +703,8 @@ pub fn ServerView(overrides: Resource, user_config: ConfigSystem } #[component] -pub fn LoginView(overrides: Resource, user_config: ConfigSystem) -> Element { +pub fn LoginView(overrides: Resource) -> Element { + let user_config = use_context::(); let net: Coroutine = use_coroutine_handle(); let last_status = use_signal(|| None::>); @@ -720,7 +741,8 @@ pub fn LoginView(overrides: Resource, user_config: ConfigSystem) config: overrides.read().clone().unwrap_or_default(), }) }; - let status = &STATE.status; + let state = use_context::(); + let status = &state.status; let bottom = match &*status.read() { Disconnected => rsx! { button { @@ -857,7 +879,17 @@ pub fn LoginView(overrides: Resource, user_config: ConfigSystem) pub fn app() -> Element { static STYLE: Asset = asset!("/assets/main.scss"); - use_coroutine(|rx: UnboundedReceiver| super::network_entrypoint(rx)); + provide_context(ConfigSystem::new().unwrap()); + provide_context(SharedState::new(State { + status: Signal::global(|| Disconnected), + server: Signal::global(Default::default), + })); + + let state = use_context::(); + let network_state = state.clone(); + use_coroutine(move |rx: UnboundedReceiver| { + super::network_entrypoint(rx, network_state.clone()) + }); let overrides = use_resource(|| async move { match Platform::load_proxy_overrides().await { Ok(overrides) => overrides, @@ -865,18 +897,17 @@ pub fn app() -> Element { } }); - let user_config = ConfigSystem::new().unwrap(); - Platform::request_permissions(); + let state = use_context::(); rsx!( document::Link{ rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200..1000;1,200..1000&display=swap" } document::Link{ rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" } document::Link{ rel: "stylesheet", href: STYLE } - match *STATE.status.read() { - Connected => rsx!(ServerView { overrides, user_config }), - _ => rsx!(LoginView { overrides, user_config }), + match *state.status.read() { + Connected => rsx!(ServerView { overrides }), + _ => rsx!(LoginView { overrides }), } ) } diff --git a/gui/src/imp/connect.rs b/gui/src/imp/connect.rs index 04a5a61..e4e7b96 100644 --- a/gui/src/imp/connect.rs +++ b/gui/src/imp/connect.rs @@ -1,4 +1,4 @@ -use crate::app::Command; +use crate::app::{Command, SharedState}; use color_eyre::eyre::{bail, Error}; use dioxus::hooks::UnboundedReceiver; use mumble_protocol::control::ClientControlCodec; @@ -74,6 +74,7 @@ pub async fn network_connect( username: String, event_rx: &mut UnboundedReceiver, overrides: &ProxyOverrides, + state: SharedState, ) -> Result<(), Error> { info!("connecting"); @@ -102,7 +103,7 @@ pub async fn network_connect( let reader = asynchronous_codec::FramedRead::new(read_server.compat(), read_codec); let writer = asynchronous_codec::FramedWrite::new(write_server.compat_write(), write_codec); - crate::network_loop(username, event_rx, reader, writer).await + crate::network_loop(username, state, event_rx, reader, writer).await } pub async fn get_status(client: &reqwest::Client) -> color_eyre::Result { diff --git a/gui/src/imp/desktop.rs b/gui/src/imp/desktop.rs index 8ea7ee6..8cff633 100644 --- a/gui/src/imp/desktop.rs +++ b/gui/src/imp/desktop.rs @@ -1,9 +1,7 @@ -use crate::app::Command; +use crate::app::{Command, SharedState}; use color_eyre::eyre::Error; use dioxus::hooks::UnboundedReceiver; -use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs}; use mumble_web2_common::{ProxyOverrides, ServerStatus}; -use std::collections::HashMap; use std::time::Duration; /// Desktop platform implementation using Tokio and native audio. @@ -30,8 +28,9 @@ impl super::PlatformInterface for DesktopPlatform { username: String, event_rx: &mut UnboundedReceiver, overrides: &ProxyOverrides, + state: SharedState, ) -> Result<(), Error> { - super::connect::network_connect(address, username, event_rx, overrides).await + super::connect::network_connect(address, username, event_rx, overrides, state).await } async fn get_status(client: &reqwest::Client) -> color_eyre::Result { diff --git a/gui/src/imp/mobile.rs b/gui/src/imp/mobile.rs index 8ff89f6..0a15995 100644 --- a/gui/src/imp/mobile.rs +++ b/gui/src/imp/mobile.rs @@ -1,4 +1,4 @@ -use crate::app::Command; +use crate::app::{Command, SharedState}; use color_eyre::eyre::Error; use dioxus::hooks::UnboundedReceiver; use mumble_web2_common::{ProxyOverrides, ServerStatus}; @@ -24,8 +24,9 @@ impl super::PlatformInterface for MobilePlatform { username: String, event_rx: &mut UnboundedReceiver, overrides: &ProxyOverrides, + state: SharedState, ) -> Result<(), Error> { - super::connect::network_connect(address, username, event_rx, overrides).await + super::connect::network_connect(address, username, event_rx, overrides, state).await } async fn get_status(client: &reqwest::Client) -> color_eyre::Result { diff --git a/gui/src/imp/mod.rs b/gui/src/imp/mod.rs index 06fe510..7504a97 100644 --- a/gui/src/imp/mod.rs +++ b/gui/src/imp/mod.rs @@ -4,7 +4,8 @@ //! The traits make the platform boundary explicit and provide compile-time verification. #![allow(async_fn_in_trait)] -use crate::{app::Command, effects::AudioProcessor}; +use crate::app::{Command, SharedState}; +use crate::effects::AudioProcessor; use color_eyre::eyre::Error; use dioxus::hooks::UnboundedReceiver; use mumble_web2_common::{ProxyOverrides, ServerStatus}; @@ -51,7 +52,7 @@ pub trait AudioPlayerInterface { fn play_opus(&mut self, payload: &[u8]); } -pub trait ConfigSystemInterface: Sized { +pub trait ConfigSystemInterface: Sized + Clone { fn new() -> Result; fn config_get(&self, key: &str) -> Option @@ -82,6 +83,7 @@ pub trait PlatformInterface { username: String, event_rx: &mut UnboundedReceiver, proxy_overrides: &ProxyOverrides, + state: SharedState, ) -> impl Future>; /// Get server status (user count, version, etc.). diff --git a/gui/src/imp/native_config.rs b/gui/src/imp/native_config.rs index bf471d9..6ae3b20 100644 --- a/gui/src/imp/native_config.rs +++ b/gui/src/imp/native_config.rs @@ -1,10 +1,6 @@ -use crate::app::Command; use color_eyre::eyre::Error; -use dioxus::hooks::UnboundedReceiver; -use mumble_web2_common::ServerStatus; use std::collections::HashMap; -use std::time::Duration; -use tracing::{error, info, warn}; +use tracing::info; #[derive(Clone, PartialEq)] pub struct NativeConfigSystem { diff --git a/gui/src/imp/stub.rs b/gui/src/imp/stub.rs index 4bf1973..a73aeb3 100644 --- a/gui/src/imp/stub.rs +++ b/gui/src/imp/stub.rs @@ -1,6 +1,6 @@ /// Stub implementation of the platform interface, so that we can /// `cargo check` without any --feature flags. -use crate::effects::AudioProcessor; +use crate::{app::SharedState, effects::AudioProcessor}; use color_eyre::eyre::Error; use dioxus::hooks::UnboundedReceiver; use mumble_web2_common::{ProxyOverrides, ServerStatus}; @@ -25,6 +25,7 @@ impl super::PlatformInterface for StubPlatform { _username: String, _event_rx: &mut UnboundedReceiver, _overrides: &ProxyOverrides, + _state: SharedState, ) -> impl Future> { async { panic!("stubbed platform") } } @@ -77,6 +78,7 @@ impl super::AudioPlayerInterface for StubAudioPlayer { } } +#[derive(Clone)] pub struct StubConfigSystem; impl super::ConfigSystemInterface for StubConfigSystem { diff --git a/gui/src/imp/web.rs b/gui/src/imp/web.rs index 432c593..8ca52e3 100644 --- a/gui/src/imp/web.rs +++ b/gui/src/imp/web.rs @@ -1,4 +1,4 @@ -use crate::app::Command; +use crate::app::{Command, SharedState}; use crate::effects::{AudioProcessor, AudioProcessorSender, TransmitState}; use color_eyre::eyre::{bail, eyre, Error}; use crossbeam::atomic::AtomicCell; @@ -111,8 +111,9 @@ impl super::PlatformInterface for WebPlatform { username: String, event_rx: &mut UnboundedReceiver, overrides: &ProxyOverrides, + state: SharedState, ) -> Result<(), Error> { - network_connect(address, username, event_rx, overrides).await + network_connect(address, username, event_rx, overrides, state).await } async fn get_status(client: &reqwest::Client) -> color_eyre::Result { @@ -434,6 +435,7 @@ pub async fn network_connect( username: String, event_rx: &mut UnboundedReceiver, overrides: &ProxyOverrides, + state: SharedState, ) -> Result<(), Error> { info!("connecting"); @@ -492,7 +494,7 @@ pub async fn network_connect( let writer = asynchronous_codec::FramedWrite::new(wasm_stream_writable.into_async_write(), write_codec); - crate::network_loop(username, event_rx, reader, writer).await + crate::network_loop(username, state, event_rx, reader, writer).await } pub fn absolute_url(path: &str) -> Result { diff --git a/gui/src/lib.rs b/gui/src/lib.rs index bb875b2..fd632d6 100644 --- a/gui/src/lib.rs +++ b/gui/src/lib.rs @@ -1,7 +1,6 @@ use app::Chat; use app::Command; use app::ConnectionState; -use app::STATE; use asynchronous_codec::FramedRead; use asynchronous_codec::FramedWrite; use color_eyre::eyre::{bail, Error}; @@ -27,6 +26,8 @@ use std::time::Duration; use tracing::error; use tracing::info; +use crate::app::SharedState; +use crate::app::State; use crate::effects::AudioProcessor; use crate::imp::{ AudioPlayer, AudioPlayerInterface as _, AudioSystem, AudioSystemInterface as _, Platform, @@ -38,7 +39,7 @@ mod effects; pub mod imp; mod msghtml; -pub async fn network_entrypoint(mut event_rx: UnboundedReceiver) { +pub async fn network_entrypoint(mut event_rx: UnboundedReceiver, state: SharedState) { loop { let Some(Command::Connect { address, @@ -49,21 +50,23 @@ pub async fn network_entrypoint(mut event_rx: UnboundedReceiver) { panic!("did not receive connect command") }; - *STATE.server.write() = Default::default(); - *STATE.status.write() = ConnectionState::Connecting; + *state.server.write() = Default::default(); + *state.status.write() = ConnectionState::Connecting; if let Err(error) = - Platform::network_connect(address, username, &mut event_rx, &config).await + Platform::network_connect(address, username, &mut event_rx, &config, state.clone()) + .await { error!("could not connect {:?}", error); - *STATE.status.write() = ConnectionState::Failed(error.to_string()); + *state.status.write() = ConnectionState::Failed(error.to_string()); } else { - *STATE.status.write() = ConnectionState::Disconnected; + *state.status.write() = ConnectionState::Disconnected; } } } pub async fn network_loop( username: String, + state: SharedState, event_rx: &mut UnboundedReceiver, mut reader: FramedRead>, mut writer: FramedWrite>, @@ -149,7 +152,7 @@ pub async fn network_loop break, Some(command) => { - let res = accept_command(command, &mut send_chan, &mut audio); + let res = accept_command(command, &mut send_chan, &mut audio, &state); if let Err(err) = res { info!("error accepting command {:?}", err) } @@ -187,9 +190,10 @@ fn accept_command( command: Command, send_chan: &mut UnboundedSender>, audio: &mut AudioSystem, + state: &State, ) -> Result<(), Error> { use Command::*; - let Some(session) = STATE.server.read().session else { + let Some(session) = state.server.read().session else { bail!("no session id") }; @@ -212,7 +216,7 @@ fn accept_command( }; { - let mut server = STATE.server.write(); + let mut server = state.server.write(); let Some(me) = server.session else { bail!("not signed in with a session id") }; @@ -253,7 +257,7 @@ fn accept_command( }; { - let mut server = STATE.server.write(); + let mut server = state.server.write(); let Some(me) = server.session else { bail!("not signed in with a session id") }; @@ -304,6 +308,7 @@ fn accept_packet( msg: ControlPacket, audio_context: &mut AudioSystem, player_map: &mut HashMap, + state: &State, ) -> Result<(), Error> { match msg { ControlPacket::UDPTunnel(u) => { @@ -340,15 +345,15 @@ fn accept_packet( } } ControlPacket::ChannelState(u) => { - let mut server = STATE.server.write(); + let mut server = state.server.write(); server.channels_state.update_from_channel_state(&u); } ControlPacket::ChannelRemove(u) => { - let mut server = STATE.server.write(); + let mut server = state.server.write(); server.channels_state.update_from_channel_remove(&u); } ControlPacket::UserState(u) => { - let mut server = STATE.server.write(); + let mut server = state.server.write(); let server = &mut *server; let id = u.get_session(); @@ -392,7 +397,7 @@ fn accept_packet( } } ControlPacket::UserRemove(u) => { - let mut server = STATE.server.write(); + let mut server = state.server.write(); let id = u.get_session(); if let Some(state) = server.users.remove(&id) { if let Some(parent) = server.channels_state.channels.get_mut(&state.channel) { @@ -401,7 +406,7 @@ fn accept_packet( } } ControlPacket::TextMessage(u) => { - let mut server = STATE.server.write(); + let mut server = state.server.write(); if u.has_message() { let text = u.get_message().to_string(); server.chat.push(Chat { @@ -416,8 +421,8 @@ fn accept_packet( } } ControlPacket::ServerSync(u) => { - *STATE.status.write() = ConnectionState::Connected; - let mut server = STATE.server.write(); + *state.status.write() = ConnectionState::Connected; + let mut server = state.server.write(); if u.has_welcome_text() { let text = u.get_welcome_text().to_string(); server.chat.push(Chat {