chat_ui_components/components/
chat_layout.rs

1use dioxus::prelude::*;
2use chat_core::history::types::{ChatMessage, ConnectionStats};
3
4use super::{MessageItem, PeerList, StatsPanel};
5
6#[component]
7pub fn ChatLayout(
8    room_name: String,
9    nickname: String,
10    messages: Vec<ChatMessage>,
11    is_online: bool,
12    stats: Option<Box<ConnectionStats>>,
13    system_messages: Vec<String>,
14    error_messages: Vec<String>,
15    show_stats: bool,
16    sidebar_open: bool,
17    sidebar_collapsed: bool,
18    on_send_message: EventHandler<String>,
19    on_set_nickname: EventHandler<String>,
20    on_dial_peer: EventHandler<String>,
21    on_leave_room: EventHandler<()>,
22    on_toggle_stats: EventHandler<()>,
23    on_toggle_sidebar: EventHandler<()>,
24    on_close_sidebar: EventHandler<()>,
25) -> Element {
26    let mut message_input = use_signal(|| "".to_string());
27    let mut nickname_input = use_signal(|| nickname.clone());
28    let mut should_auto_scroll = use_signal(|| true);
29    let mut last_scrolled_id = use_signal(|| String::new());
30    let stats_for_effect = stats.clone();
31
32    let mut send = move || {
33        let content = message_input.read().trim().to_string();
34        if !content.is_empty() {
35            on_send_message.call(content);
36            message_input.set("".to_string());
37            should_auto_scroll.set(true);
38        }
39    };
40
41    use_effect(use_reactive((&messages,), move |(messages,)| {
42        if messages.is_empty() {
43            return;
44        }
45        let last_id = messages.last().map(|m| m.id.clone()).unwrap_or_default();
46        if last_id == last_scrolled_id() {
47            return;
48        }
49        let local_peer_id = stats_for_effect.as_ref().map(|s| s.peer_id.clone()).unwrap_or_default();
50        let is_local = messages.last().map(|m| m.peer_id == local_peer_id).unwrap_or(false);
51        if is_local || should_auto_scroll() {
52            last_scrolled_id.set(last_id);
53            spawn(async move {
54                let _ = document::eval(r#"
55                    let el = document.getElementById("messages");
56                    if (el) el.scrollTop = el.scrollHeight;
57                "#).await;
58            });
59        }
60    }));
61
62    let status_text = if is_online { "● online" } else { "● offline" };
63    let status_class = if is_online { "status-badge online" } else { "status-badge offline" };
64
65    let peer_count = stats.as_ref().map(|s| s.connected_peers.len()).unwrap_or(0);
66    let conn_count = stats.as_ref().map(|s| s.direct_peers + s.bootstrap_connections).unwrap_or(0);
67
68    let sidebar_class = {
69        let mut c = "sidebar".to_string();
70        if sidebar_open { c.push_str(" open"); }
71        if sidebar_collapsed { c.push_str(" collapsed"); }
72        c
73    };
74
75    rsx! {
76        div {
77            id: "app",
78
79            // Main content
80            div {
81                class: "main-content",
82
83                // Header
84                div {
85                    class: "chat-header",
86                    div {
87                        class: "logo",
88                        span { class: "logo-icon", "🔥" }
89                        span { class: "logo-text", "P2Pandemonium" }
90                    }
91                    div {
92                        class: "header-stats",
93                        div {
94                            class: "stat-badge",
95                            "Room: {room_name}"
96                        }
97                        div {
98                            class: "stat-badge",
99                            "Peers: {peer_count} ({conn_count} conns)"
100                        }
101                        div {
102                            class: status_class,
103                            "{status_text}"
104                        }
105                        button {
106                            class: "sidebar-toggle",
107                            onclick: move |_| on_toggle_sidebar.call(()),
108                            "👥"
109                        }
110                    }
111                }
112
113                // Stats panel (overlay)
114                if show_stats {
115                    StatsPanel {
116                        stats: stats.clone(),
117                        local_peer_id: stats.as_ref().map(|s| s.peer_id.clone()).unwrap_or_default(),
118                        on_close: move || on_toggle_stats.call(()),
119                        on_dial_peer,
120                    }
121                }
122
123                // Messages area
124                div {
125                    class: "chat-container",
126                    div {
127                        class: "messages-list",
128                        id: "messages",
129                        onscroll: move |evt| {
130                            let data = evt.data();
131                            let scroll_top = data.scroll_top();
132                            let client_height = data.client_height();
133                            let scroll_height = data.scroll_height();
134                            let at_bottom = (scroll_top + client_height as f64) >= (scroll_height as f64 - 50.0);
135                            should_auto_scroll.set(at_bottom);
136                        },
137
138                        // System messages
139                        for (idx, msg) in system_messages.iter().enumerate() {
140                            div {
141                                key: "sys-{idx}",
142                                class: "system-message",
143                                "{msg}"
144                            }
145                        }
146
147                        // Error messages
148                        for (idx, msg) in error_messages.iter().enumerate() {
149                            div {
150                                key: "err-{idx}",
151                                class: "error-message",
152                                "{msg}"
153                            }
154                        }
155
156                        // Chat messages
157                        for msg in messages {
158                            MessageItem {
159                                key: "{msg.id}",
160                                msg: msg.clone(),
161                                is_local: msg.peer_id == stats.as_ref().map(|s| s.peer_id.clone()).unwrap_or_default(),
162                            }
163                        }
164                    }
165                }
166
167                // Footer / Input area
168                div {
169                    class: "chat-footer",
170
171                    // Input row (nickname + stats button)
172                    div {
173                        class: "input-row",
174                        div {
175                            class: "nickname-section",
176                            label { "Nick:" }
177                            input {
178                                id: "nickname",
179                                value: "{nickname_input}",
180                                oninput: move |evt| nickname_input.set(evt.value().clone()),
181                                onchange: move |_| {
182                                    let nick = nickname_input.read().trim().to_string();
183                                    if !nick.is_empty() {
184                                        on_set_nickname.call(nick);
185                                    }
186                                }
187                            }
188                        }
189                        button {
190                            class: "stats-btn",
191                            onclick: move |_| on_toggle_stats.call(()),
192                            "📊 Stats"
193                        }
194                    }
195
196                    // Message input row
197                    div {
198                        class: "message-input-section",
199                        input {
200                            id: "message-input",
201                            value: "{message_input}",
202                            placeholder: "Type a message...",
203                            disabled: !is_online,
204                            oninput: move |evt| message_input.set(evt.value().clone()),
205                            onkeypress: move |evt| {
206                                if evt.key().to_string() == "Enter" {
207                                    send();
208                                }
209                            }
210                        }
211                        button {
212                            id: "send-btn",
213                            class: "btn-primary",
214                            disabled: !is_online || message_input.read().trim().is_empty(),
215                            onclick: move |_| send(),
216                            "Send"
217                        }
218                    }
219                }
220            }
221
222            // Sidebar (right)
223            div {
224                class: "{sidebar_class}",
225                div {
226                    class: "sidebar-header",
227                    h3 { "Peers" }
228                    button {
229                        class: "close-btn",
230                        onclick: move |_| on_close_sidebar.call(()),
231                        "×"
232                    }
233                }
234                div {
235                    class: "peer-list",
236                    PeerList { stats: stats.clone(), sidebar_open: sidebar_open }
237
238                    // Leave room button
239                    div {
240                        style: "padding: 1rem; border-top: 1px solid var(--border-color); margin-top: auto;",
241                        button {
242                            class: "leave-btn",
243                            onclick: move |_| on_leave_room.call(()),
244                            "Leave Room"
245                        }
246                    }
247                }
248            }
249
250            // Mobile backdrop
251            if sidebar_open {
252                div {
253                    class: "sidebar-backdrop",
254                    onclick: move |_| on_close_sidebar.call(()),
255                }
256            }
257        }
258    }
259}