#![allow(non_snake_case)] use dioxus::prelude::*; use ordermap::OrderSet; use sir::{css, global_css}; use std::collections::HashMap; use crate::imp; pub type ChannelId = u32; pub type UserId = u32; pub enum ConnectionState { Disconnected, Connecting, Connected, Failed(String), } #[derive(Debug)] pub enum Command { Connect { address: String, username: String, }, SendChat { markdown: String, channels: Vec, }, SetMute { mute: bool, }, SetDeaf { deaf: bool, }, EnterChannel { channel: ChannelId, user: UserId, }, Disconnect, } use Command::*; use ConnectionState::*; #[derive(Default)] pub struct ChannelState { pub name: String, pub children: OrderSet, pub users: OrderSet, pub parent: Option, } #[derive(Default)] pub struct UserState { pub name: String, pub channel: ChannelId, pub deaf: bool, pub mute: bool, pub self_deaf: bool, pub self_mute: bool, } pub struct Chat { pub raw: String, pub dangerous_html: String, pub sender: Option, } #[derive(Default)] pub struct ServerState { pub channels: HashMap, pub users: HashMap, pub chat: Vec, pub session: Option, } impl ServerState { pub fn this_user(&self) -> Option<&UserState> { self.users.get(&self.session?) } } pub struct State { pub status: GlobalSignal, pub server: GlobalSignal, } pub static STATE: State = State { status: Signal::global(|| Disconnected), server: Signal::global(|| Default::default()), }; #[component] pub fn UserPill(name: String) -> Element { let pill = css!( " border: solid 1px black; border-radius: 8px; padding: 4px; width: fit-content; " ); rsx!( div { class: "{pill}", "{name}" } ) } #[component] pub fn User(id: UserId) -> Element { let server = STATE.server.read(); let state = server.users.get(&id)?; rsx!(UserPill { name: state.name.clone() }) } #[component] pub fn Channel(id: ChannelId) -> Element { let net: Coroutine = use_coroutine_handle(); let server = STATE.server.read(); let user = server.session?; let state = server.channels.get(&id)?; let channel_details = css!( " flex: 0 0 100%; " ); let channel_children = css!( " border-left: solid black 1px; display: flex; flex-direction: row; flex-wrap: wrap; gap: 8px; margin-left: 5px; padding-left: 11px; " ); rsx!( details { class: "{channel_details}", open: true, summary { ondoubleclick: move |_| net.send(EnterChannel { channel: id, user }), "{state.name}" } div { class: "{channel_children}", for id in state.users.iter() { User { id: *id } } for child in state.children.iter() { Channel { id: *child } } } } ) } #[component] pub fn ChatView() -> Element { let net: Coroutine = use_coroutine_handle(); let server = STATE.server.read(); let mut draft = use_signal(|| "".to_string()); let chat_history = css!( " overflow-y: auto; flex: 1 0 0; " ); let chat_message = css!( " display: flex; flex-direction: row; margin: 16px; gap: 8px; align-items: center; " ); let chat_box = css!( " display: flex; flex-direction: row; padding: 16px; gap: 8px; border-top: solid black 1px; input { flex-grow: 1; padding: 8px; } " ); let mut do_send = move || { if let Some(user) = STATE.server.read().this_user() { net.send(SendChat { markdown: draft.write().split_off(0), channels: vec![user.channel], }); } }; rsx!( div { class: "{chat_history}", for chat in server.chat.iter() { div { class: "{chat_message}", if let Some(sender) = chat.sender.and_then(|u| server.users.get(&u)) { UserPill { name: sender.name.clone() } } span { dangerous_inner_html: "{chat.dangerous_html}", } } } } div { class: "{chat_box}", input { placeholder: "say something", value: "{draft.read()}", oninput: move |evt| draft.set(evt.value().clone()), onkeypress: move |evt: Event| { if evt.code() == Code::Enter && evt.modifiers().is_empty() { do_send(); } } } button { onclick: move |_| do_send(), "Send" } } ) } #[component] pub fn ServerView() -> Element { let net: Coroutine = use_coroutine_handle(); let server = STATE.server.read(); let &UserState { deaf, self_deaf, mute, self_mute, .. } = server.this_user()?; let grid = css!( r#" display: grid; height: 100%; background-color: white; grid-template-rows: auto 1fr; grid-template-columns: 1fr 1fr; grid-template-areas: "bar bar" "tree chat"; @media screen and (max-width: 720px) { grid-template-rows: auto 1fr 1fr; grid-template-columns: 1fr; grid-template-areas: "bar" "tree" "chat"; } gap: 4px; padding: 4px; "# ); let channel_box = css!( " padding: 16px; border: solid black 1px; overflow: auto; grid-area: tree; " ); let chat_box = css!( " border: solid black 1px; display: flex; flex-direction: column; grid-area: chat; " ); let top_bar = css!( " padding: 16px; border: solid black 1px; grid-area: bar; display: flex; flex-direction: row; gap: 16px; align-items: center; button { padding: 8px; } " ); rsx!( div { class: "{grid}", div { class: "{top_bar}", button { onclick: move |_| net.send(Disconnect), "Disconnect" } span { input { r#type: "checkbox", id: "mute", checked: mute || self_mute, disabled: mute, onchange: move |_| net.send(SetMute { mute: !self_mute }), } label { r#for: "mute", "Mute" } } span { input { r#type: "checkbox", id: "deaf", checked: deaf || self_deaf, disabled: deaf, onchange: move |_| net.send(SetDeaf { deaf: !self_deaf }), } label { r#for: "deaf", "Deafen" } } } div { class: "{channel_box}", for (id, state) in server.channels.iter() { if state.parent.is_none() { Channel { id: *id } } } } div { class: "{chat_box}", ChatView {} } } ) } #[component] pub fn LoginView() -> Element { let net: Coroutine = use_coroutine_handle(); let default_address = option_env!("MUMBLEWEB2_WEBTRANSPORT_SERVER_ADDRESS").unwrap_or(""); let mut address = use_signal(|| default_address.to_string()); let previous_username = imp::load_username(); let mut username = use_signal(|| previous_username.unwrap_or(String::new())); let error = css!( " color: red; pre { color: black; } " ); let login_box = css!( " max-width: 50vw; align-self: center; padding: 16px; background-color: white; display: flex; flex-direction: column; gap: 16px; input,button { padding: 8px; } " ); let do_connect = move |_| { //let _ = set_default_username(&username.read()); let _ = imp::set_default_username(&username.read()); net.send(Connect { address: address.read().clone(), username: username.read().clone(), }) }; let status = &STATE.status; let bottom = match &*status.read() { Disconnected => rsx! { button { onclick: do_connect.clone(), "Connect!" } }, Connecting => rsx! { "Connecting..." }, Failed(msg) => rsx!( button { onclick: do_connect.clone(), "Reconnect!" } div { class: "{error}", "Failed to connect:" pre { "{msg}" } } ), Connected => unreachable!(), }; rsx!( div { class: "{login_box}", input { placeholder: "username", value: "{username.read()}", autofocus: "true", oninput: move |evt| username.set(evt.value().clone()), } input { placeholder: "server address", value: "{address.read()}", autofocus: "true", oninput: move |evt| address.set(evt.value().clone()), } {bottom} } ) } pub fn app() -> Element { use_coroutine(|rx: UnboundedReceiver| super::network_entrypoint(rx)); global_css!( " body { margin: 0; } #main { height: 100vh; display: flex; flex-direction: column; justify-content: space-around; background-color: grey; overflow: auto; } " ); rsx!( sir::AppStyle { } match *STATE.status.read() { Connected => rsx!(ServerView {}), _ => rsx!(LoginView {}), } ) }