subdomain routing works

Orual 8b6741eb 6e274038

+193 -171
+1
crates/weaver-app/src/components/app_link.rs
··· 75 75 #[component] 76 76 pub fn AppLink(props: AppLinkProps) -> Element { 77 77 let link_mode = use_context::<LinkMode>(); 78 + tracing::info!(?link_mode, "AppLink: reading LinkMode context"); 78 79 let class = props.class.clone().unwrap_or_default(); 79 80 80 81 match link_mode {
+8 -8
crates/weaver-app/src/components/entry.rs
··· 1 1 #![allow(non_snake_case)] 2 2 3 - use crate::components::{AppLink, AppLinkTarget}; 4 - use crate::Route; 5 3 #[cfg(feature = "server")] 6 4 use crate::blobcache::BlobCache; 7 5 use crate::components::AuthorList; 6 + use crate::components::{AppLink, AppLinkTarget}; 8 7 use crate::{components::EntryActions, data::use_handle}; 9 8 use dioxus::prelude::*; 10 9 use jacquard::types::aturi::AtUri; ··· 32 31 ) -> Element { 33 32 // Use feature-gated hook for SSR support 34 33 let (entry_res, entry) = crate::data::use_entry_data(ident, book_title, title); 35 - let route = use_route::<Route>(); 36 - let mut last_route = use_signal(|| route.clone()); 34 + 35 + // Track props for change detection (works with both Route and SubdomainRoute) 36 + let mut last_title = use_signal(|| title().clone()); 37 37 38 38 #[cfg(all( 39 39 target_family = "wasm", ··· 47 47 let mut entry_res = entry_res?; 48 48 49 49 #[cfg(feature = "fullstack-server")] 50 - use_effect(use_reactive!(|route| { 51 - if route != last_route() { 52 - tracing::debug!("[EntryPage] route changed, restarting resource"); 50 + use_effect(use_reactive!(|title| { 51 + if title != last_title() { 52 + tracing::debug!("[EntryPage] title changed, restarting resource"); 53 53 entry_res.restart(); 54 - last_route.set(route.clone()); 54 + last_title.set(title()); 55 55 } 56 56 })); 57 57
+14 -17
crates/weaver-app/src/components/login.rs
··· 4 4 use jacquard::oauth::session::ClientData; 5 5 use jacquard::{oauth::types::AuthorizeOptions, smol_str::SmolStr}; 6 6 7 - use crate::{CONFIG, Route}; 8 - use crate::{ 9 - components::{ 10 - button::{Button, ButtonVariant}, 11 - dialog::{DialogContent, DialogRoot, DialogTitle}, 12 - input::Input, 13 - }, 14 - fetch::Fetcher, 7 + use crate::components::{ 8 + button::{Button, ButtonVariant}, 9 + dialog::{DialogContent, DialogRoot, DialogTitle}, 10 + input::Input, 15 11 }; 12 + use crate::fetch::Fetcher; 13 + use crate::CONFIG; 16 14 17 15 fn handle_submit( 18 - full_route: Route, 16 + cached_route: String, 19 17 fetcher: Fetcher, 20 18 mut error: Signal<Option<String>>, 21 19 mut is_loading: Signal<bool>, ··· 34 32 #[cfg(target_arch = "wasm32")] 35 33 { 36 34 use gloo_storage::Storage; 37 - gloo_storage::LocalStorage::set("cached_route", format!("{}", full_route)).ok(); 35 + gloo_storage::LocalStorage::set("cached_route", cached_route).ok(); 38 36 spawn(async move { 39 37 match start_oauth_flow(handle, fetcher).await { 40 38 Ok(_) => { ··· 51 49 } 52 50 53 51 #[component] 54 - pub fn LoginModal(open: Signal<bool>) -> Element { 52 + pub fn LoginModal(open: Signal<bool>, cached_route: String) -> Element { 55 53 let mut handle_input = use_signal(|| String::new()); 56 54 let error = use_signal(|| Option::<String>::None); 57 55 let is_loading = use_signal(|| false); 58 - let full_route = use_route::<Route>(); 59 56 let fetcher = use_context::<Fetcher>(); 60 - let submit_route = full_route.clone(); 57 + let cached_route_clone = cached_route.clone(); 61 58 let submit_fetcher = fetcher.clone(); 62 59 let submit_closure1 = move || { 63 - let submit_route = submit_route.clone(); 60 + let cached_route = cached_route_clone.clone(); 64 61 let submit_fetcher = submit_fetcher.clone(); 65 62 handle_submit( 66 - submit_route, 63 + cached_route, 67 64 submit_fetcher, 68 65 error, 69 66 is_loading, ··· 73 70 }; 74 71 75 72 let submit_closure2 = move || { 76 - let submit_route = full_route.clone(); 73 + let cached_route = cached_route.clone(); 77 74 let submit_fetcher = fetcher.clone(); 78 75 handle_submit( 79 - submit_route, 76 + cached_route, 80 77 submit_fetcher, 81 78 error, 82 79 is_loading,
+4 -11
crates/weaver-app/src/components/profile.rs
··· 2 2 3 3 use std::sync::Arc; 4 4 5 - use crate::Route; 6 5 use crate::components::button::{Button, ButtonVariant}; 7 6 use crate::components::collab::api::{ReceivedInvite, accept_invite, fetch_received_invites}; 8 7 use crate::components::{ 9 8 BskyIcon, TangledIcon, 10 9 avatar::{Avatar, AvatarImage}, 11 10 }; 11 + use crate::env::WEAVER_APP_HOST; 12 12 use crate::fetch::Fetcher; 13 13 use dioxus::prelude::*; 14 14 use weaver_api::com_atproto::repo::strong_ref::StrongRef; ··· 406 406 use gloo_timers::future::TimeoutFuture; 407 407 TimeoutFuture::new(500).await; 408 408 } 409 - // Navigate to the entry - parse AT-URI into path segments 410 - // at://did/collection/rkey -> ["did", "collection", "rkey"] 411 - let uri_str = resource_uri_nav.to_string(); 412 - let uri_parts: Vec<String> = uri_str 413 - .strip_prefix("at://") 414 - .unwrap_or(&uri_str) 415 - .split('/') 416 - .map(|s| s.to_string()) 417 - .collect(); 418 - nav.push(Route::RecordPage { uri: uri_parts }); 409 + // Navigate to record page on main domain 410 + let url = format!("{}/record/{}", WEAVER_APP_HOST, resource_uri_nav); 411 + nav.push(url); 419 412 } 420 413 Err(e) => { 421 414 error.set(Some(format!("Failed: {}", e)));
+16 -16
crates/weaver-app/src/components/profile_actions.rs
··· 1 1 //! Actions sidebar/menubar for profile page. 2 2 3 - use crate::Route; 4 3 use crate::auth::AuthState; 4 + use crate::components::app_link::{AppLink, AppLinkTarget}; 5 5 use crate::components::button::{Button, ButtonVariant}; 6 6 use dioxus::prelude::*; 7 7 use jacquard::types::ident::AtIdentifier; ··· 32 32 aside { class: "profile-actions", 33 33 div { class: "profile-actions-container", 34 34 div { class: "profile-actions-list", 35 - Link { 36 - to: Route::NewDraft { ident: ident(), notebook: None }, 37 - class: "profile-action-link", 35 + AppLink { 36 + to: AppLinkTarget::NewDraft { ident: ident(), notebook: None }, 37 + class: "profile-action-link".to_string(), 38 38 Button { 39 39 variant: ButtonVariant::Outline, 40 40 "New Entry" ··· 48 48 "New Notebook" 49 49 } 50 50 51 - Link { 52 - to: Route::DraftsList { ident: ident() }, 53 - class: "profile-action-link", 51 + AppLink { 52 + to: AppLinkTarget::Drafts { ident: ident() }, 53 + class: "profile-action-link".to_string(), 54 54 Button { 55 55 variant: ButtonVariant::Ghost, 56 56 "Drafts" 57 57 } 58 58 } 59 59 60 - Link { 61 - to: Route::InvitesPage { ident: ident() }, 62 - class: "profile-action-link", 60 + AppLink { 61 + to: AppLinkTarget::Invites { ident: ident() }, 62 + class: "profile-action-link".to_string(), 63 63 Button { 64 64 variant: ButtonVariant::Ghost, 65 65 "Invites" ··· 90 90 91 91 rsx! { 92 92 div { class: "profile-actions-menubar", 93 - Link { 94 - to: Route::NewDraft { ident: ident(), notebook: None }, 93 + AppLink { 94 + to: AppLinkTarget::NewDraft { ident: ident(), notebook: None }, 95 95 Button { 96 96 variant: ButtonVariant::Primary, 97 97 "New Entry" 98 98 } 99 99 } 100 100 101 - Link { 102 - to: Route::DraftsList { ident: ident() }, 101 + AppLink { 102 + to: AppLinkTarget::Drafts { ident: ident() }, 103 103 Button { 104 104 variant: ButtonVariant::Ghost, 105 105 "Drafts" 106 106 } 107 107 } 108 108 109 - Link { 110 - to: Route::InvitesPage { ident: ident() }, 109 + AppLink { 110 + to: AppLinkTarget::Invites { ident: ident() }, 111 111 Button { 112 112 variant: ButtonVariant::Ghost, 113 113 "Invites"
+34 -6
crates/weaver-app/src/data.rs
··· 1585 1585 ident: ReadSignal<AtIdentifier<'static>>, 1586 1586 rkey: ReadSignal<SmolStr>, 1587 1587 ) -> ( 1588 - Result<Resource<Option<(serde_json::Value, serde_json::Value, Option<String>, Option<String>)>>, RenderError>, 1588 + Result< 1589 + Resource< 1590 + Option<( 1591 + serde_json::Value, 1592 + serde_json::Value, 1593 + Option<String>, 1594 + Option<String>, 1595 + )>, 1596 + >, 1597 + RenderError, 1598 + >, 1589 1599 Memo<Option<crate::fetch::LeafletDocumentData>>, 1590 1600 ) { 1591 1601 use weaver_api::pub_leaflet::document::Document; ··· 1661 1671 for page in &record.value.pages { 1662 1672 match page { 1663 1673 DocumentPagesItem::LinearDocument(linear_doc) => { 1664 - html.push_str(&render_linear_document(linear_doc, &ctx, &fetcher).await); 1674 + html.push_str( 1675 + &render_linear_document(linear_doc, &ctx, &fetcher).await, 1676 + ); 1665 1677 } 1666 1678 DocumentPagesItem::Canvas(_) => { 1667 1679 html.push_str("<div class=\"embed-video-placeholder\">[Canvas layout not yet supported]</div>"); 1668 1680 } 1669 1681 DocumentPagesItem::Unknown(_) => { 1670 - html.push_str("<div class=\"embed-video-placeholder\">[Unknown page type]</div>"); 1682 + html.push_str( 1683 + "<div class=\"embed-video-placeholder\">[Unknown page type]</div>", 1684 + ); 1671 1685 } 1672 1686 } 1673 1687 } ··· 1773 1787 for page in &record.value.pages { 1774 1788 match page { 1775 1789 DocumentPagesItem::LinearDocument(linear_doc) => { 1776 - html.push_str(&render_linear_document(linear_doc, &ctx, &fetcher).await); 1790 + html.push_str( 1791 + &render_linear_document(linear_doc, &ctx, &fetcher).await, 1792 + ); 1777 1793 } 1778 1794 DocumentPagesItem::Canvas(_) => { 1779 1795 html.push_str("<div class=\"embed-video-placeholder\">[Canvas layout not yet supported]</div>"); 1780 1796 } 1781 1797 DocumentPagesItem::Unknown(_) => { 1782 - html.push_str("<div class=\"embed-video-placeholder\">[Unknown page type]</div>"); 1798 + html.push_str( 1799 + "<div class=\"embed-video-placeholder\">[Unknown page type]</div>", 1800 + ); 1783 1801 } 1784 1802 } 1785 1803 } ··· 1810 1828 ident: ReadSignal<AtIdentifier<'static>>, 1811 1829 rkey: ReadSignal<SmolStr>, 1812 1830 ) -> ( 1813 - Result<Resource<Option<(serde_json::Value, serde_json::Value, Option<String>, Option<String>)>>, RenderError>, 1831 + Result< 1832 + Resource< 1833 + Option<( 1834 + serde_json::Value, 1835 + serde_json::Value, 1836 + Option<String>, 1837 + Option<String>, 1838 + )>, 1839 + >, 1840 + RenderError, 1841 + >, 1814 1842 Memo<Option<crate::fetch::PcktDocumentData>>, 1815 1843 ) { 1816 1844 let fetcher = use_context::<crate::fetch::Fetcher>();
+13 -10
crates/weaver-app/src/lib.rs
··· 72 72 #[layout(RecordIndex)] 73 73 #[route("/:..uri")] 74 74 RecordPage { uri: Vec<String> }, 75 - #[end_layout] 75 + #[end_layout] 76 76 #[end_nest] 77 77 #[route("/callback?:state&:iss&:code")] 78 78 Callback { state: SmolStr, iss: SmolStr, code: SmolStr }, ··· 124 124 #[route("/e/:rkey")] 125 125 NotebookEntryByRkey { ident: AtIdentifier<'static>, book_title: SmolStr, rkey: SmolStr }, 126 126 #[route("/e/:rkey/edit")] 127 - NotebookEntryEdit { ident: AtIdentifier<'static>, book_title: SmolStr, rkey: SmolStr } 128 - 127 + NotebookEntryEdit { ident: AtIdentifier<'static>, book_title: SmolStr, rkey: SmolStr }, 129 128 } 130 129 131 130 pub static CONFIG: LazyLock<Config> = LazyLock::new(|| Config { ··· 170 169 tracing::warn!("Host header not valid UTF-8"); 171 170 return None; 172 171 }; 173 - tracing::info!(host, "Subdomain detection: got host"); 174 172 175 173 let host_str = host.split(':').next().unwrap_or(host); 176 174 let Some(subdomain) = extract_subdomain(host_str, WEAVER_APP_DOMAIN) else { 177 - tracing::info!(host_str, domain = WEAVER_APP_DOMAIN, "Not a subdomain request"); 175 + tracing::info!( 176 + host_str, 177 + domain = WEAVER_APP_DOMAIN, 178 + "Not a subdomain request" 179 + ); 178 180 return None; 179 181 }; 180 - tracing::info!(subdomain, "Subdomain detection: extracted subdomain"); 181 - 182 182 // Look up notebook by global path 183 183 let result = lookup_subdomain_context(&fetcher, &subdomain).await; 184 184 if result.is_none() { ··· 192 192 #[cfg(feature = "fullstack-server")] 193 193 let ctx = match &*ctx_resource.read() { 194 194 Some(ctx) => ctx.clone(), 195 - None => return rsx! { div { "Loading..." } }, 195 + None => { 196 + return rsx! { div { "Loading..." } }; 197 + } 196 198 }; 197 199 198 200 #[cfg(not(feature = "fullstack-server"))] 199 - let ctx = None::<SubdomainContext>; 201 + let ctx = { None::<SubdomainContext> }; 200 202 201 203 let auth_state = use_signal(|| AuthState::default()); 202 204 #[allow(unused)] ··· 235 237 }); 236 238 } 237 239 238 - #[cfg(all(target_family = "wasm", target_os = "unknown"))] 239 240 use_context_provider(|| restore_result); 240 241 241 242 if sub == LinkMode::Subdomain { 243 + tracing::info!("App: rendering SubdomainApp"); 242 244 use_context_provider(|| ctx.unwrap()); 243 245 rsx! { 244 246 SubdomainApp {} 245 247 } 246 248 } else { 249 + tracing::info!("App: rendering MainDomainApp"); 247 250 rsx! { 248 251 MainDomainApp {} 249 252 }
+4 -11
crates/weaver-app/src/main.rs
··· 1 1 //! Weaver App main binary. 2 2 3 + #[allow(unused)] 3 4 use dioxus::prelude::*; 4 - 5 + #[cfg(target_arch = "wasm32")] 6 + use lol_alloc::{FreeListAllocator, LockedAllocator}; 5 7 #[cfg(feature = "server")] 6 8 use std::sync::Arc; 7 - 8 9 #[cfg(feature = "server")] 9 10 use tower::Service; 10 - 11 - #[cfg(feature = "server")] 11 + #[allow(unused)] 12 12 use weaver_app::{App, CONFIG, SubdomainApp, SubdomainContext, fetch}; 13 - 14 - #[cfg(not(feature = "server"))] 15 - use weaver_app::{App, SubdomainApp}; 16 - 17 - #[cfg(target_arch = "wasm32")] 18 - use lol_alloc::{FreeListAllocator, LockedAllocator}; 19 - 20 13 #[cfg(target_arch = "wasm32")] 21 14 #[global_allocator] 22 15 static ALLOCATOR: LockedAllocator<FreeListAllocator> =
+28 -23
crates/weaver-app/src/subdomain_app.rs
··· 3 3 //! Separate router for subdomain hosting with simpler route structure. 4 4 5 5 use dioxus::prelude::*; 6 - use jacquard::oauth::client::OAuthClient; 7 - use jacquard::oauth::session::ClientData; 8 6 use jacquard::smol_str::{SmolStr, ToSmolStr}; 9 7 use jacquard::types::string::AtIdentifier; 10 8 11 - use crate::auth; 12 - use crate::auth::{AuthState, AuthStore}; 13 9 use crate::components::identity::RepositoryIndex; 14 10 use crate::components::{EntryPage, NotebookCss}; 15 - use crate::host_mode::{LinkMode, SubdomainContext}; 11 + use crate::host_mode::SubdomainContext; 16 12 use crate::views::{NotebookEntryByRkey, NotebookEntryEdit, NotebookIndex, SubdomainNavbar}; 17 - use crate::{CONFIG, fetch}; 18 13 19 14 /// Subdomain route enum - simpler paths without /:ident/:notebook prefix. 20 15 #[derive(Debug, Clone, Routable, PartialEq)] ··· 54 49 let request = ResolveGlobalNotebook::new().path(path).build(); 55 50 56 51 match fetcher.send(request).await { 57 - Ok(response) => { 58 - let output = response.into_output().ok()?; 59 - let notebook = output.notebook; 52 + Ok(response) => match response.into_output() { 53 + Ok(output) => { 54 + let notebook = output.notebook; 60 55 61 - let owner = notebook.uri.authority().clone().into_static(); 62 - let rkey = notebook.uri.rkey()?.0.to_smolstr(); 63 - let notebook_path = notebook 64 - .path 65 - .map(|p| SmolStr::new(p.as_ref())) 66 - .unwrap_or_else(|| SmolStr::new(path)); 56 + let owner = notebook.uri.authority().clone().into_static(); 57 + let Some(rkey) = notebook.uri.rkey() else { 58 + tracing::warn!(path, uri = %notebook.uri, "Notebook URI missing rkey"); 59 + return None; 60 + }; 61 + let rkey = rkey.0.to_smolstr(); 62 + let notebook_path = notebook 63 + .path 64 + .map(|p| SmolStr::new(p.as_ref())) 65 + .unwrap_or_else(|| SmolStr::new(path)); 67 66 68 - Some(SubdomainContext { 69 - owner, 70 - notebook_path, 71 - notebook_rkey: rkey, 72 - notebook_title: notebook.title.clone().unwrap_or_default().to_smolstr(), 73 - }) 74 - } 67 + tracing::info!(path, %owner, %rkey, "Notebook lookup succeeded"); 68 + Some(SubdomainContext { 69 + owner, 70 + notebook_path, 71 + notebook_rkey: rkey, 72 + notebook_title: notebook.title.clone().unwrap_or_default().to_smolstr(), 73 + }) 74 + } 75 + Err(e) => { 76 + tracing::warn!(path, error = %e, "Failed to parse notebook response"); 77 + None 78 + } 79 + }, 75 80 Err(e) => { 76 - tracing::debug!(path = path, error = %e, "Global notebook lookup failed"); 81 + tracing::warn!(path, error = %e, "Global notebook lookup request failed"); 77 82 None 78 83 } 79 84 }
+11 -5
crates/weaver-app/src/views/footer.rs
··· 12 12 13 13 /// Determines if the current route should show the full footer or just the minimal version. 14 14 /// Full footer shows on shell pages (Home, Editor) and on owner's content pages. 15 - fn should_show_full_footer(route: &Route) -> bool { 15 + pub fn should_show_full_footer(route: &Route) -> bool { 16 16 match route { 17 17 // Shell pages: always show full footer 18 18 Route::Home {} ··· 67 67 } 68 68 69 69 #[component] 70 - pub fn Footer() -> Element { 71 - let route = use_route::<Route>(); 72 - let show_full = should_show_full_footer(&route); 73 - 70 + pub fn Footer(#[props(default = true)] show_full: bool) -> Element { 74 71 rsx! { 75 72 document::Link { rel: "stylesheet", href: FOOTER_CSS } 76 73 ··· 174 171 rel: "noopener", 175 172 "Report Bug" 176 173 } 174 + 175 + span { class: "footer-separator", "|" } 176 + 177 + Link { 178 + to: crate::env::WEAVER_APP_HOST, 179 + class: "footer-link", 180 + "weaver.sh" 181 + } 182 + 177 183 } 178 184 } 179 185 }
+1 -1
crates/weaver-app/src/views/mod.rs
··· 39 39 pub use invites::InvitesPage; 40 40 41 41 mod footer; 42 - pub use footer::Footer; 42 + pub use footer::{Footer, should_show_full_footer}; 43 43 44 44 mod static_page; 45 45 pub use static_page::{AboutPage, PrivacyPage, TermsPage};
+5 -3
crates/weaver-app/src/views/navbar.rs
··· 4 4 use crate::components::login::LoginModal; 5 5 use crate::data::{use_get_handle, use_load_handle}; 6 6 use crate::fetch::Fetcher; 7 - use crate::views::Footer; 7 + use crate::views::{Footer, should_show_full_footer}; 8 8 use dioxus::prelude::*; 9 9 use dioxus_primitives::toast::{ToastOptions, use_toast}; 10 10 use jacquard::types::ident::AtIdentifier; ··· 62 62 #[cfg(feature = "fullstack-server")] 63 63 route_handle_res?; 64 64 65 + #[allow(unused)] 65 66 let fetcher = use_context::<Fetcher>(); 66 67 let mut show_login_modal = use_signal(|| false); 67 68 ··· 292 293 293 294 } 294 295 LoginModal { 295 - open: show_login_modal 296 + open: show_login_modal, 297 + cached_route: format!("{}", route), 296 298 } 297 299 } 298 300 } ··· 301 303 Outlet::<Route> {} 302 304 } 303 305 304 - Footer {} 306 + Footer { show_full: should_show_full_footer(&route) } 305 307 } 306 308 } 307 309 }
+39 -45
crates/weaver-app/src/views/subdomain_navbar.rs
··· 4 4 use jacquard::types::string::{AtIdentifier, Did}; 5 5 6 6 use crate::SubdomainRoute; 7 + #[allow(unused_imports)] 7 8 use crate::auth::{AuthState, RestoreResult}; 8 9 use crate::components::button::{Button, ButtonVariant}; 9 10 use crate::components::login::LoginModal; 10 11 use crate::data::{use_get_handle, use_handle}; 12 + #[allow(unused_imports)] 13 + use crate::env::WEAVER_APP_HOST; 11 14 use crate::fetch::Fetcher; 12 15 use crate::host_mode::SubdomainContext; 13 16 use crate::views::Footer; ··· 26 29 let ctx = use_context::<SubdomainContext>(); 27 30 let route = use_route::<SubdomainRoute>(); 28 31 let auth_state = use_context::<Signal<AuthState>>(); 32 + 33 + #[allow(unused)] 29 34 let fetcher = use_context::<Fetcher>(); 30 35 let mut show_login_modal = use_signal(|| false); 31 36 ··· 61 66 div { 62 67 id: "navbar", 63 68 nav { class: "breadcrumbs", 64 - // Notebook title links to index 65 - Link { 66 - to: SubdomainRoute::SubdomainLanding {}, 67 - class: "breadcrumb", 68 - "{ctx.notebook_title}" 69 - } 70 - 71 - // Show current location breadcrumb based on route 72 - match &route { 73 - SubdomainRoute::SubdomainLanding {} | SubdomainRoute::SubdomainIndexPage {} => { 74 - rsx! {} 75 - } 76 - SubdomainRoute::SubdomainEntry { title } => { 77 - rsx! { 78 - span { class: "breadcrumb-separator", " > " } 79 - span { class: "breadcrumb breadcrumb-current", "{title}" } 80 - } 69 + // Notebook title links to index 70 + Link { 71 + to: SubdomainRoute::SubdomainLanding {}, 72 + class: "breadcrumb", 73 + "{ctx.notebook_title}" 81 74 } 82 - SubdomainRoute::SubdomainEntryByRkey { rkey } => { 83 - rsx! { 84 - span { class: "breadcrumb-separator", " > " } 85 - span { class: "breadcrumb breadcrumb-current", "{rkey}" } 75 + 76 + // Show current location breadcrumb based on route 77 + match &route { 78 + SubdomainRoute::SubdomainLanding {} | SubdomainRoute::SubdomainIndexPage {} | SubdomainRoute::SubdomainEntryByRkey { .. } | SubdomainRoute::SubdomainEntry { .. } => { 79 + rsx! {} 86 80 } 87 - } 88 - SubdomainRoute::SubdomainEntryEdit { rkey } => { 89 - rsx! { 90 - span { class: "breadcrumb-separator", " > " } 91 - Link { 92 - to: SubdomainRoute::SubdomainEntryByRkey { rkey: rkey.clone() }, 93 - class: "breadcrumb", 94 - "{rkey}" 81 + SubdomainRoute::SubdomainEntryEdit { rkey } => { 82 + rsx! { 83 + span { class: "breadcrumb-separator", " > " } 84 + Link { 85 + to: SubdomainRoute::SubdomainEntryByRkey { rkey: rkey.clone() }, 86 + class: "breadcrumb", 87 + "{rkey}" 88 + } 95 89 } 96 - span { class: "breadcrumb-separator", " > " } 97 - span { class: "breadcrumb breadcrumb-current", "Edit" } 98 90 } 99 - } 100 - SubdomainRoute::SubdomainProfile { ident } => { 101 - rsx! { 102 - span { class: "breadcrumb-separator", " > " } 103 - span { class: "breadcrumb breadcrumb-current", "@{ident}" } 91 + SubdomainRoute::SubdomainProfile { ident } => { 92 + rsx! { 93 + span { class: "breadcrumb-separator", " > " } 94 + span { class: "breadcrumb breadcrumb-current", "@{ident}" } 95 + } 104 96 } 105 97 } 106 98 } 107 - } 108 - 109 - // Author profile link 110 - nav { class: "nav-tools", 111 - AuthorProfileLink { ident: ctx.owner.clone() } 112 - } 99 + // Author profile link 100 + nav { class: "nav-tools", 101 + AuthorProfileLink { ident: ctx.owner.clone() } 102 + } 113 103 114 104 // Auth button 115 105 if auth_state.read().is_authenticated() { ··· 126 116 } 127 117 } 128 118 LoginModal { 129 - open: show_login_modal 119 + open: show_login_modal, 120 + cached_route: format!("{}", route), 130 121 } 131 122 } 132 123 } 133 124 125 + 126 + 127 + 134 128 main { class: "app-main", 135 129 Outlet::<SubdomainRoute> {} 136 130 } 137 131 138 - Footer {} 132 + Footer { show_full: false } 139 133 } 140 134 } 141 135 } ··· 189 183 let (handle_res, handle) = use_handle(ident); 190 184 191 185 #[cfg(feature = "fullstack-server")] 192 - let mut handle_res = handle_res?; 186 + handle_res?; 193 187 194 188 rsx! { 195 189 Link {
+1 -1
crates/weaver-index/migrations/clickhouse/009_handle_mappings_identity_mv.sql
··· 8 8 'active' as account_status, 9 9 'identity' as source, 10 10 event_time, 11 - now64(3) as indexed_at 11 + indexed_at 12 12 FROM raw_identity_events 13 13 WHERE handle != ''
+1 -1
crates/weaver-index/migrations/clickhouse/010_handle_mappings_account_mv.sql
··· 8 8 a.status as account_status, 9 9 'account' as source, 10 10 a.event_time, 11 - now64(3) as indexed_at 11 + a.indexed_at 12 12 FROM raw_account_events a 13 13 INNER JOIN handle_mappings h ON h.did = a.did AND h.freed = 0 14 14 WHERE a.active = 0 AND a.status != ''
+1 -1
crates/weaver-index/migrations/clickhouse/012_profiles_weaver_mv.sql
··· 10 10 coalesce(record.banner.ref.`$link`, '') as banner_cid, 11 11 parseDateTime64BestEffortOrZero(toString(record.createdAt), 3) as created_at, 12 12 event_time, 13 - now64(3) as indexed_at, 13 + indexed_at, 14 14 if(operation = 'delete', event_time, toDateTime64(0, 3)) as deleted_at 15 15 FROM raw_records 16 16 WHERE collection = 'sh.weaver.actor.profile'
+1 -1
crates/weaver-index/migrations/clickhouse/014_profiles_bsky_mv.sql
··· 10 10 coalesce(record.banner.ref.`$link`, '') as banner_cid, 11 11 parseDateTime64BestEffortOrZero(toString(record.createdAt), 3) as created_at, 12 12 event_time, 13 - now64(3) as indexed_at, 13 + indexed_at, 14 14 if(operation = 'delete', event_time, toDateTime64(0, 3)) as deleted_at 15 15 FROM raw_records 16 16 WHERE collection = 'app.bsky.actor.profile'
+1 -1
crates/weaver-index/migrations/clickhouse/018_notebooks_mv.sql
··· 13 13 parseDateTime64BestEffortOrZero(toString(record.createdAt), 3) as created_at, 14 14 parseDateTime64BestEffortOrZero(toString(record.updatedAt), 3) as updated_at, 15 15 event_time, 16 - now64(3) as indexed_at, 16 + indexed_at, 17 17 if(operation = 'delete', event_time, toDateTime64(0, 3)) as deleted_at, 18 18 record 19 19 FROM raw_records
+1 -1
crates/weaver-index/migrations/clickhouse/021_entries_mv.sql
··· 12 12 parseDateTime64BestEffortOrZero(toString(record.createdAt), 3) as created_at, 13 13 parseDateTime64BestEffortOrZero(toString(record.updatedAt), 3) as updated_at, 14 14 event_time, 15 - now64(3) as indexed_at, 15 + indexed_at, 16 16 if(operation = 'delete', event_time, toDateTime64(0, 3)) as deleted_at, 17 17 record 18 18 FROM raw_records
+1 -1
crates/weaver-index/migrations/clickhouse/024_drafts_mv.sql
··· 7 7 cid, 8 8 parseDateTime64BestEffortOrZero(toString(record.createdAt), 3) as created_at, 9 9 event_time, 10 - now64(3) as indexed_at, 10 + indexed_at, 11 11 if(operation = 'delete', event_time, toDateTime64(0, 3)) as deleted_at 12 12 FROM raw_records 13 13 WHERE collection = 'sh.weaver.edit.draft'
+1 -1
crates/weaver-index/migrations/clickhouse/026_edit_roots_mv.sql
··· 60 60 61 61 event_time as created_at, 62 62 event_time, 63 - now64(3) as indexed_at, 63 + indexed_at, 64 64 if(operation = 'delete', event_time, toDateTime64(0, 3)) as deleted_at 65 65 FROM raw_records 66 66 WHERE collection = 'sh.weaver.edit.root'
+1 -1
crates/weaver-index/migrations/clickhouse/027_edit_diffs_mv.sql
··· 68 68 69 69 coalesce(parseDateTime64BestEffortOrNull(toString(record.createdAt), 3), event_time) as created_at, 70 70 event_time, 71 - now64(3) as indexed_at, 71 + indexed_at, 72 72 if(operation = 'delete', event_time, toDateTime64(0, 3)) as deleted_at 73 73 FROM raw_records 74 74 WHERE collection = 'sh.weaver.edit.diff'
+1 -1
crates/weaver-index/migrations/clickhouse/030_collab_invites_mv.sql
··· 17 17 parseDateTime64BestEffortOrZero(toString(record.expiresAt), 3) as expires_at, 18 18 coalesce(parseDateTime64BestEffortOrNull(toString(record.createdAt), 3), event_time) as created_at, 19 19 event_time, 20 - now64(3) as indexed_at, 20 + indexed_at, 21 21 if(operation = 'delete', event_time, toDateTime64(0, 3)) as deleted_at 22 22 FROM raw_records 23 23 WHERE collection = 'sh.weaver.collab.invite'
+1 -1
crates/weaver-index/migrations/clickhouse/032_collab_accepts_mv.sql
··· 18 18 19 19 coalesce(parseDateTime64BestEffortOrNull(toString(record.createdAt), 3), event_time) as created_at, 20 20 event_time, 21 - now64(3) as indexed_at, 21 + indexed_at, 22 22 if(operation = 'delete', event_time, toDateTime64(0, 3)) as deleted_at 23 23 FROM raw_records 24 24 WHERE collection = 'sh.weaver.collab.accept'
+1 -1
crates/weaver-index/migrations/clickhouse/034_collab_sessions_mv.sql
··· 16 16 coalesce(parseDateTime64BestEffortOrNull(toString(record.createdAt), 3), event_time) as created_at, 17 17 parseDateTime64BestEffortOrZero(toString(record.expiresAt), 3) as expires_at, 18 18 event_time, 19 - now64(3) as indexed_at, 19 + indexed_at, 20 20 if(operation = 'delete', event_time, toDateTime64(0, 3)) as deleted_at 21 21 FROM raw_records 22 22 WHERE collection = 'sh.weaver.collab.session'
+1 -1
crates/weaver-index/migrations/clickhouse/039_notebook_entries_mv.sql
··· 14 14 -- Position from array index 15 15 assumeNotNull(entry_position) as position, 16 16 17 - now64(3) as indexed_at 17 + indexed_at 18 18 FROM notebooks 19 19 ARRAY JOIN 20 20 record.entryList[].uri as entry_uri,
+2 -2
docker-compose.yml
··· 54 54 #TAP_FULL_NETWORK: true 55 55 #TAP_SIGNAL_COLLECTION: place.stream.chat.profile 56 56 TAP_SIGNAL_COLLECTION: sh.weaver.actor.profile 57 - TAP_COLLECTION_FILTERS: "sh.weaver.*,app.bsky.actor.profile,sh.tangled.*,pub.leaflet.*,net.anisota.*,place.stream.*" 57 + TAP_COLLECTION_FILTERS: "sh.weaver.*,app.bsky.actor.profile,sh.tangled.*,pub.leaflet.*,net.anisota.*,place.stream.*,site.standard.*" 58 58 healthcheck: 59 59 test: ["CMD", "wget", "-q", "--spider", "http://localhost:2480/health"] 60 60 interval: 20s ··· 81 81 TAP_URL: ws://tap:2480/channel 82 82 TAP_SEND_ACKS: "true" 83 83 FIREHOSE_RELAY_URL: wss://bsky.network 84 - INDEXER_COLLECTIONS: "sh.weaver.*,app.bsky.actor.profile,sh.tangled.*,pub.leaflet.*,net.anisota.*,place.stream.*" 84 + INDEXER_COLLECTIONS: "sh.weaver.*,app.bsky.actor.profile,sh.tangled.*,pub.leaflet.*,net.anisota.*,place.stream.*,site.standard.*" 85 85 depends_on: 86 86 tap: 87 87 condition: service_healthy