chat_ui_components/
chat_app.rs

1use dioxus::prelude::*;
2use chat_core::coordinator::{ChatCoordinatorHandle, CoordinatorEvent};
3use chat_core::history::types::{ChatMessage, ConnectionStats};
4use crate::components::{ChatLayout, RoomPrompt, TabLocked};
5
6/// All common application signals used by both web and desktop apps.
7#[derive(Clone, Copy, PartialEq)]
8pub struct ChatAppSignals {
9    pub tab_lock_acquired: Signal<bool>,
10    pub room_name: Signal<Option<String>>,
11    pub messages: Signal<Vec<ChatMessage>>,
12    pub nickname: Signal<String>,
13    pub is_online: Signal<bool>,
14    pub stats: Signal<Option<Box<ConnectionStats>>>,
15    pub system_messages: Signal<Vec<String>>,
16    pub error_messages: Signal<Vec<String>>,
17    pub show_stats: Signal<bool>,
18    pub sidebar_open: Signal<bool>,
19    pub sidebar_collapsed: Signal<bool>,
20    pub coordinator_handle: Signal<Option<ChatCoordinatorHandle>>,
21}
22
23/// Create the common application signals.
24pub fn use_chat_app_signals() -> ChatAppSignals {
25    ChatAppSignals {
26        tab_lock_acquired: use_signal(|| true),
27        room_name: use_signal(|| None::<String>),
28        messages: use_signal(Vec::new),
29        nickname: use_signal(|| "anon".to_string()),
30        is_online: use_signal(|| false),
31        stats: use_signal(|| None::<Box<ConnectionStats>>),
32        system_messages: use_signal(Vec::<String>::new),
33        error_messages: use_signal(Vec::<String>::new),
34        show_stats: use_signal(|| false),
35        sidebar_open: use_signal(|| false),
36        sidebar_collapsed: use_signal(|| false),
37        coordinator_handle: use_signal(|| None::<ChatCoordinatorHandle>),
38    }
39}
40
41/// Handle a single coordinator event, updating the provided signals.
42pub fn handle_chat_event(event: CoordinatorEvent, mut signals: ChatAppSignals) {
43    match event {
44        CoordinatorEvent::MessagesUpdated(msgs) => {
45            signals.messages.set(msgs);
46        }
47        CoordinatorEvent::PeerConnected { .. } => {}
48        CoordinatorEvent::PeerDisconnected { .. } => {}
49        CoordinatorEvent::StatsUpdated(s) => {
50            signals.stats.set(Some(s));
51        }
52        CoordinatorEvent::SystemMessage(msg) => {
53            let mut current = signals.system_messages.read().clone();
54            current.push(msg);
55            signals.system_messages.set(current);
56        }
57        CoordinatorEvent::ErrorMessage(msg) => {
58            let mut current = signals.error_messages.read().clone();
59            current.push(msg);
60            signals.error_messages.set(current);
61        }
62        CoordinatorEvent::RoomJoined { room_name: name } => {
63            signals.room_name.set(Some(name));
64        }
65        CoordinatorEvent::NicknameChanged { nickname: nick } => {
66            signals.nickname.set(nick);
67        }
68        CoordinatorEvent::IsOnline(online) => {
69            signals.is_online.set(online);
70        }
71    }
72}
73
74/// Run the shared coordinator event loop, updating the provided signals.
75pub async fn run_chat_event_loop(
76    mut events: async_broadcast::Receiver<CoordinatorEvent>,
77    signals: ChatAppSignals,
78) {
79    while let Ok(event) = events.recv().await {
80        handle_chat_event(event, signals);
81    }
82}
83
84/// Shared application shell component used by both web and desktop apps.
85/// Renders the tab-lock screen, room prompt, or the main chat layout.
86#[component]
87pub fn ChatApp(
88    signals: ChatAppSignals,
89    on_room_left: Option<EventHandler<()>>,
90) -> Element {
91    let ChatAppSignals {
92        tab_lock_acquired,
93        mut room_name,
94        messages,
95        nickname,
96        is_online,
97        stats,
98        system_messages,
99        error_messages,
100        mut show_stats,
101        mut sidebar_open,
102        mut sidebar_collapsed,
103        coordinator_handle,
104    } = signals;
105
106    rsx! {
107        if !tab_lock_acquired() {
108            TabLocked {}
109        } else if room_name().is_none() {
110            RoomPrompt {
111                on_join: move |name: String| {
112                    if let Some(handle) = coordinator_handle.read().as_ref() {
113                        let handle = handle.clone();
114                        spawn(async move {
115                            handle.join_room(name).await;
116                        });
117                    }
118                }
119            }
120        } else {
121            ChatLayout {
122                room_name: room_name().unwrap_or_default(),
123                nickname: nickname(),
124                messages: messages(),
125                is_online: is_online(),
126                stats: stats(),
127                system_messages: system_messages(),
128                error_messages: error_messages(),
129                show_stats: show_stats(),
130                sidebar_open: sidebar_open(),
131                sidebar_collapsed: sidebar_collapsed(),
132                on_send_message: move |content: String| {
133                    if let Some(handle) = coordinator_handle.read().as_ref() {
134                        let handle = handle.clone();
135                        spawn(async move {
136                            handle.send_message(content).await;
137                        });
138                    }
139                },
140                on_set_nickname: move |nick: String| {
141                    if let Some(handle) = coordinator_handle.read().as_ref() {
142                        let handle = handle.clone();
143                        spawn(async move {
144                            handle.set_nickname(nick).await;
145                        });
146                    }
147                },
148                on_dial_peer: move |addr: String| {
149                    if let Some(handle) = coordinator_handle.read().as_ref() {
150                        let handle = handle.clone();
151                        spawn(async move {
152                            handle.dial_peer(addr).await;
153                        });
154                    }
155                },
156                on_leave_room: move || {
157                    if let Some(handle) = coordinator_handle.read().as_ref() {
158                        let handle = handle.clone();
159                        spawn(async move {
160                            handle.leave_room().await;
161                        });
162                    }
163                    room_name.set(None);
164                    if let Some(cb) = on_room_left {
165                        cb.call(());
166                    }
167                },
168                on_toggle_stats: move || {
169                    show_stats.set(!show_stats());
170                },
171                on_toggle_sidebar: move || {
172                    sidebar_open.set(!sidebar_open());
173                    sidebar_collapsed.set(!sidebar_collapsed());
174                },
175                on_close_sidebar: move || {
176                    sidebar_open.set(false);
177                    sidebar_collapsed.set(true);
178                },
179            }
180        }
181    }
182}