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 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 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 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![], 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 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(loop_fut.run());
120
121 coordinator_handle.set(Some(handle.clone()));
122
123 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 if let Some(window) = web_sys::window() {
129 let test_api = js_sys::Object::new();
130
131 let _ = js_sys::Reflect::set(
133 &test_api,
134 &JsValue::from_str("peerId"),
135 &JsValue::from_str(peer_id.as_str()),
136 );
137
138 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 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 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 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 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 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 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 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 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 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}