at main 269 lines 10 kB view raw
1//! Weaver App library. 2#[allow(unused)] 3use dioxus::{CapturedError, prelude::*}; 4 5#[cfg(feature = "fullstack-server")] 6pub use dioxus::fullstack::FullstackContext; 7use jacquard::oauth::{client::OAuthClient, session::ClientData}; 8#[allow(unused)] 9use jacquard::{ 10 smol_str::SmolStr, 11 types::{cid::Cid, string::AtIdentifier}, 12}; 13use std::sync::LazyLock; 14 15pub mod auth; 16#[cfg(feature = "server")] 17pub mod blobcache; 18pub mod cache_impl; 19pub mod collab_context; 20pub mod components; 21pub mod config; 22pub mod data; 23pub mod env; 24pub mod fetch; 25pub mod host_mode; 26#[cfg(feature = "server")] 27pub mod og; 28pub mod perf; 29pub mod record_utils; 30pub mod service_worker; 31 32pub mod custom_domain_app; 33#[cfg(feature = "server")] 34pub mod middleware; 35pub mod subdomain_app; 36pub mod views; 37#[cfg(feature = "server")] 38pub mod well_known; 39 40pub use custom_domain_app::{CustomDomainApp, CustomDomainRoute}; 41pub use host_mode::{CustomDomainContext, HostContext, LinkMode, SubdomainContext}; 42pub use subdomain_app::{SubdomainApp, SubdomainRoute}; 43 44use auth::{AuthState, AuthStore}; 45use components::{EntryPage, Repository, RepositoryIndex}; 46use config::{Config, OAuthConfig}; 47#[allow(unused)] 48use views::{ 49 AboutPage, Callback, DraftEdit, DraftsList, Editor, Home, InvitesPage, LeafletEntry, 50 LeafletEntryNsid, Navbar, NewDraft, Notebook, NotebookEntryByRkey, NotebookEntryEdit, 51 NotebookIndex, NotebookPage, NotebookSettings, PcktEntry, PcktEntryBlogNsid, PcktEntryNsid, 52 PrivacyPage, RecordIndex, RecordPage, StandaloneEntry, StandaloneEntryEdit, 53 StandaloneEntryNsid, TermsPage, WhiteWindEntry, WhiteWindEntryNsid, 54}; 55 56#[derive(Debug, Clone, Routable, PartialEq)] 57#[rustfmt::skip] 58pub enum Route { 59 #[layout(Navbar)] 60 #[route("/")] 61 Home {}, 62 #[route("/editor?:entry")] 63 Editor { entry: Option<String> }, 64 #[route("/about")] 65 AboutPage {}, 66 #[route("/tos")] 67 TermsPage {}, 68 #[route("/privacy")] 69 PrivacyPage {}, 70 #[layout(ErrorLayout)] 71 #[nest("/record")] 72 #[layout(RecordIndex)] 73 #[route("/:..uri")] 74 RecordPage { uri: Vec<String> }, 75 #[end_layout] 76 #[end_nest] 77 #[route("/callback?:state&:iss&:code")] 78 Callback { state: SmolStr, iss: SmolStr, code: SmolStr }, 79 #[nest("/:ident")] 80 #[layout(Repository)] 81 #[route("/")] 82 RepositoryIndex { ident: AtIdentifier<'static> }, 83 // Drafts routes (before /:book_title to avoid capture) 84 #[route("/drafts")] 85 DraftsList { ident: AtIdentifier<'static> }, 86 #[route("/drafts/:tid")] 87 DraftEdit { ident: AtIdentifier<'static>, tid: SmolStr }, 88 #[route("/new?:notebook")] 89 NewDraft { ident: AtIdentifier<'static>, notebook: Option<SmolStr> }, 90 // Collaboration invites 91 #[route("/invites")] 92 InvitesPage { ident: AtIdentifier<'static> }, 93 // Standalone entry routes 94 #[route("/e/:rkey")] 95 StandaloneEntry { ident: AtIdentifier<'static>, rkey: SmolStr }, 96 #[route("/sh.weaver.notebook.entry/:rkey")] 97 StandaloneEntryNsid { ident: AtIdentifier<'static>, rkey: SmolStr }, 98 #[route("/e/:rkey/edit")] 99 StandaloneEntryEdit { ident: AtIdentifier<'static>, rkey: SmolStr }, 100 // External blog routes (short paths) 101 #[route("/w/:rkey")] 102 WhiteWindEntry { ident: AtIdentifier<'static>, rkey: SmolStr }, 103 #[route("/l/:rkey")] 104 LeafletEntry { ident: AtIdentifier<'static>, rkey: SmolStr }, 105 #[route("/sd/:rkey")] 106 PcktEntry { ident: AtIdentifier<'static>, rkey: SmolStr }, 107 // External blog routes (NSID paths - replace at:// with https://host/) 108 #[route("/com.whtwnd.blog.entry/:rkey")] 109 WhiteWindEntryNsid { ident: AtIdentifier<'static>, rkey: SmolStr }, 110 #[route("/pub.leaflet.document/:rkey")] 111 LeafletEntryNsid { ident: AtIdentifier<'static>, rkey: SmolStr }, 112 #[route("/site.standard.document/:rkey")] 113 PcktEntryNsid { ident: AtIdentifier<'static>, rkey: SmolStr }, 114 #[route("/blog.pckt.document/:rkey")] 115 PcktEntryBlogNsid { ident: AtIdentifier<'static>, rkey: SmolStr }, 116 // Notebook routes 117 #[nest("/:book_title")] 118 #[layout(Notebook)] 119 #[route("/")] 120 NotebookIndex { ident: AtIdentifier<'static>, book_title: SmolStr }, 121 // Settings must come before /:title to avoid capture 122 #[route("/settings")] 123 NotebookSettings { ident: AtIdentifier<'static>, book_title: SmolStr }, 124 #[route("/:title")] 125 EntryPage { ident: AtIdentifier<'static>, book_title: SmolStr, title: SmolStr }, 126 // Entry by rkey (canonical path) 127 #[route("/e/:rkey")] 128 NotebookEntryByRkey { ident: AtIdentifier<'static>, book_title: SmolStr, rkey: SmolStr }, 129 #[route("/e/:rkey/edit")] 130 NotebookEntryEdit { ident: AtIdentifier<'static>, book_title: SmolStr, rkey: SmolStr }, 131} 132 133pub static CONFIG: LazyLock<Config> = LazyLock::new(|| Config { 134 oauth: OAuthConfig::from_env().as_metadata(), 135}); 136 137const FAVICON: Asset = asset!("/assets/weaver_photo_sm.jpg"); 138const MAIN_CSS: Asset = asset!("/assets/styling/main.css"); 139const THEME_DEFAULTS_CSS: Asset = asset!("/assets/styling/theme-defaults.css"); 140 141#[component] 142pub fn App() -> Element { 143 #[allow(unused)] 144 let fetcher = use_context_provider(|| { 145 fetch::Fetcher::new(OAuthClient::new( 146 AuthStore::new(), 147 ClientData::new_public(CONFIG.oauth.clone()), 148 )) 149 }); 150 151 // Read host context from request extensions (set by middleware). 152 #[cfg(feature = "fullstack-server")] 153 let host_ctx = { 154 use_server_cached(|| { 155 use dioxus::fullstack::FullstackContext; 156 157 let ctx = FullstackContext::current(); 158 ctx.and_then(|c| { 159 let parts = c.parts_mut(); 160 parts.extensions.get::<HostContext>().cloned() 161 }) 162 .unwrap_or(HostContext::MainDomain) 163 }) 164 }; 165 166 #[cfg(not(feature = "fullstack-server"))] 167 let host_ctx = HostContext::MainDomain; 168 169 let auth_state = use_signal(|| AuthState::default()); 170 #[allow(unused)] 171 let auth_state = use_context_provider(|| auth_state); 172 173 // Provide link mode for router-agnostic link generation. 174 let _link_mode = use_context_provider(|| host_ctx.link_mode()); 175 176 #[cfg(all(target_family = "wasm", target_os = "unknown"))] 177 let restore_result = { 178 let fetcher = fetcher.clone(); 179 use_resource(move || { 180 let fetcher = fetcher.clone(); 181 async move { auth::restore_session(fetcher, auth_state).await } 182 }) 183 }; 184 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))] 185 let restore_result: Option<auth::RestoreResult> = None; 186 187 #[cfg(all(target_family = "wasm", target_os = "unknown",))] 188 { 189 use_effect(move || { 190 let fetcher = fetcher.clone(); 191 spawn(async move { 192 use crate::service_worker; 193 194 tracing::info!("Registering service worker"); 195 let _ = service_worker::register_service_worker().await; 196 }); 197 }); 198 } 199 200 use_context_provider(|| restore_result); 201 202 // Dispatch to appropriate app based on host context. 203 match host_ctx { 204 HostContext::Subdomain(ctx) => { 205 tracing::info!("App: rendering SubdomainApp"); 206 use_context_provider(|| ctx); 207 rsx! { SubdomainApp {} } 208 } 209 HostContext::CustomDomain(ctx) => { 210 tracing::info!("App: rendering CustomDomainApp"); 211 use_context_provider(|| ctx); 212 rsx! { CustomDomainApp {} } 213 } 214 HostContext::MainDomain => { 215 tracing::info!("App: rendering MainDomainApp"); 216 rsx! { MainDomainApp {} } 217 } 218 } 219} 220 221#[component] 222pub fn MainDomainApp() -> Element { 223 rsx! { 224 document::Link { rel: "icon", href: FAVICON } 225 // Preconnect for external fonts (before loading them) 226 document::Link { rel: "preconnect", href: "https://fonts.googleapis.com" } 227 document::Link { rel: "preconnect", href: "https://fonts.gstatic.com" } 228 // Theme defaults first: CSS variables, font-faces, reset 229 document::Link { rel: "stylesheet", href: THEME_DEFAULTS_CSS } 230 document::Link { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;1,200;1,300;1,400;1,500;1,600;1,700&family=IBM+Plex+Sans:ital,wght@0,100..700;1,100..700&family=IBM+Plex+Serif:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;1,200;1,300;1,400;1,500;1,600;1,700&display=swap" } 231 // App shell styles (depends on theme variables) 232 document::Link { rel: "stylesheet", href: MAIN_CSS } 233 components::toast::ToastProvider { 234 Router::<Route> {} 235 } 236 } 237} 238 239// And then our Outlet is wrapped in a fallback UI 240#[component] 241pub fn ErrorLayout() -> Element { 242 rsx! { 243 ErrorBoundary { 244 handle_error: move |_err: ErrorContext| { 245 #[cfg(feature = "fullstack-server")] 246 { 247 let http_error = FullstackContext::commit_error_status(_err.error().unwrap()); 248 match http_error.status { 249 StatusCode::NOT_FOUND => rsx! { div { "404 - Page not found" } }, 250 _ => rsx! { div { "An unknown error occurred" } }, 251 } 252 } 253 #[cfg(not(feature = "fullstack-server"))] 254 { 255 rsx! { div { "An error occurred" } } 256 } 257 }, 258 Outlet::<Route> {} 259 } 260 } 261} 262 263#[cfg(all(feature = "fullstack-server", feature = "server"))] 264pub async fn favicon() -> axum::response::Response { 265 use axum::{http::header::CONTENT_TYPE, response::IntoResponse}; 266 let favicon_bytes = include_bytes!("../assets/weaver_photo_sm.jpg"); 267 268 ([(CONTENT_TYPE, "image/jpg")], favicon_bytes).into_response() 269}