3 Commits

Author SHA1 Message Date
sam 7f35a216cd Persist denoise setting (#24)
Build android container / android-release-builder-container-build (push) Successful in 1s
Build Mumble Web 2 release builder containers / windows-release-builder-container-build (push) Successful in 16s
Build Mumble Web 2 / macos_build (push) Successful in 1m2s
Build Mumble Web 2 / linux_build (push) Successful in 1m26s
Build Mumble Web 2 / windows_build (push) Successful in 3m23s
Build Mumble Web 2 / android_build (push) Successful in 4m59s
Puts the denoise bool into an AudioSettings struct in the model state, and persists changes to user state.

Co-authored-by: Sam Sartor <me@samsartor.com>
Co-committed-by: Sam Sartor <me@samsartor.com>
2026-03-30 01:30:14 +00:00
sam f0ce15000e Put model state into an Arc (#28)
Build Mumble Web 2 / macos_build (push) Successful in 1m14s
Build Mumble Web 2 / linux_build (push) Successful in 1m27s
Build Mumble Web 2 / windows_build (push) Successful in 2m56s
Build Mumble Web 2 / android_build (push) Successful in 4m39s
Previously the model state was in a `static STATE` to make it accessible to all the various subsystems. This moves it into an Arc and plumbs the reference around via function arguments. That allows us to do non-static initialization, eg based on user config. I also moved some things into dioxus context.

Co-authored-by: Sam Sartor <me@samsartor.com>
Co-committed-by: Sam Sartor <me@samsartor.com>
2026-03-30 00:56:36 +00:00
liamwarfield 7337b3e49b Quick fixes for S&T (#27)
Build Mumble Web 2 / macos_build (push) Successful in 1m14s
Build Mumble Web 2 / linux_build (push) Successful in 1m26s
Build Mumble Web 2 / windows_build (push) Successful in 2m51s
Build Mumble Web 2 / android_build (push) Successful in 4m34s
Some quick QAL changes I banged out this morning. The commit messages describe the individual changes in details.

## Changes

- Min window width on desktop.
- Removes white flash on desktop startup
- Removes right click menu on release builds (still exists on debug, and might come back in the future with new features).

Reviewed-on: #27
Reviewed-by: restitux <restitux@ohea.xyz>
2026-03-29 18:24:16 +00:00
11 changed files with 162 additions and 83 deletions
+1
View File
@@ -16,6 +16,7 @@ body {
} }
#main { #main {
visibility: visible;
height: 100vh; height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
+87 -41
View File
@@ -5,12 +5,14 @@ use mime_guess::Mime;
use mumble_web2_common::{ProxyOverrides, ServerStatus}; use mumble_web2_common::{ProxyOverrides, ServerStatus};
use ordermap::OrderSet; use ordermap::OrderSet;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::{fmt, sync::Arc};
use crate::imp::{ConfigSystem, ConfigSystemInterface as _, Platform, PlatformInterface as _}; use crate::imp::{ConfigSystem, ConfigSystemInterface as _, Platform, PlatformInterface as _};
pub type ChannelId = u32; pub type ChannelId = u32;
pub type UserId = u32; pub type UserId = u32;
#[derive(Debug)]
pub enum ConnectionState { pub enum ConnectionState {
Disconnected, Disconnected,
Connecting, Connecting,
@@ -18,6 +20,11 @@ pub enum ConnectionState {
Failed(String), Failed(String),
} }
#[derive(Debug, Clone)]
pub struct AudioSettings {
pub denoise: bool,
}
#[derive(Debug)] #[derive(Debug)]
pub enum Command { pub enum Command {
Connect { Connect {
@@ -45,16 +52,14 @@ pub enum Command {
channel: ChannelId, channel: ChannelId,
user: UserId, user: UserId,
}, },
UpdateMicEffects { UpdateAudioSettings(AudioSettings),
denoise: bool,
},
Disconnect, Disconnect,
} }
use Command::*; use Command::*;
use ConnectionState::*; use ConnectionState::*;
#[derive(Default)] #[derive(Default, Debug)]
pub struct UserState { pub struct UserState {
pub name: String, pub name: String,
pub channel: ChannelId, pub channel: ChannelId,
@@ -79,13 +84,14 @@ impl UserState {
} }
} }
#[derive(Debug)]
pub struct Chat { pub struct Chat {
pub raw: String, pub raw: String,
pub dangerous_html: String, pub dangerous_html: String,
pub sender: Option<UserId>, pub sender: Option<UserId>,
} }
#[derive(Default)] #[derive(Default, Debug)]
pub struct ChannelState { pub struct ChannelState {
pub name: String, pub name: String,
pub children: OrderSet<ChannelId>, pub children: OrderSet<ChannelId>,
@@ -111,7 +117,7 @@ impl ChannelState {
} }
} }
#[derive(Default)] #[derive(Default, Debug)]
pub struct ChannelsState { pub struct ChannelsState {
pub channels: HashMap<ChannelId, ChannelState>, pub channels: HashMap<ChannelId, ChannelState>,
} }
@@ -198,7 +204,7 @@ impl ChannelsState {
} }
} }
#[derive(Default)] #[derive(Default, Debug)]
pub struct ServerState { pub struct ServerState {
pub channels_state: ChannelsState, pub channels_state: ChannelsState,
pub users: HashMap<UserId, UserState>, pub users: HashMap<UserId, UserState>,
@@ -213,14 +219,21 @@ impl ServerState {
} }
pub struct State { pub struct State {
pub status: GlobalSignal<ConnectionState>, pub status: Signal<ConnectionState>,
pub server: GlobalSignal<ServerState>, pub server: Signal<ServerState>,
pub audio: Signal<AudioSettings>,
} }
pub static STATE: State = State { impl fmt::Debug for State {
status: Signal::global(|| Disconnected), fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
server: Signal::global(|| Default::default()), f.debug_struct("State")
}; .field("status", &self.status.read())
.field("server", &self.server.read())
.finish()
}
}
pub type SharedState = Arc<State>;
#[derive(Clone, Copy, PartialEq, Eq)] #[derive(Clone, Copy, PartialEq, Eq)]
pub enum UserIcon { pub enum UserIcon {
@@ -267,7 +280,8 @@ pub fn UserPill(name: String, icon: UserIcon, isself: bool) -> Element {
#[component] #[component]
pub fn User(id: UserId) -> Element { pub fn User(id: UserId) -> Element {
let server = STATE.server.read(); let state = use_context::<SharedState>();
let server = state.server.read();
match server.users.get(&id) { match server.users.get(&id) {
Some(state) => rsx!(UserPill { Some(state) => rsx!(UserPill {
name: state.name.clone(), name: state.name.clone(),
@@ -285,7 +299,8 @@ pub fn User(id: UserId) -> Element {
#[component] #[component]
pub fn Channel(id: ChannelId) -> Element { pub fn Channel(id: ChannelId) -> Element {
let net: Coroutine<Command> = use_coroutine_handle(); let net: Coroutine<Command> = use_coroutine_handle();
let server = STATE.server.read(); let state = use_context::<SharedState>();
let server = state.server.read();
let user = server.session.unwrap(); let user = server.session.unwrap();
let Some(state) = server.channels_state.channels.get(&id) else { let Some(state) = server.channels_state.channels.get(&id) else {
return rsx!("missing channel {id}"); return rsx!("missing channel {id}");
@@ -354,7 +369,8 @@ pub fn Channel(id: ChannelId) -> Element {
#[cfg(any(feature = "desktop", feature = "web"))] #[cfg(any(feature = "desktop", feature = "web"))]
pub fn pick_and_send_file(net: &Coroutine<Command>) { pub fn pick_and_send_file(net: &Coroutine<Command>) {
let channels = if let Some(user) = STATE.server.read().this_user() { let state = use_context::<SharedState>();
let channels = if let Some(user) = state.server.read().this_user() {
vec![user.channel] vec![user.channel]
} else { } else {
return; return;
@@ -380,11 +396,14 @@ pub fn pick_and_send_file(net: &Coroutine<Command>) {}
#[component] #[component]
pub fn ChatView() -> Element { pub fn ChatView() -> Element {
let net: Coroutine<Command> = use_coroutine_handle(); let net: Coroutine<Command> = use_coroutine_handle();
let server = STATE.server.read(); let state = use_context::<SharedState>();
let server = state.server.read();
let mut draft = use_signal(|| "".to_string()); let mut draft = use_signal(|| "".to_string());
let mut do_send = move || { let mut do_send = move || {
if let Some(user) = STATE.server.read().this_user() { let state = use_context::<SharedState>();
let server = state.server.read();
if let Some(user) = server.this_user() {
net.send(SendChat { net.send(SendChat {
markdown: draft.write().split_off(0), markdown: draft.write().split_off(0),
channels: vec![user.channel], channels: vec![user.channel],
@@ -456,8 +475,10 @@ pub fn ChatView() -> Element {
#[component] #[component]
pub fn ControlView(overrides: Resource<ProxyOverrides>) -> Element { pub fn ControlView(overrides: Resource<ProxyOverrides>) -> Element {
let net: Coroutine<Command> = use_coroutine_handle(); let net: Coroutine<Command> = use_coroutine_handle();
let status = &STATE.status; let state = use_context::<SharedState>();
let server = STATE.server.read(); let status = &state.status;
let server = state.server.read();
let audio = state.audio.read();
let Some(&UserState { let Some(&UserState {
deaf, deaf,
self_deaf, self_deaf,
@@ -555,7 +576,6 @@ pub fn ControlView(overrides: Resource<ProxyOverrides>) -> Element {
}, },
}; };
let denoise = use_signal(|| false);
rsx!( rsx!(
// Server control // Server control
div { div {
@@ -596,18 +616,23 @@ pub fn ControlView(overrides: Resource<ProxyOverrides>) -> Element {
} }
span { class: "spacer" } span { class: "spacer" }
button { button {
class: match denoise() { class: match audio.denoise {
true => "toggle_button is_on", true => "toggle_button is_on",
false => "toggle_button", false => "toggle_button",
}, },
role: "switch", role: "switch",
aria_checked: denoise(), aria_checked: audio.denoise,
onclick: move |_| { onclick: move |_| {
let new_denoise = !denoise(); let state = use_context::<SharedState>();
*denoise.write_unchecked() = new_denoise; let mut audio = state.audio.read().clone();
net.send(UpdateMicEffects { denoise: new_denoise }) audio.denoise = !audio.denoise;
let denoise = audio.denoise;
*state.audio.write_unchecked() = audio;
net.send(UpdateAudioSettings(AudioSettings { denoise: denoise }));
let user_config = use_context::<ConfigSystem>();
user_config.config_set::<bool>("denoise", &denoise);
}, },
match denoise() { match audio.denoise {
true => rsx!(span { class: "material-symbols-outlined", "cadence"}), true => rsx!(span { class: "material-symbols-outlined", "cadence"}),
false => rsx!(span { class: "material-symbols-outlined", "graphic_eq"}), false => rsx!(span { class: "material-symbols-outlined", "graphic_eq"}),
} }
@@ -645,9 +670,10 @@ pub fn ControlView(overrides: Resource<ProxyOverrides>) -> Element {
} }
#[component] #[component]
pub fn ServerView(overrides: Resource<ProxyOverrides>, user_config: ConfigSystem) -> Element { pub fn ServerView(overrides: Resource<ProxyOverrides>) -> Element {
let net: Coroutine<Command> = use_coroutine_handle(); let net: Coroutine<Command> = use_coroutine_handle();
let server = STATE.server.read(); let state = use_context::<SharedState>();
let server = state.server.read();
let Some(&UserState { let Some(&UserState {
deaf, deaf,
self_deaf, self_deaf,
@@ -683,7 +709,8 @@ pub fn ServerView(overrides: Resource<ProxyOverrides>, user_config: ConfigSystem
} }
#[component] #[component]
pub fn LoginView(overrides: Resource<ProxyOverrides>, user_config: ConfigSystem) -> Element { pub fn LoginView(overrides: Resource<ProxyOverrides>) -> Element {
let user_config = use_context::<ConfigSystem>();
let net: Coroutine<Command> = use_coroutine_handle(); let net: Coroutine<Command> = use_coroutine_handle();
let last_status = use_signal(|| None::<color_eyre::Result<ServerStatus>>); let last_status = use_signal(|| None::<color_eyre::Result<ServerStatus>>);
@@ -706,8 +733,11 @@ pub fn LoginView(overrides: Resource<ProxyOverrides>, user_config: ConfigSystem)
} }
}); });
let previous_username = user_config.config_get::<String>("username"); let mut username = use_signal(|| {
let mut username = use_signal(|| previous_username.unwrap_or(String::new())); user_config
.config_get::<String>("username")
.unwrap_or(String::new())
});
let do_connect = move |_| { let do_connect = move |_| {
let _ = user_config.config_set::<String>("username", &username.read()); let _ = user_config.config_set::<String>("username", &username.read());
@@ -720,7 +750,8 @@ pub fn LoginView(overrides: Resource<ProxyOverrides>, user_config: ConfigSystem)
config: overrides.read().clone().unwrap_or_default(), config: overrides.read().clone().unwrap_or_default(),
}) })
}; };
let status = &STATE.status; let state = use_context::<SharedState>();
let status = &state.status;
let bottom = match &*status.read() { let bottom = match &*status.read() {
Disconnected => rsx! { Disconnected => rsx! {
button { button {
@@ -854,10 +885,29 @@ pub fn LoginView(overrides: Resource<ProxyOverrides>, user_config: ConfigSystem)
// ) // )
} }
#[component]
pub fn app() -> Element { pub fn app() -> Element {
static STYLE: Asset = asset!("/assets/main.scss"); static STYLE: Asset = asset!("/assets/main.scss");
use_coroutine(|rx: UnboundedReceiver<Command>| super::network_entrypoint(rx)); use_effect(|| {
Platform::request_permissions();
});
let user_config = use_root_context(|| ConfigSystem::new().unwrap());
let state = use_root_context(|| {
SharedState::new(State {
status: Signal::new(Disconnected),
server: Signal::new(Default::default()),
audio: Signal::new(AudioSettings {
denoise: user_config.config_get::<bool>("denoise").unwrap_or(true),
}),
})
});
let network_state = state.clone();
use_coroutine(move |rx: UnboundedReceiver<Command>| {
super::network_entrypoint(rx, network_state.clone())
});
let overrides = use_resource(|| async move { let overrides = use_resource(|| async move {
match Platform::load_proxy_overrides().await { match Platform::load_proxy_overrides().await {
Ok(overrides) => overrides, Ok(overrides) => overrides,
@@ -865,18 +915,14 @@ pub fn app() -> Element {
} }
}); });
let user_config = ConfigSystem::new().unwrap();
Platform::request_permissions();
rsx!( 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=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: "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 } document::Link{ rel: "stylesheet", href: STYLE }
match *STATE.status.read() { match *state.status.read() {
Connected => rsx!(ServerView { overrides, user_config }), Connected => rsx!(ServerView { overrides }),
_ => rsx!(LoginView { overrides, user_config }), _ => rsx!(LoginView { overrides }),
} }
) )
} }
+3 -2
View File
@@ -1,4 +1,4 @@
use crate::app::Command; use crate::app::{Command, SharedState};
use color_eyre::eyre::{bail, Error}; use color_eyre::eyre::{bail, Error};
use dioxus::hooks::UnboundedReceiver; use dioxus::hooks::UnboundedReceiver;
use mumble_protocol::control::ClientControlCodec; use mumble_protocol::control::ClientControlCodec;
@@ -74,6 +74,7 @@ pub async fn network_connect(
username: String, username: String,
event_rx: &mut UnboundedReceiver<Command>, event_rx: &mut UnboundedReceiver<Command>,
overrides: &ProxyOverrides, overrides: &ProxyOverrides,
state: SharedState,
) -> Result<(), Error> { ) -> Result<(), Error> {
info!("connecting"); info!("connecting");
@@ -102,7 +103,7 @@ pub async fn network_connect(
let reader = asynchronous_codec::FramedRead::new(read_server.compat(), read_codec); let reader = asynchronous_codec::FramedRead::new(read_server.compat(), read_codec);
let writer = asynchronous_codec::FramedWrite::new(write_server.compat_write(), write_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<ServerStatus> { pub async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
+3 -4
View File
@@ -1,9 +1,7 @@
use crate::app::Command; use crate::app::{Command, SharedState};
use color_eyre::eyre::Error; use color_eyre::eyre::Error;
use dioxus::hooks::UnboundedReceiver; use dioxus::hooks::UnboundedReceiver;
use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs};
use mumble_web2_common::{ProxyOverrides, ServerStatus}; use mumble_web2_common::{ProxyOverrides, ServerStatus};
use std::collections::HashMap;
use std::time::Duration; use std::time::Duration;
/// Desktop platform implementation using Tokio and native audio. /// Desktop platform implementation using Tokio and native audio.
@@ -30,8 +28,9 @@ impl super::PlatformInterface for DesktopPlatform {
username: String, username: String,
event_rx: &mut UnboundedReceiver<Command>, event_rx: &mut UnboundedReceiver<Command>,
overrides: &ProxyOverrides, overrides: &ProxyOverrides,
state: SharedState,
) -> Result<(), Error> { ) -> 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<ServerStatus> { async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
+3 -2
View File
@@ -1,4 +1,4 @@
use crate::app::Command; use crate::app::{Command, SharedState};
use color_eyre::eyre::Error; use color_eyre::eyre::Error;
use dioxus::hooks::UnboundedReceiver; use dioxus::hooks::UnboundedReceiver;
use mumble_web2_common::{ProxyOverrides, ServerStatus}; use mumble_web2_common::{ProxyOverrides, ServerStatus};
@@ -24,8 +24,9 @@ impl super::PlatformInterface for MobilePlatform {
username: String, username: String,
event_rx: &mut UnboundedReceiver<Command>, event_rx: &mut UnboundedReceiver<Command>,
overrides: &ProxyOverrides, overrides: &ProxyOverrides,
state: SharedState,
) -> Result<(), Error> { ) -> 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<ServerStatus> { async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
+4 -2
View File
@@ -4,7 +4,8 @@
//! The traits make the platform boundary explicit and provide compile-time verification. //! The traits make the platform boundary explicit and provide compile-time verification.
#![allow(async_fn_in_trait)] #![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 color_eyre::eyre::Error;
use dioxus::hooks::UnboundedReceiver; use dioxus::hooks::UnboundedReceiver;
use mumble_web2_common::{ProxyOverrides, ServerStatus}; use mumble_web2_common::{ProxyOverrides, ServerStatus};
@@ -51,7 +52,7 @@ pub trait AudioPlayerInterface {
fn play_opus(&mut self, payload: &[u8]); fn play_opus(&mut self, payload: &[u8]);
} }
pub trait ConfigSystemInterface: Sized { pub trait ConfigSystemInterface: Sized + Clone {
fn new() -> Result<Self, Error>; fn new() -> Result<Self, Error>;
fn config_get<T>(&self, key: &str) -> Option<T> fn config_get<T>(&self, key: &str) -> Option<T>
@@ -82,6 +83,7 @@ pub trait PlatformInterface {
username: String, username: String,
event_rx: &mut UnboundedReceiver<Command>, event_rx: &mut UnboundedReceiver<Command>,
proxy_overrides: &ProxyOverrides, proxy_overrides: &ProxyOverrides,
state: SharedState,
) -> impl Future<Output = Result<(), Error>>; ) -> impl Future<Output = Result<(), Error>>;
/// Get server status (user count, version, etc.). /// Get server status (user count, version, etc.).
+1 -5
View File
@@ -1,10 +1,6 @@
use crate::app::Command;
use color_eyre::eyre::Error; use color_eyre::eyre::Error;
use dioxus::hooks::UnboundedReceiver;
use mumble_web2_common::ServerStatus;
use std::collections::HashMap; use std::collections::HashMap;
use std::time::Duration; use tracing::info;
use tracing::{error, info, warn};
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub struct NativeConfigSystem { pub struct NativeConfigSystem {
+3 -1
View File
@@ -1,6 +1,6 @@
/// Stub implementation of the platform interface, so that we can /// Stub implementation of the platform interface, so that we can
/// `cargo check` without any --feature flags. /// `cargo check` without any --feature flags.
use crate::effects::AudioProcessor; use crate::{app::SharedState, effects::AudioProcessor};
use color_eyre::eyre::Error; use color_eyre::eyre::Error;
use dioxus::hooks::UnboundedReceiver; use dioxus::hooks::UnboundedReceiver;
use mumble_web2_common::{ProxyOverrides, ServerStatus}; use mumble_web2_common::{ProxyOverrides, ServerStatus};
@@ -25,6 +25,7 @@ impl super::PlatformInterface for StubPlatform {
_username: String, _username: String,
_event_rx: &mut UnboundedReceiver<crate::app::Command>, _event_rx: &mut UnboundedReceiver<crate::app::Command>,
_overrides: &ProxyOverrides, _overrides: &ProxyOverrides,
_state: SharedState,
) -> impl Future<Output = Result<(), Error>> { ) -> impl Future<Output = Result<(), Error>> {
async { panic!("stubbed platform") } async { panic!("stubbed platform") }
} }
@@ -77,6 +78,7 @@ impl super::AudioPlayerInterface for StubAudioPlayer {
} }
} }
#[derive(Clone)]
pub struct StubConfigSystem; pub struct StubConfigSystem;
impl super::ConfigSystemInterface for StubConfigSystem { impl super::ConfigSystemInterface for StubConfigSystem {
+5 -3
View File
@@ -1,4 +1,4 @@
use crate::app::Command; use crate::app::{Command, SharedState};
use crate::effects::{AudioProcessor, AudioProcessorSender, TransmitState}; use crate::effects::{AudioProcessor, AudioProcessorSender, TransmitState};
use color_eyre::eyre::{bail, eyre, Error}; use color_eyre::eyre::{bail, eyre, Error};
use crossbeam::atomic::AtomicCell; use crossbeam::atomic::AtomicCell;
@@ -111,8 +111,9 @@ impl super::PlatformInterface for WebPlatform {
username: String, username: String,
event_rx: &mut UnboundedReceiver<Command>, event_rx: &mut UnboundedReceiver<Command>,
overrides: &ProxyOverrides, overrides: &ProxyOverrides,
state: SharedState,
) -> Result<(), Error> { ) -> 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<ServerStatus> { async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
@@ -434,6 +435,7 @@ pub async fn network_connect(
username: String, username: String,
event_rx: &mut UnboundedReceiver<Command>, event_rx: &mut UnboundedReceiver<Command>,
overrides: &ProxyOverrides, overrides: &ProxyOverrides,
state: SharedState,
) -> Result<(), Error> { ) -> Result<(), Error> {
info!("connecting"); info!("connecting");
@@ -492,7 +494,7 @@ pub async fn network_connect(
let writer = let writer =
asynchronous_codec::FramedWrite::new(wasm_stream_writable.into_async_write(), write_codec); 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<Url, Error> { pub fn absolute_url(path: &str) -> Result<Url, Error> {
+35 -22
View File
@@ -1,7 +1,6 @@
use app::Chat; use app::Chat;
use app::Command; use app::Command;
use app::ConnectionState; use app::ConnectionState;
use app::STATE;
use asynchronous_codec::FramedRead; use asynchronous_codec::FramedRead;
use asynchronous_codec::FramedWrite; use asynchronous_codec::FramedWrite;
use color_eyre::eyre::{bail, Error}; use color_eyre::eyre::{bail, Error};
@@ -27,6 +26,9 @@ use std::time::Duration;
use tracing::error; use tracing::error;
use tracing::info; use tracing::info;
use crate::app::AudioSettings;
use crate::app::SharedState;
use crate::app::State;
use crate::effects::AudioProcessor; use crate::effects::AudioProcessor;
use crate::imp::{ use crate::imp::{
AudioPlayer, AudioPlayerInterface as _, AudioSystem, AudioSystemInterface as _, Platform, AudioPlayer, AudioPlayerInterface as _, AudioSystem, AudioSystemInterface as _, Platform,
@@ -38,7 +40,7 @@ mod effects;
pub mod imp; pub mod imp;
mod msghtml; mod msghtml;
pub async fn network_entrypoint(mut event_rx: UnboundedReceiver<Command>) { pub async fn network_entrypoint(mut event_rx: UnboundedReceiver<Command>, state: SharedState) {
loop { loop {
let Some(Command::Connect { let Some(Command::Connect {
address, address,
@@ -49,25 +51,29 @@ pub async fn network_entrypoint(mut event_rx: UnboundedReceiver<Command>) {
panic!("did not receive connect command") panic!("did not receive connect command")
}; };
*STATE.server.write() = Default::default(); *state.server.write_unchecked() = Default::default();
*STATE.status.write() = ConnectionState::Connecting; *state.status.write_unchecked() = ConnectionState::Connecting;
if let Err(error) = 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); error!("could not connect {:?}", error);
*STATE.status.write() = ConnectionState::Failed(error.to_string()); *state.status.write_unchecked() = ConnectionState::Failed(error.to_string());
} else { } else {
*STATE.status.write() = ConnectionState::Disconnected; *state.status.write_unchecked() = ConnectionState::Disconnected;
} }
} }
} }
pub async fn network_loop<R: AsyncRead + Unpin + 'static, W: AsyncWrite + Unpin + 'static>( pub async fn network_loop<R: AsyncRead + Unpin + 'static, W: AsyncWrite + Unpin + 'static>(
username: String, username: String,
state: SharedState,
event_rx: &mut UnboundedReceiver<Command>, event_rx: &mut UnboundedReceiver<Command>,
mut reader: FramedRead<R, ControlCodec<Serverbound, Clientbound>>, mut reader: FramedRead<R, ControlCodec<Serverbound, Clientbound>>,
mut writer: FramedWrite<W, ControlCodec<Serverbound, Clientbound>>, mut writer: FramedWrite<W, ControlCodec<Serverbound, Clientbound>>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let audio_settings = state.audio.read().clone();
let (mut send_chan, mut writer_recv_chan) = futures_channel::mpsc::unbounded(); let (mut send_chan, mut writer_recv_chan) = futures_channel::mpsc::unbounded();
spawn(async move { spawn(async move {
while let Some(msg) = writer_recv_chan.next().await { while let Some(msg) = writer_recv_chan.next().await {
@@ -117,10 +123,13 @@ pub async fn network_loop<R: AsyncRead + Unpin + 'static, W: AsyncWrite + Unpin
} }
let mut audio = AudioSystem::new().await?; let mut audio = AudioSystem::new().await?;
if audio_settings.denoise {
audio.set_processor(AudioProcessor::new_denoising());
}
{ {
let send_chan = send_chan.clone(); let send_chan = send_chan.clone();
let mut sequence_num = 0; let mut sequence_num = 0;
audio.start_recording(move |opus_frame, is_terminator| { if let Err(err) = audio.start_recording(move |opus_frame, is_terminator| {
let _ = let _ =
send_chan.unbounded_send(ControlPacket::UDPTunnel(Box::new(VoicePacket::Audio { send_chan.unbounded_send(ControlPacket::UDPTunnel(Box::new(VoicePacket::Audio {
_dst: std::marker::PhantomData, _dst: std::marker::PhantomData,
@@ -131,7 +140,9 @@ pub async fn network_loop<R: AsyncRead + Unpin + 'static, W: AsyncWrite + Unpin
position_info: None, position_info: None,
}))); })));
sequence_num = sequence_num.wrapping_add(2); sequence_num = sequence_num.wrapping_add(2);
}); }) {
error!("could not begin recording: {err:?}")
}
} }
// Create map of session_id -> AudioDecoder // Create map of session_id -> AudioDecoder
@@ -149,7 +160,7 @@ pub async fn network_loop<R: AsyncRead + Unpin + 'static, W: AsyncWrite + Unpin
if !matches!(msg, ControlPacket::UDPTunnel(_) | ControlPacket::Ping(_)) { if !matches!(msg, ControlPacket::UDPTunnel(_) | ControlPacket::Ping(_)) {
info!("receiving packet {:#?}", msg); info!("receiving packet {:#?}", msg);
} }
let res = accept_packet(msg, &mut audio, &mut decoder_map); let res = accept_packet(msg, &mut audio, &mut decoder_map, &state);
if let Err(err) = res { if let Err(err) = res {
error!("error accepting packet {:?}", err) error!("error accepting packet {:?}", err)
} }
@@ -168,7 +179,7 @@ pub async fn network_loop<R: AsyncRead + Unpin + 'static, W: AsyncWrite + Unpin
match command { match command {
Some(Command::Disconnect) => break, Some(Command::Disconnect) => break,
Some(command) => { 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 { if let Err(err) = res {
info!("error accepting command {:?}", err) info!("error accepting command {:?}", err)
} }
@@ -187,9 +198,10 @@ fn accept_command(
command: Command, command: Command,
send_chan: &mut UnboundedSender<ControlPacket<mumble_protocol::Serverbound>>, send_chan: &mut UnboundedSender<ControlPacket<mumble_protocol::Serverbound>>,
audio: &mut AudioSystem, audio: &mut AudioSystem,
state: &State,
) -> Result<(), Error> { ) -> Result<(), Error> {
use Command::*; use Command::*;
let Some(session) = STATE.server.read().session else { let Some(session) = state.server.read().session else {
bail!("no session id") bail!("no session id")
}; };
@@ -212,7 +224,7 @@ fn accept_command(
}; };
{ {
let mut server = STATE.server.write(); let mut server = state.server.write_unchecked();
let Some(me) = server.session else { let Some(me) = server.session else {
bail!("not signed in with a session id") bail!("not signed in with a session id")
}; };
@@ -253,7 +265,7 @@ fn accept_command(
}; };
{ {
let mut server = STATE.server.write(); let mut server = state.server.write_unchecked();
let Some(me) = server.session else { let Some(me) = server.session else {
bail!("not signed in with a session id") bail!("not signed in with a session id")
}; };
@@ -288,7 +300,7 @@ fn accept_command(
let _ = send_chan.unbounded_send(u.into()); let _ = send_chan.unbounded_send(u.into());
} }
Connect { .. } | Disconnect => (), Connect { .. } | Disconnect => (),
UpdateMicEffects { denoise } => { UpdateAudioSettings(AudioSettings { denoise }) => {
if denoise { if denoise {
audio.set_processor(AudioProcessor::new_denoising()); audio.set_processor(AudioProcessor::new_denoising());
} else { } else {
@@ -304,6 +316,7 @@ fn accept_packet(
msg: ControlPacket<mumble_protocol::Clientbound>, msg: ControlPacket<mumble_protocol::Clientbound>,
audio_context: &mut AudioSystem, audio_context: &mut AudioSystem,
player_map: &mut HashMap<u32, AudioPlayer>, player_map: &mut HashMap<u32, AudioPlayer>,
state: &State,
) -> Result<(), Error> { ) -> Result<(), Error> {
match msg { match msg {
ControlPacket::UDPTunnel(u) => { ControlPacket::UDPTunnel(u) => {
@@ -340,15 +353,15 @@ fn accept_packet(
} }
} }
ControlPacket::ChannelState(u) => { ControlPacket::ChannelState(u) => {
let mut server = STATE.server.write(); let mut server = state.server.write_unchecked();
server.channels_state.update_from_channel_state(&u); server.channels_state.update_from_channel_state(&u);
} }
ControlPacket::ChannelRemove(u) => { ControlPacket::ChannelRemove(u) => {
let mut server = STATE.server.write(); let mut server = state.server.write_unchecked();
server.channels_state.update_from_channel_remove(&u); server.channels_state.update_from_channel_remove(&u);
} }
ControlPacket::UserState(u) => { ControlPacket::UserState(u) => {
let mut server = STATE.server.write(); let mut server = state.server.write_unchecked();
let server = &mut *server; let server = &mut *server;
let id = u.get_session(); let id = u.get_session();
@@ -392,7 +405,7 @@ fn accept_packet(
} }
} }
ControlPacket::UserRemove(u) => { ControlPacket::UserRemove(u) => {
let mut server = STATE.server.write(); let mut server = state.server.write_unchecked();
let id = u.get_session(); let id = u.get_session();
if let Some(state) = server.users.remove(&id) { if let Some(state) = server.users.remove(&id) {
if let Some(parent) = server.channels_state.channels.get_mut(&state.channel) { if let Some(parent) = server.channels_state.channels.get_mut(&state.channel) {
@@ -401,7 +414,7 @@ fn accept_packet(
} }
} }
ControlPacket::TextMessage(u) => { ControlPacket::TextMessage(u) => {
let mut server = STATE.server.write(); let mut server = state.server.write_unchecked();
if u.has_message() { if u.has_message() {
let text = u.get_message().to_string(); let text = u.get_message().to_string();
server.chat.push(Chat { server.chat.push(Chat {
@@ -416,8 +429,8 @@ fn accept_packet(
} }
} }
ControlPacket::ServerSync(u) => { ControlPacket::ServerSync(u) => {
*STATE.status.write() = ConnectionState::Connected; *state.status.write_unchecked() = ConnectionState::Connected;
let mut server = STATE.server.write(); let mut server = state.server.write_unchecked();
if u.has_welcome_text() { if u.has_welcome_text() {
let text = u.get_welcome_text().to_string(); let text = u.get_welcome_text().to_string();
server.chat.push(Chat { server.chat.push(Chat {
+17 -1
View File
@@ -1,6 +1,22 @@
use dioxus::prelude::*;
use mumble_web2_gui::{app, imp::Platform, imp::PlatformInterface as _}; use mumble_web2_gui::{app, imp::Platform, imp::PlatformInterface as _};
pub fn main() { pub fn main() {
Platform::init_logging(); Platform::init_logging();
dioxus::launch(app::app); dioxus::LaunchBuilder::new()
.with_cfg(desktop! {
dioxus::desktop::Config::new()
// Reduce white flash on startup by setting background color and hiding main element
.with_background_color((0, 0, 0, 255))
.with_custom_head("<style>html, body { background: black; } #main { visibility: hidden; }</style>".into())
.with_disable_context_menu(cfg!(not(debug_assertions)))
.with_window(
dioxus::desktop::WindowBuilder::new()
.with_title("Mumble Web 2")
.with_min_inner_size(dioxus::desktop::LogicalSize::new(600.0, 300.0))
.with_inner_size(dioxus::desktop::LogicalSize::new(900.0, 700.0))
.with_maximized(false),
)
})
.launch(app::app);
} }