chat_web/
main.rs

1use dioxus::prelude::*;
2use chat_core::history::types::ChatMessage;
3use chat_core::network::NetworkNode;
4
5#[cfg(feature = "backend-libp2p")]
6use network_libp2p::NodeOptions;
7
8use wasm_bindgen::prelude::*;
9use wasm_bindgen::closure::Closure;
10
11use chat_ui_components::{ChatApp, ChatAppSignals, use_chat_app_signals, transport_utils::transport_name_to_info};
12use std::cell::RefCell;
13use std::rc::Rc;
14
15const CSS: &str = include_str!("../public/styles.css");
16
17fn main() {
18    #[cfg(target_arch = "wasm32")]
19    {
20        use tracing_subscriber::layer::SubscriberExt;
21        use tracing_subscriber::util::SubscriberInitExt;
22        use tracing_subscriber::{filter::EnvFilter, Registry};
23        use tracing_wasm::{WASMLayer, WASMLayerConfigBuilder};
24
25        let filter = EnvFilter::new("info,libp2p=warn,chat_core=info,network_libp2p=info,libp2p_webrtc_websys=info,libp2p_webtransport_websys=error");
26
27        let layer_config = WASMLayerConfigBuilder::new()
28            .set_max_level(tracing::Level::TRACE)
29            .build();
30        let layer = WASMLayer::new(layer_config);
31
32        Registry::default()
33            .with(layer)
34            .with(filter)
35            .init();
36    }
37
38    dioxus::launch(App);
39}
40
41#[component]
42fn App() -> Element {
43    let signals = use_chat_app_signals();
44    let ChatAppSignals {
45        mut tab_lock_acquired,
46        mut room_name,
47        mut coordinator_handle,
48        mut error_messages,
49        mut messages,
50        mut stats,
51        ..
52    } = signals;
53
54    // Spawn coordinator and event listener
55    use_hook(|| {
56        spawn(async move {
57            let bootstrap_peers: Vec<String> = web_sys::window()
58                .and_then(|w| w.location().search().ok())
59                .and_then(|search| {
60                    let params = web_sys::UrlSearchParams::new_with_str(&search).ok()?;
61                    let mut peers = Vec::new();
62                    // Support multiple ?bootstrap=addr params
63                    for i in 0..10 {
64                        if let Some(addr) = params.get(&format!("bootstrap{}", i))
65                            && !addr.is_empty()
66                        {
67                            peers.push(addr);
68                        }
69                    }
70                    // Also support comma-separated ?bootstrap=addr1,addr2
71                    if let Some(raw) = params.get("bootstrap") {
72                        for part in raw.split(',') {
73                            let trimmed = part.trim();
74                            if !trimmed.is_empty() { peers.push(trimmed.to_string()); }
75                        }
76                    }
77                    Some(peers)
78                })
79                .filter(|v| !v.is_empty())
80                .unwrap_or_else(|| {
81                    chat_core::config::BOOTSTRAP_PEERS.iter().map(|s| s.to_string()).collect()
82                });
83
84            tracing::info!("Bootstrap peers: {:?}", bootstrap_peers);
85            let node = {
86                let node_options = NodeOptions {
87                    listen_addresses: vec![], // WASM cannot listen
88                    bootstrap_peers,
89                    max_connections: 100,
90                    dht_client_mode: true,
91                    dht_discovery_enabled: true,
92                    stun_servers: Some(vec![
93                        "stun:stun.l.google.com:19302".into(),
94                        "stun:global.stun.twilio.com:3478".into(),
95                    ]),
96                };
97                network_libp2p::build_node(node_options).await
98            };
99
100            match node {
101                Ok(node) => {
102                    let peer_id = node.local_id();
103
104                    let conn = chat_platform_wasm::storage::open_storage("chat")
105                        .await
106                        .unwrap();
107                    let storage = chat_core::storage::SqliteStorage::new(conn);
108                    storage.init_schema().unwrap();
109
110                    let (handle, mut events, loop_fut) = chat_core::coordinator::build(node, storage);
111
112                    // Check tab lock
113                    if chat_platform_wasm::tab_lock::TabLock::try_acquire("p2pandemonium-tab-lock").is_none() {
114                        tab_lock_acquired.set(false);
115                        return;
116                    }
117
118                    // Spawn the coordinator loop
119                    spawn(loop_fut.run());
120
121                    coordinator_handle.set(Some(handle.clone()));
122
123                    // Shared state for e2e test API
124                    let doc_state_store: Rc<RefCell<Vec<ChatMessage>>> = Rc::new(RefCell::new(Vec::new()));
125                    let stats_store: Rc<RefCell<Option<Box<chat_core::history::types::ConnectionStats>>>> = Rc::new(RefCell::new(None));
126
127                    // Set up e2e test API on window
128                    if let Some(window) = web_sys::window() {
129                        let test_api = js_sys::Object::new();
130
131                        // peerId
132                        let _ = js_sys::Reflect::set(
133                            &test_api,
134                            &JsValue::from_str("peerId"),
135                            &JsValue::from_str(peer_id.as_str()),
136                        );
137
138                        // dialPeer(addr)
139                        let dial_handle = handle.clone();
140                        let dial_closure = Closure::wrap(Box::new(move |addr: JsValue| {
141                            if let Some(addr_str) = addr.as_string() {
142                                let h = dial_handle.clone();
143                                wasm_bindgen_futures::spawn_local(async move {
144                                    let _ = h.dial_peer(addr_str).await;
145                                });
146                            }
147                        }) as Box<dyn FnMut(JsValue)>);
148                        let _ = js_sys::Reflect::set(
149                            &test_api,
150                            &JsValue::from_str("dialPeer"),
151                            dial_closure.as_ref(),
152                        );
153                        dial_closure.forget();
154
155                        // getMultiaddrs -> returns array of current dialable addresses
156                        let get_multiaddrs_closure = Closure::wrap(Box::new({
157                            let stats_store = Rc::clone(&stats_store);
158                            move || {
159                                let stats = stats_store.borrow();
160                                let addrs = js_sys::Array::new();
161                                if let Some(ref s) = *stats {
162                                    if let Some(ref circuit) = s.circuit_address {
163                                        addrs.push(&JsValue::from_str(circuit));
164                                    }
165                                    if let Some(ref webrtc) = s.web_rtc_address {
166                                        addrs.push(&JsValue::from_str(webrtc));
167                                    }
168                                }
169                                addrs.into()
170                            }
171                        }) as Box<dyn FnMut() -> JsValue>);
172                        let _ = js_sys::Reflect::set(
173                            &test_api,
174                            &JsValue::from_str("getMultiaddrs"),
175                            get_multiaddrs_closure.as_ref(),
176                        );
177                        get_multiaddrs_closure.forget();
178
179                        // getPeerList -> returns array of connected peer IDs
180                        let get_peer_list_closure = Closure::wrap(Box::new({
181                            let stats_store = Rc::clone(&stats_store);
182                            move || {
183                                let stats = stats_store.borrow();
184                                let peers = js_sys::Array::new();
185                                if let Some(ref s) = *stats {
186                                    for p in &s.connected_peers {
187                                        peers.push(&JsValue::from_str(p));
188                                    }
189                                }
190                                peers.into()
191                            }
192                        }) as Box<dyn FnMut() -> JsValue>);
193                        let _ = js_sys::Reflect::set(
194                            &test_api,
195                            &JsValue::from_str("getPeerList"),
196                            get_peer_list_closure.as_ref(),
197                        );
198                        get_peer_list_closure.forget();
199
200                        // getPeerAddresses(peerId) -> returns array of known addresses for a peer
201                        let get_peer_addresses_closure = Closure::wrap(Box::new({
202                            let stats_store = Rc::clone(&stats_store);
203                            move |peer_id: JsValue| {
204                                let stats = stats_store.borrow();
205                                let addrs = js_sys::Array::new();
206                                if let Some(ref s) = *stats {
207                                    if let Some(peer_id_str) = peer_id.as_string() {
208                                        if let Some(known_addrs) = s.peer_addresses.get(&peer_id_str) {
209                                            for addr in known_addrs {
210                                                addrs.push(&JsValue::from_str(addr));
211                                            }
212                                        }
213                                    }
214                                }
215                                addrs.into()
216                            }
217                        }) as Box<dyn FnMut(JsValue) -> JsValue>);
218                        let _ = js_sys::Reflect::set(
219                            &test_api,
220                            &JsValue::from_str("getPeerAddresses"),
221                            get_peer_addresses_closure.as_ref(),
222                        );
223                        get_peer_addresses_closure.forget();
224
225                        // getPeerTransports(peerId) -> returns array of {emoji, title} objects
226                        let get_peer_transports_closure = Closure::wrap(Box::new({
227                            let stats_store = Rc::clone(&stats_store);
228                            move |peer_id: JsValue| {
229                                let stats = stats_store.borrow();
230                                let transports = js_sys::Array::new();
231                                if let Some(ref s) = *stats {
232                                    if let Some(peer_id_str) = peer_id.as_string() {
233                                        if let Some(peer_transports) = s.peer_transports.get(&peer_id_str) {
234                                            for name in peer_transports {
235                                                let (emoji, title) = transport_name_to_info(name);
236                                                let obj = js_sys::Object::new();
237                                                let _ = js_sys::Reflect::set(
238                                                    &obj,
239                                                    &JsValue::from_str("emoji"),
240                                                    &JsValue::from_str(emoji),
241                                                );
242                                                let _ = js_sys::Reflect::set(
243                                                    &obj,
244                                                    &JsValue::from_str("title"),
245                                                    &JsValue::from_str(title),
246                                                );
247                                                transports.push(&obj.into());
248                                            }
249                                        }
250                                    }
251                                }
252                                transports.into()
253                            }
254                        }) as Box<dyn FnMut(JsValue) -> JsValue>);
255                        let _ = js_sys::Reflect::set(
256                            &test_api,
257                            &JsValue::from_str("getPeerTransports"),
258                            get_peer_transports_closure.as_ref(),
259                        );
260                        get_peer_transports_closure.forget();
261
262                        // getDocState -> returns { messageCount, messageIds }
263                        let get_doc_closure = Closure::wrap(Box::new({
264                            let doc_state_store = Rc::clone(&doc_state_store);
265                            move || {
266                                let msgs = doc_state_store.borrow();
267                                let obj = js_sys::Object::new();
268                                let _ = js_sys::Reflect::set(
269                                    &obj,
270                                    &JsValue::from_str("messageCount"),
271                                    &JsValue::from_f64(msgs.len() as f64),
272                                );
273                                let ids = js_sys::Array::new();
274                                for msg in msgs.iter().take(20) {
275                                    ids.push(&JsValue::from_str(&msg.id));
276                                }
277                                let _ = js_sys::Reflect::set(
278                                    &obj,
279                                    &JsValue::from_str("messageIds"),
280                                    &ids.into(),
281                                );
282                                obj.into()
283                            }
284                        }) as Box<dyn FnMut() -> JsValue>);
285                        let _ = js_sys::Reflect::set(
286                            &test_api,
287                            &JsValue::from_str("getDocState"),
288                            get_doc_closure.as_ref(),
289                        );
290                        get_doc_closure.forget();
291
292                        let _ = js_sys::Reflect::set(
293                            &window,
294                            &JsValue::from_str("__p2pandemonium_test_api"),
295                            &test_api,
296                        );
297                    }
298
299                    // Read room from URL params for auto-join (only if no room stored)
300                    if room_name().is_none() {
301                        let room_from_url = web_sys::window()
302                            .and_then(|w| w.location().search().ok())
303                            .and_then(|search| {
304                                let params = web_sys::UrlSearchParams::new_with_str(&search).ok()?;
305                                params.get("room")
306                            });
307
308                        if let Some(room) = room_from_url {
309                            let _ = handle.join_room(room).await;
310                        }
311                    }
312
313                    // Shared event loop with test API side effects
314                    while let Ok(event) = events.recv().await {
315                        match event {
316                            chat_core::coordinator::CoordinatorEvent::MessagesUpdated(msgs) => {
317                                messages.set(msgs.clone());
318                                *doc_state_store.borrow_mut() = msgs;
319                            }
320                            chat_core::coordinator::CoordinatorEvent::StatsUpdated(s) => {
321                                stats.set(Some(s.clone()));
322                                *stats_store.borrow_mut() = Some(s);
323                            }
324                            chat_core::coordinator::CoordinatorEvent::RoomJoined { room_name: name } => {
325                                room_name.set(Some(name.clone()));
326                                // Update URL with room name for easy sharing
327                                if let Some(window) = web_sys::window()
328                                    && let Ok(history) = window.history()
329                                    && let Ok(href) = window.location().href()
330                                {
331                                    let base = href.split('?').next().unwrap_or(&href);
332                                    let new_url = format!("{}?room={}", base, name);
333                                    let _ = history.replace_state_with_url(
334                                        &wasm_bindgen::JsValue::undefined(),
335                                        "",
336                                        Some(&new_url),
337                                    );
338                                }
339                            }
340                            other => {
341                                chat_ui_components::handle_chat_event(other, signals);
342                            }
343                        }
344                    }
345                }
346                Err(e) => {
347                    error_messages.set(vec![format!("Failed to create node: {}", e)]);
348                }
349            }
350        });
351    });
352
353    let on_room_left = Some(EventHandler::new(move |_| {
354        // Clear URL room param
355        if let Some(window) = web_sys::window()
356            && let Ok(history) = window.history()
357            && let Ok(href) = window.location().href()
358        {
359            let base = href.split('?').next().unwrap_or(&href);
360            let _ = history.replace_state_with_url(
361                &wasm_bindgen::JsValue::undefined(),
362                "",
363                Some(base),
364            );
365        }
366    }));
367
368    rsx! {
369        style { dangerous_inner_html: CSS }
370        ChatApp { signals, on_room_left }
371    }
372}