#![allow(non_snake_case)] use dioxus::prelude::*; use ordermap::OrderSet; use sir::{css, global_css}; use std::collections::{BTreeMap, BTreeSet, HashMap}; 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, hash: String, }, SendChat { markdown: String, channels: Vec, }, 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 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 server = STATE.server.read(); 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 { "{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 grid = css!( " display: grid; grid-template-rows: auto 1fr; grid-template-columns: 1fr 1fr; height: 100%; background-color: white; gap: 4px; padding: 4px; " ); let channel_box = css!( " padding: 16px; border: solid black 1px; grid-row: 2; overflow: auto; " ); let chat_box = css!( " border: solid black 1px; grid-row: 2; display: flex; flex-direction: column; " ); let top_bar = css!( " padding: 16px; border: solid black 1px; grid-row: 1; grid-column: span 2; button { padding: 8px; } " ); rsx!( div { class: "{grid}", div { class: "{top_bar}", button { onclick: move |_| net.send(Disconnect), "Disconnect" } } 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 mut username = use_signal(|| "".to_string()); let default_address = option_env!("MUMBLEWEB2_WEBTRANSPORT_SERVER_ADDRESS").unwrap_or(""); let mut address = use_signal(|| default_address.to_string()); let error = css!( " color: red; pre { color: black; } " ); let login_box = css!( " max-width: 50vw; min-width: 640px; align-self: center; padding: 16px; background-color: white; display: flex; flex-direction: column; gap: 16px; input,button { padding: 8px; } " ); let do_connect = move |_| { net.send(Connect{address: address.read().clone(), username: username.read().clone(), hash: "[39, 96, 204, 127, 26, 59, 35, 209, 197, 103, 192, 6, 3, 98, 203, 228, 124, 46, 247, 72, 44, 224, 123, 238, 218, 140, 128, 100, 115, 14, 23, 233]".to_string()}) }; 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 {}), } ) }