use std::borrow::Cow; use std::cell::RefCell; use gpui::*; use theme::{Theme, ThemeMode}; use ui::Root; use universal_time::{Instant, MonotonicClock, SystemTime, WallClock, define_time_provider}; use wasm_bindgen::prelude::*; struct CustomTimeProvider; impl WallClock for CustomTimeProvider { fn system_time(&self) -> SystemTime { // Browser wall clock: milliseconds since the Unix epoch. let millis = js_sys::Date::now(); SystemTime::from_unix_duration(instant::Duration::from_millis(millis as u64)) } } impl MonotonicClock for CustomTimeProvider { fn instant(&self) -> Instant { // `performance.now()` is monotonic; fall back to the wall clock if // it's unavailable. let millis = web_sys::window() .and_then(|window| window.performance()) .map(|performance| performance.now()) .unwrap_or_else(js_sys::Date::now); Instant::from_ticks(instant::Duration::from_millis(millis as u64)) } } define_time_provider!(CustomTimeProvider); thread_local! { static APPLICATION: RefCell> = const { RefCell::new(None) }; } /// Applies a theme mode and restores the bundled web fonts. /// /// `Theme::change` reapplies the theme config, which can carry its own font /// family; host system fonts are unavailable in wasm, so the bundled Inter /// fonts are put back afterwards. fn apply_theme(mode: ThemeMode, cx: &mut App) { Theme::change(mode, None, cx); Theme::global_mut(cx).font_family = "Inter".into(); } /// Switches the app between light and dark after it is running. /// /// The embedding page calls this to keep the app in sync with its own /// appearance. #[cfg(target_family = "wasm")] #[wasm_bindgen] pub fn set_theme(dark: bool) { let mode = if dark { ThemeMode::Dark } else { ThemeMode::Light }; APPLICATION.with(|application| { if let Some(handle) = application.borrow().as_ref() { handle.update(|cx| { apply_theme(mode, cx); cx.refresh_windows(); }); } }); } #[wasm_bindgen] pub async fn run() -> Result<(), JsValue> { // Initialize logging to browser console console_log::init_with_level(log::Level::Info).expect("Failed to initialize logger"); // Also initialize tracing for WASM tracing_wasm::set_as_global_default(); #[cfg(target_family = "wasm")] gpui_platform::web_init(); // Install the panic hook AFTER `web_init` (which sets the default // `console_error_panic_hook`), so ours wins. It prints the entire // JS/wasm stack as a single string: `console_error_panic_hook`'s default // output is an `Error` object whose stack is collapsed in the console, // and on wasm the frames below the panic machinery name the task that // panicked (debug builds keep symbol names) — essential for diagnosing // `RefCell already borrowed`. #[cfg(target_family = "wasm")] std::panic::set_hook(Box::new(|info| { // Capture the JS stack (which includes the wasm frames with symbol // names in debug builds) without constructing DOM objects. let stack = js_sys::Reflect::get(&js_sys::Error::new(""), &"stack".into()) .ok() .and_then(|v| v.as_string()) .unwrap_or_default(); web_sys::console::error_1( &format!("{info}\n\n==== full stack ====\n{stack}\n=====================").into(), ); })); #[cfg(not(target_family = "wasm"))] let app = gpui_platform::application(); #[cfg(target_family = "wasm")] let app = { // Assets are not embedded in the WASM bundle; they are served from // the `/assets/...` URL prefix (see `web/www/vite.config.js`) and // downloaded by the `assets` crate. let assets = assets::Assets::new(""); // Download every icon and brand asset before the first frame: brand // images are loaded through GPUI's image cache, which does not retry // failed loads, and pre-caching the icons lets them render // immediately instead of waiting for a repaint. assets.preload().await; // NOTE: the multithreaded web backend (application_with_web_backend) // cannot host this app's backend. gpui's wasm background workers // block on `Atomics.wait` while idle, freezing their JS event loop, // so `spawn_local`-driven tasks (nostr-sdk's client actor, the // WebSocket transport) and fetch promises never make progress on a // worker thread. Everything must run on the main thread. gpui_platform::single_threaded_web().with_assets(assets) }; let launch = move |cx: &mut App| { // Load the embedded Inter font stack for WASM, where host system // fonts are unavailable. Inter is the app's UI font on Linux; the // wasm build reuses it so the web app matches the desktop look. let inter_regular = Cow::Borrowed(include_bytes!("../../assets/fonts/Inter/Inter-Regular.ttf").as_slice()); let inter_italic = Cow::Borrowed(include_bytes!("../../assets/fonts/Inter/Inter-Italic.ttf").as_slice()); let inter_medium = Cow::Borrowed(include_bytes!("../../assets/fonts/Inter/Inter-Medium.ttf").as_slice()); let inter_medium_italic = Cow::Borrowed( include_bytes!("../../assets/fonts/Inter/Inter-MediumItalic.ttf").as_slice(), ); let inter_semibold = Cow::Borrowed(include_bytes!("../../assets/fonts/Inter/Inter-SemiBold.ttf").as_slice()); let inter_semibold_italic = Cow::Borrowed( include_bytes!("../../assets/fonts/Inter/Inter-SemiBoldItalic.ttf").as_slice(), ); let inter_bold = Cow::Borrowed(include_bytes!("../../assets/fonts/Inter/Inter-Bold.ttf").as_slice()); let inter_bold_italic = Cow::Borrowed( include_bytes!("../../assets/fonts/Inter/Inter-BoldItalic.ttf").as_slice(), ); cx.text_system() .add_fonts(vec![ inter_regular, inter_italic, inter_medium, inter_medium_italic, inter_semibold, inter_semibold_italic, inter_bold, inter_bold_italic, ]) .expect("Failed to load fonts"); // Apply the system appearance before the first frame, so the app // never flashes the default light theme. apply_theme(cx.window_appearance().into(), cx); // Open the root window cx.open_window(WindowOptions::default(), |window, cx| { // Initialize components ui::init(cx); // Initialize theme registry theme::init(cx); // Initialize settings settings::init(window, cx); // Initialize the nostr client state::init(window, cx, None); // Initialize person registry person::init(window, cx); // Initialize device signer // // NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md device::init(window, cx); // Initialize app registry chat::init(window, cx); // Root view cx.new(|cx| Root::new(workspace::init(window, cx).into(), window, cx)) }) .expect("Failed to open window. Please restart the application."); cx.activate(true); }; #[cfg(target_family = "wasm")] APPLICATION.with(|application| { *application.borrow_mut() = Some(app.run_embedded(launch)); }); #[cfg(not(target_family = "wasm"))] app.run(launch); Ok(()) }