From 306d3c04cdc78de080d4610cd4787ba0cb7d3ec1 Mon Sep 17 00:00:00 2001 From: Noah Date: Thu, 26 Mar 2026 03:28:32 -0700 Subject: [PATCH 01/18] feat: add video/call page state, web-sys features, and icons Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/web/Cargo.toml | 4 +++ crates/web/src/app.rs | 3 +- crates/web/src/icons.rs | 50 +++++++++++++++++++++++++++++++++ crates/web/src/state.rs | 61 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 115 insertions(+), 3 deletions(-) diff --git a/crates/web/Cargo.toml b/crates/web/Cargo.toml index 8a19dac6..180de61e 100644 --- a/crates/web/Cargo.toml +++ b/crates/web/Cargo.toml @@ -25,6 +25,10 @@ web-sys = { version = "0.3", features = [ "RtcRtpSender", "MediaStream", "MediaStreamTrack", "MediaStreamConstraints", "MediaDevices", "HtmlMediaElement", "ScrollIntoViewOptions", "ScrollBehavior", "ScrollLogicalPosition", + "DisplayMediaStreamConstraints", + "AudioContext", "BaseAudioContext", "AudioNode", + "AnalyserNode", "MediaStreamAudioSourceNode", + "RtcSignalingState", "RtcRtpTransceiver", ] } js-sys = "0.3" wasm-bindgen-futures = "0.4" diff --git a/crates/web/src/app.rs b/crates/web/src/app.rs index 3fc8b3ae..70ded77b 100644 --- a/crates/web/src/app.rs +++ b/crates/web/src/app.rs @@ -423,8 +423,7 @@ pub fn App() -> impl IntoView { }.into_any() } else if show_settings.get() { - let tab = app_state.ui.settings_tab.get_untracked(); - view! { }.into_any() } else { diff --git a/crates/web/src/icons.rs b/crates/web/src/icons.rs index 9c8efad3..5708f0e0 100644 --- a/crates/web/src/icons.rs +++ b/crates/web/src/icons.rs @@ -298,3 +298,53 @@ pub fn icon_copy() -> impl IntoView { "icon-copy", ) } + +/// Monitor / screen icon (rectangle with stand). +pub fn icon_monitor() -> impl IntoView { + icon( + &format!( + r#""# + ), + "icon-monitor", + ) +} + +/// Video camera icon. +pub fn icon_video() -> impl IntoView { + icon( + &format!( + r#""# + ), + "icon-video", + ) +} + +/// Video camera off icon (with diagonal slash). +pub fn icon_video_off() -> impl IntoView { + icon( + &format!( + r#""# + ), + "icon-video-off", + ) +} + +/// 2x2 grid icon. +pub fn icon_grid() -> impl IntoView { + icon( + &format!( + r#""# + ), + "icon-grid", + ) +} + +/// Maximize / expand icon (corner arrows). +pub fn icon_maximize() -> impl IntoView { + icon( + &format!( + r#""# + ), + "icon-maximize", + ) +} diff --git a/crates/web/src/state.rs b/crates/web/src/state.rs index 3e44ee92..acf138f6 100644 --- a/crates/web/src/state.rs +++ b/crates/web/src/state.rs @@ -1,8 +1,28 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use leptos::prelude::*; use willow_client::DisplayMessage; +#[derive(Clone, Copy, PartialEq)] +pub enum VideoSource { + Camera, + Screen, +} + +#[derive(Clone, PartialEq, Default)] +pub enum CallLayout { + #[default] + Grid, + Focus(String), // focused peer_id +} + +#[derive(Clone, Copy, PartialEq, Default)] +pub enum SettingsTab { + #[default] + Profile, + Server, +} + /// Per-channel UI state. Extensible for future needs (drafts, scroll pos). #[derive(Clone, Default, PartialEq)] pub struct ChannelViewState { @@ -59,6 +79,10 @@ pub struct UiState { pub show_members: ReadSignal, pub show_add_server: ReadSignal, pub show_pinned: ReadSignal, + pub show_call_page: ReadSignal, + pub show_palette: ReadSignal, + pub call_layout: ReadSignal, + pub settings_tab: ReadSignal, } #[derive(Clone, Copy)] @@ -70,6 +94,10 @@ pub struct VoiceState { #[allow(dead_code)] pub voice_participants_map: ReadSignal>>, pub voice_channel_name: ReadSignal, + pub video_source: ReadSignal>, + pub speaking_peers: ReadSignal>, + pub remote_video_streams: + ReadSignal>>, } // ── Write signals (NOT in context — held by event processing) ──────── @@ -122,6 +150,10 @@ pub struct UiWriteSignals { pub set_show_members: WriteSignal, pub set_show_add_server: WriteSignal, pub set_show_pinned: WriteSignal, + pub set_show_call_page: WriteSignal, + pub set_show_palette: WriteSignal, + pub set_call_layout: WriteSignal, + pub set_settings_tab: WriteSignal, } #[derive(Clone, Copy)] @@ -131,6 +163,10 @@ pub struct VoiceWriteSignals { pub set_voice_deafened: WriteSignal, pub set_voice_participants_map: WriteSignal>>, pub set_voice_channel_name: WriteSignal, + pub set_video_source: WriteSignal>, + pub set_speaking_peers: WriteSignal>, + pub set_remote_video_streams: + WriteSignal>>, } /// Create all signal pairs and return the read/write halves. @@ -167,6 +203,10 @@ pub fn create_signals() -> (AppState, AppWriteSignals) { let (show_members, set_show_members) = signal(false); let (show_add_server, set_show_add_server) = signal(false); let (show_pinned, set_show_pinned) = signal(false); + let (show_call_page, set_show_call_page) = signal(false); + let (show_palette, set_show_palette) = signal(false); + let (call_layout, set_call_layout) = signal(CallLayout::default()); + let (settings_tab, set_settings_tab) = signal(SettingsTab::default()); // Voice signals let (voice_channel, set_voice_channel) = signal(Option::::None); @@ -175,6 +215,11 @@ pub fn create_signals() -> (AppState, AppWriteSignals) { let (voice_participants_map, set_voice_participants_map) = signal(HashMap::>::new()); let (voice_channel_name, set_voice_channel_name) = signal(String::new()); + let (video_source, set_video_source) = signal(Option::::None); + let (speaking_peers, set_speaking_peers) = signal(HashSet::::new()); + let (remote_video_streams, set_remote_video_streams) = signal( + HashMap::>::new(), + ); let app_state = AppState { chat: ChatState { @@ -209,6 +254,10 @@ pub fn create_signals() -> (AppState, AppWriteSignals) { show_members, show_add_server, show_pinned, + show_call_page, + show_palette, + call_layout, + settings_tab, }, voice: VoiceState { voice_channel, @@ -216,6 +265,9 @@ pub fn create_signals() -> (AppState, AppWriteSignals) { voice_deafened, voice_participants_map, voice_channel_name, + video_source, + speaking_peers, + remote_video_streams, }, }; @@ -252,6 +304,10 @@ pub fn create_signals() -> (AppState, AppWriteSignals) { set_show_members, set_show_add_server, set_show_pinned, + set_show_call_page, + set_show_palette, + set_call_layout, + set_settings_tab, }, voice: VoiceWriteSignals { set_voice_channel, @@ -259,6 +315,9 @@ pub fn create_signals() -> (AppState, AppWriteSignals) { set_voice_deafened, set_voice_participants_map, set_voice_channel_name, + set_video_source, + set_speaking_peers, + set_remote_video_streams, }, }; From d44fd2ebb8008e3f5f003e083a3a5d596d5ccb8f Mon Sep 17 00:00:00 2001 From: Noah Date: Thu, 26 Mar 2026 03:32:41 -0700 Subject: [PATCH 02/18] refactor: VoiceManager connection reuse, perfect negotiation, video track management - Connections reused on renegotiation instead of recreated - Perfect negotiation: polite/impolite by peer ID comparison - onnegotiationneeded handler for addTrack/removeTrack flows - Unified video management: start_video/stop_video_share - ontrack redesign: routes audio to