hmmm x 2

Orual 27f77851 94b6302a

+180 -192
+1
Cargo.lock
··· 4156 "n0-future", 4157 "ouroboros", 4158 "p256", 4159 "rand 0.9.2", 4160 "regex", 4161 "regex-lite",
··· 4156 "n0-future", 4157 "ouroboros", 4158 "p256", 4159 + "postcard", 4160 "rand 0.9.2", 4161 "regex", 4162 "regex-lite",
-10
crates/weaver-app/.env-dev
··· 1 - WEAVER_APP_ENV="dev" 2 - WEAVER_APP_HOST="http://localhost" 3 - WEAVER_APP_DOMAIN="" 4 - WEAVER_PORT=8080 5 - WEAVER_APP_SCOPES="atproto transition:generic" 6 - WEAVER_CLIENT_NAME="Weaver" 7 - 8 - WEAVER_LOGO_URI="" 9 - WEAVER_TOS_URI="" 10 - WEAVER_PRIVACY_POLICY_URI=""
···
+10
crates/weaver-app/.env-prod
···
··· 1 + WEAVER_APP_ENV="prod" 2 + WEAVER_APP_HOST="https://alpha.weaver.sh" 3 + WEAVER_APP_DOMAIN="https://alpha.weaver.sh" 4 + WEAVER_PORT=8080 5 + WEAVER_APP_SCOPES="atproto transition:generic" 6 + WEAVER_CLIENT_NAME="Weaver" 7 + 8 + WEAVER_LOGO_URI="" 9 + WEAVER_TOS_URI="" 10 + WEAVER_PRIVACY_POLICY_URI=""
+1 -1
crates/weaver-app/Cargo.toml
··· 5 edition = "2024" 6 7 [features] 8 - default = ["web", "fullstack-server", "no-app-index"] 9 # Fullstack mode with SSR and server functions 10 fullstack-server = ["dioxus/fullstack"] 11 wasm-split = ["dioxus/wasm-split"]
··· 5 edition = "2024" 6 7 [features] 8 + default = ["web", "no-app-index"] 9 # Fullstack mode with SSR and server functions 10 fullstack-server = ["dioxus/fullstack"] 11 wasm-split = ["dioxus/wasm-split"]
+8 -6
crates/weaver-app/src/components/entry.rs
··· 42 title: ReadSignal<SmolStr>, 43 ) -> Element { 44 // Use feature-gated hook for SSR support 45 - let entry = crate::data::use_entry_data(ident(), book_title(), title()); 46 let handle = use_context::<NotebookHandle>(); 47 let fetcher = use_context::<crate::fetch::Fetcher>(); 48 ··· 433 #[allow(unused)] 434 pub fn EntryMarkdown(props: EntryMarkdownProps) -> Element { 435 let processed = crate::data::use_rendered_markdown( 436 - props.content.read().clone(), 437 - props.ident.read().clone(), 438 )?; 439 440 - match &*processed.read_unchecked() { 441 Some(Some(html_buf)) => rsx! { 442 div { 443 id: "{&*props.id.read()}", ··· 464 ident: AtIdentifier<'static>, 465 ) -> Element { 466 // Use feature-gated hook for SSR support 467 - let processed = crate::data::use_rendered_markdown(content, ident)?; 468 469 - match &*processed.read_unchecked() { 470 Some(Some(html_buf)) => rsx! { 471 div { 472 id: "{id}",
··· 42 title: ReadSignal<SmolStr>, 43 ) -> Element { 44 // Use feature-gated hook for SSR support 45 + let entry = crate::data::use_entry_data(ident, book_title, title); 46 let handle = use_context::<NotebookHandle>(); 47 let fetcher = use_context::<crate::fetch::Fetcher>(); 48 ··· 433 #[allow(unused)] 434 pub fn EntryMarkdown(props: EntryMarkdownProps) -> Element { 435 let processed = crate::data::use_rendered_markdown( 436 + props.content, 437 + props.ident, 438 )?; 439 440 + match &*processed.read() { 441 Some(Some(html_buf)) => rsx! { 442 div { 443 id: "{&*props.id.read()}", ··· 464 ident: AtIdentifier<'static>, 465 ) -> Element { 466 // Use feature-gated hook for SSR support 467 + let content = use_signal(|| content); 468 + let ident = use_signal(|| ident); 469 + let processed = crate::data::use_rendered_markdown(content.into(), ident.into())?; 470 471 + match &*processed.read() { 472 Some(Some(html_buf)) => rsx! { 473 div { 474 id: "{id}",
+7 -6
crates/weaver-app/src/components/identity.rs
··· 9 #[component] 10 pub fn Repository(ident: ReadSignal<AtIdentifier<'static>>) -> Element { 11 // Fetch notebooks for this specific DID with SSR support 12 - let notebooks = data::use_notebooks_for_did(ident())?; 13 use_context_provider(|| notebooks); 14 rsx! { 15 div { ··· 19 } 20 21 pub fn use_repo_notebook_context() 22 - -> Option<Resource<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>>> { 23 - try_use_context::<Resource<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>>>() 24 } 25 26 #[component] 27 pub fn RepositoryIndex(ident: ReadSignal<AtIdentifier<'static>>) -> Element { 28 use crate::components::ProfileDisplay; 29 let notebooks = use_repo_notebook_context(); 30 rsx! { 31 document::Stylesheet { href: NOTEBOOK_CARD_CSS } 32 33 div { class: "repository-layout", 34 // Profile sidebar (desktop) / header (mobile) 35 aside { class: "repository-sidebar", 36 - ProfileDisplay { ident: ident() } 37 } 38 39 // Main content area ··· 41 div { class: "notebooks-list", 42 if let Some(notebooks) = notebooks { 43 match &*notebooks.read() { 44 - Some(Some(notebook_list)) => rsx! { 45 for notebook in notebook_list.iter() { 46 { 47 let view = &notebook.0; ··· 58 } 59 } 60 }, 61 - _ => rsx! { 62 div { "Loading notebooks..." } 63 } 64 }
··· 9 #[component] 10 pub fn Repository(ident: ReadSignal<AtIdentifier<'static>>) -> Element { 11 // Fetch notebooks for this specific DID with SSR support 12 + let notebooks = data::use_notebooks_for_did(ident)?; 13 use_context_provider(|| notebooks); 14 rsx! { 15 div { ··· 19 } 20 21 pub fn use_repo_notebook_context() 22 + -> Option<Memo<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>>> { 23 + try_use_context::<Memo<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>>>() 24 } 25 26 #[component] 27 pub fn RepositoryIndex(ident: ReadSignal<AtIdentifier<'static>>) -> Element { 28 use crate::components::ProfileDisplay; 29 let notebooks = use_repo_notebook_context(); 30 + let profile = crate::data::use_profile_data(ident)?; 31 rsx! { 32 document::Stylesheet { href: NOTEBOOK_CARD_CSS } 33 34 div { class: "repository-layout", 35 // Profile sidebar (desktop) / header (mobile) 36 aside { class: "repository-sidebar", 37 + ProfileDisplay { profile } 38 } 39 40 // Main content area ··· 42 div { class: "notebooks-list", 43 if let Some(notebooks) = notebooks { 44 match &*notebooks.read() { 45 + Some(notebook_list) => rsx! { 46 for notebook in notebook_list.iter() { 47 { 48 let view = &notebook.0; ··· 59 } 60 } 61 }, 62 + None => rsx! { 63 div { "Loading notebooks..." } 64 } 65 }
+30 -40
crates/weaver-app/src/components/profile.rs
··· 1 #![allow(non_snake_case)] 2 3 use crate::components::{ 4 BskyIcon, TangledIcon, 5 avatar::{Avatar, AvatarImage}, 6 identity::use_repo_notebook_context, 7 }; 8 use dioxus::prelude::*; 9 - use jacquard::types::ident::AtIdentifier; 10 - use weaver_api::sh_weaver::actor::ProfileDataViewInner; 11 12 const PROFILE_CSS: Asset = asset!("/assets/styling/profile.css"); 13 14 #[component] 15 - pub fn ProfileDisplay(ident: ReadSignal<AtIdentifier<'static>>) -> Element { 16 - // Fetch profile data 17 - let profile = crate::data::use_profile_data(ident()); 18 - 19 - match &*profile?.read() { 20 Some(profile_view) => { 21 - let profile_view = use_signal(|| profile_view.clone()); 22 rsx! { 23 document::Stylesheet { href: PROFILE_CSS } 24 25 div { class: "profile-display", 26 // Banner if present 27 - {match &profile_view.read().inner { 28 ProfileDataViewInner::ProfileView(p) => { 29 if let Some(ref banner) = p.banner { 30 rsx! { ··· 52 53 div { class: "profile-content", 54 // Avatar and identity 55 - ProfileIdentity { profile_view, ident } 56 div { 57 class: "profile-extras", 58 // Stats 59 - ProfileStats { ident } 60 61 // Links 62 - ProfileLinks { profile_view, ident } 63 } 64 65 ··· 76 } 77 78 #[component] 79 - fn ProfileIdentity( 80 - profile_view: ReadSignal<weaver_api::sh_weaver::actor::ProfileDataView<'static>>, 81 - ident: ReadSignal<AtIdentifier<'static>>, 82 - ) -> Element { 83 - match &profile_view.read().inner { 84 ProfileDataViewInner::ProfileView(profile) => { 85 let display_name = profile 86 .display_name ··· 171 div { class: "profile-identity", 172 div { class: "profile-name-section", 173 h1 { class: "profile-display-name", "@{profile.handle.as_ref()}" } 174 - div { class: "profile-handle", "{ident}" } 175 176 if let Some(ref location) = profile.location { 177 div { class: "profile-location", "{location}" } ··· 193 } 194 195 #[component] 196 - fn ProfileStats(ident: ReadSignal<AtIdentifier<'static>>) -> Element { 197 // Fetch notebook count 198 - let notebooks = use_repo_notebook_context(); 199 - 200 - let notebook_count = if let Some(notebooks) = notebooks { 201 - if let Some(Some(notebooks)) = &*notebooks.read() { 202 - notebooks.len() 203 - } else { 204 - 0 205 - } 206 } else { 207 0 208 }; ··· 218 } 219 220 #[component] 221 - fn ProfileLinks( 222 - profile_view: ReadSignal<weaver_api::sh_weaver::actor::ProfileDataView<'static>>, 223 - 224 - ident: ReadSignal<AtIdentifier<'static>>, 225 - ) -> Element { 226 - match &profile_view.read().inner { 227 ProfileDataViewInner::ProfileView(profile) => { 228 rsx! { 229 div { class: "profile-links", ··· 243 // Platform-specific links 244 if profile.bluesky.unwrap_or(false) { 245 a { 246 - href: "https://bsky.app/profile/{ident}", 247 target: "_blank", 248 rel: "noopener noreferrer", 249 class: "profile-link profile-link-platform", ··· 254 255 if profile.tangled.unwrap_or(false) { 256 a { 257 - href: "https://tangled.org/@{ident}", 258 target: "_blank", 259 rel: "noopener noreferrer", 260 class: "profile-link profile-link-platform", ··· 265 266 if profile.streamplace.unwrap_or(false) { 267 a { 268 - href: "https://stream.place/{ident}", 269 target: "_blank", 270 rel: "noopener noreferrer", 271 class: "profile-link profile-link-platform", ··· 275 } 276 } 277 } 278 - ProfileDataViewInner::ProfileViewDetailed(_profile) => { 279 // Bluesky ProfileViewDetailed - doesn't have weaver platform flags 280 rsx! { 281 div { class: "profile-links", 282 a { 283 - href: "https://bsky.app/profile/{ident}", 284 target: "_blank", 285 rel: "noopener noreferrer", 286 class: "profile-link profile-link-platform", ··· 306 } 307 } 308 a { 309 - href: "https://tangled.org/@{ident}", 310 target: "_blank", 311 rel: "noopener noreferrer", 312 class: "profile-link profile-link-platform", ··· 316 317 if profile.bluesky { 318 a { 319 - href: "https://bsky.app/profile/{ident}", 320 target: "_blank", 321 rel: "noopener noreferrer", 322 class: "profile-link profile-link-platform",
··· 1 #![allow(non_snake_case)] 2 3 + use std::sync::Arc; 4 + 5 use crate::components::{ 6 BskyIcon, TangledIcon, 7 avatar::{Avatar, AvatarImage}, 8 identity::use_repo_notebook_context, 9 }; 10 use dioxus::prelude::*; 11 + use weaver_api::com_atproto::repo::strong_ref::StrongRef; 12 + use weaver_api::sh_weaver::actor::{ProfileDataView, ProfileDataViewInner}; 13 + use weaver_common::agent::NotebookView; 14 15 const PROFILE_CSS: Asset = asset!("/assets/styling/profile.css"); 16 17 #[component] 18 + pub fn ProfileDisplay(profile: Memo<Option<ProfileDataView<'static>>>) -> Element { 19 + let notebooks = use_repo_notebook_context(); 20 + match &*profile.read() { 21 Some(profile_view) => { 22 + let profile_view = Arc::new(profile_view.clone()); 23 rsx! { 24 document::Stylesheet { href: PROFILE_CSS } 25 26 div { class: "profile-display", 27 // Banner if present 28 + {match &profile_view.inner { 29 ProfileDataViewInner::ProfileView(p) => { 30 if let Some(ref banner) = p.banner { 31 rsx! { ··· 53 54 div { class: "profile-content", 55 // Avatar and identity 56 + ProfileIdentity { profile_view: profile_view.clone() } 57 div { 58 class: "profile-extras", 59 // Stats 60 + ProfileStats { notebooks: notebooks.unwrap() } 61 62 // Links 63 + ProfileLinks { profile_view } 64 } 65 66 ··· 77 } 78 79 #[component] 80 + fn ProfileIdentity(profile_view: Arc<ProfileDataView<'static>>) -> Element { 81 + match &profile_view.inner { 82 ProfileDataViewInner::ProfileView(profile) => { 83 let display_name = profile 84 .display_name ··· 169 div { class: "profile-identity", 170 div { class: "profile-name-section", 171 h1 { class: "profile-display-name", "@{profile.handle.as_ref()}" } 172 + //div { class: "profile-handle", "{profile.handle.as_ref()}" } 173 174 if let Some(ref location) = profile.location { 175 div { class: "profile-location", "{location}" } ··· 191 } 192 193 #[component] 194 + fn ProfileStats( 195 + notebooks: Memo<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>>, 196 + ) -> Element { 197 // Fetch notebook count 198 + let notebook_count = if let Some(notebooks) = &*notebooks.read() { 199 + notebooks.len() 200 } else { 201 0 202 }; ··· 212 } 213 214 #[component] 215 + fn ProfileLinks(profile_view: Arc<ProfileDataView<'static>>) -> Element { 216 + match &profile_view.inner { 217 ProfileDataViewInner::ProfileView(profile) => { 218 rsx! { 219 div { class: "profile-links", ··· 233 // Platform-specific links 234 if profile.bluesky.unwrap_or(false) { 235 a { 236 + href: "https://bsky.app/profile/{profile.did}", 237 target: "_blank", 238 rel: "noopener noreferrer", 239 class: "profile-link profile-link-platform", ··· 244 245 if profile.tangled.unwrap_or(false) { 246 a { 247 + href: "https://tangled.org/{profile.did}", 248 target: "_blank", 249 rel: "noopener noreferrer", 250 class: "profile-link profile-link-platform", ··· 255 256 if profile.streamplace.unwrap_or(false) { 257 a { 258 + href: "https://stream.place/{profile.did}", 259 target: "_blank", 260 rel: "noopener noreferrer", 261 class: "profile-link profile-link-platform", ··· 265 } 266 } 267 } 268 + ProfileDataViewInner::ProfileViewDetailed(profile) => { 269 // Bluesky ProfileViewDetailed - doesn't have weaver platform flags 270 rsx! { 271 div { class: "profile-links", 272 a { 273 + href: "https://bsky.app/profile/{profile.did}", 274 target: "_blank", 275 rel: "noopener noreferrer", 276 class: "profile-link profile-link-platform", ··· 296 } 297 } 298 a { 299 + href: "https://tangled.org/{profile.did}", 300 target: "_blank", 301 rel: "noopener noreferrer", 302 class: "profile-link profile-link-platform", ··· 306 307 if profile.bluesky { 308 a { 309 + href: "https://bsky.app/profile/{profile.did}", 310 target: "_blank", 311 rel: "noopener noreferrer", 312 class: "profile-link profile-link-platform",
+77 -98
crates/weaver-app/src/data.rs
··· 21 smol_str::SmolStr, 22 types::{cid::Cid, string::AtIdentifier}, 23 }; 24 - use std::cell::Ref; 25 #[allow(unused_imports)] 26 use std::sync::Arc; 27 use weaver_api::com_atproto::repo::strong_ref::StrongRef; ··· 34 /// Fetches entry data with SSR support in fullstack mode. 35 #[cfg(feature = "fullstack-server")] 36 pub fn use_entry_data( 37 - ident: AtIdentifier<'static>, 38 - book_title: SmolStr, 39 - title: SmolStr, 40 ) -> Result<Memo<Option<(BookEntryView<'static>, Entry<'static>)>>, RenderError> { 41 let fetcher = use_context::<crate::fetch::Fetcher>(); 42 let fetcher = fetcher.clone(); 43 - let ident = use_signal(|| ident); 44 - let book_title = use_signal(|| book_title); 45 - let entry_title = use_signal(|| title); 46 let res = use_server_future(move || { 47 let fetcher = fetcher.clone(); 48 async move { 49 if let Some(entry) = fetcher 50 - .get_entry(ident(), book_title(), entry_title()) 51 .await 52 .ok() 53 .flatten() ··· 55 let (_book_entry_view, entry_record) = (&entry.0, &entry.1); 56 if let Some(embeds) = &entry_record.embeds { 57 if let Some(images) = &embeds.images { 58 - let ident = ident.clone(); 59 let images = images.clone(); 60 for image in &images.images { 61 use jacquard::smol_str::ToSmolStr; 62 63 let cid = image.image.blob().cid(); 64 cache_blob( 65 - ident.to_smolstr(), 66 cid.to_smolstr(), 67 image.name.as_ref().map(|n| n.to_smolstr()), 68 ) ··· 79 None 80 } 81 } 82 - }); 83 - res.map(|r| { 84 - use_memo(move || { 85 - if let Some(Some((ev, e))) = &*r.read_unchecked() { 86 - use jacquard::from_json_value; 87 88 - let book_entry = from_json_value::<BookEntryView>(ev.clone()).unwrap(); 89 - let entry = from_json_value::<Entry>(e.clone()).unwrap(); 90 91 - Some((book_entry, entry)) 92 - } else { 93 - None 94 - } 95 - }) 96 - }) 97 } 98 /// Fetches entry data client-side only (no SSR). 99 #[cfg(not(feature = "fullstack-server"))] 100 pub fn use_entry_data( 101 - ident: AtIdentifier<'static>, 102 - book_title: SmolStr, 103 - title: SmolStr, 104 ) -> Result<Memo<Option<(BookEntryView<'static>, Entry<'static>)>>, RenderError> { 105 let fetcher = use_context::<crate::fetch::Fetcher>(); 106 let fetcher = fetcher.clone(); 107 - let ident = use_signal(|| ident); 108 - let book_title = use_signal(|| book_title); 109 - let entry_title = use_signal(|| title); 110 let res = use_resource(move || { 111 let fetcher = fetcher.clone(); 112 async move { 113 fetcher 114 - .get_entry(ident(), book_title(), entry_title()) 115 .await 116 .ok() 117 .flatten() 118 .map(|arc| { 119 ( 120 - serde_json::to_value(entry.0.clone()).unwrap(), 121 - serde_json::to_value(entry.1.clone()).unwrap(), 122 ) 123 }) 124 } 125 }); 126 - res.map(|r| { 127 - use_memo(move || { 128 - if let Some(Some((ev, e))) = &*r.read_unchecked() { 129 - use jacquard::from_json_value; 130 131 - let book_entry = from_json_value::<BookEntryView>(ev.clone()).unwrap(); 132 - let entry = from_json_value::<Entry>(e.clone()).unwrap(); 133 134 - Some((book_entry, entry)) 135 - } else { 136 - None 137 - } 138 - }) 139 - }) 140 } 141 142 - pub fn get_handle(did: Did<'static>) -> AtIdentifier<'static> { 143 - let ident = AtIdentifier::Did(did); 144 - use_handle(ident.clone()) 145 .read() 146 .as_ref() 147 - .unwrap_or(&Ok(ident)) 148 .as_ref() 149 .unwrap() 150 .clone() 151 } 152 153 pub fn use_handle( 154 - ident: AtIdentifier<'static>, 155 ) -> Resource<Result<AtIdentifier<'static>, IdentityError>> { 156 let fetcher = use_context::<crate::fetch::Fetcher>(); 157 let fetcher = fetcher.clone(); 158 - let ident = use_signal(|| ident); 159 - 160 use_resource(move || { 161 let client = fetcher.get_client(); 162 async move { ··· 184 185 pub fn use_notebook_handle(ident: Signal<Option<AtIdentifier<'static>>>) -> NotebookHandle { 186 let ident = if let Some(ident) = &*ident.read() { 187 - if let Some(Ok(handle)) = &*use_handle(ident.clone()).read() { 188 Some(handle.clone()) 189 } else { 190 Some(ident.clone()) ··· 195 use_context_provider(|| NotebookHandle(Arc::new(ident))) 196 } 197 198 - /// Hook to render markdown client-side only (no SSR). 199 #[cfg(feature = "fullstack-server")] 200 pub fn use_rendered_markdown( 201 - content: Entry<'static>, 202 - ident: AtIdentifier<'static>, 203 ) -> Result<Resource<Option<String>>, RenderError> { 204 - let ident = use_signal(|| ident); 205 - let content = use_signal(|| content); 206 let fetcher = use_context::<crate::fetch::Fetcher>(); 207 Ok(use_server_future(move || { 208 let client = fetcher.get_client(); ··· 219 /// Hook to render markdown client-side only (no SSR). 220 #[cfg(not(feature = "fullstack-server"))] 221 pub fn use_rendered_markdown( 222 - content: Entry<'static>, 223 - ident: AtIdentifier<'static>, 224 ) -> Result<Resource<Option<String>>, RenderError> { 225 - let ident = use_signal(|| ident); 226 - let content = use_signal(|| content); 227 let fetcher = use_context::<crate::fetch::Fetcher>(); 228 Ok(use_resource(move || { 229 let client = fetcher.get_client(); ··· 261 /// Fetches profile data for a given identifier 262 #[cfg(feature = "fullstack-server")] 263 pub fn use_profile_data( 264 - ident: AtIdentifier<'static>, 265 ) -> Result<Memo<Option<ProfileDataView<'static>>>, RenderError> { 266 let fetcher = use_context::<crate::fetch::Fetcher>(); 267 - let ident = use_signal(|| ident); 268 let res = use_server_future(move || { 269 let fetcher = fetcher.clone(); 270 async move { ··· 277 } 278 })?; 279 Ok(use_memo(move || { 280 - if let Some(Some(value)) = &*res.read_unchecked() { 281 jacquard::from_json_value::<ProfileDataView>(value.clone()).ok() 282 } else { 283 None ··· 288 /// Fetches profile data client-side only (no SSR) 289 #[cfg(not(feature = "fullstack-server"))] 290 pub fn use_profile_data( 291 - ident: AtIdentifier<'static>, 292 ) -> Result<Memo<Option<ProfileDataView<'static>>>, RenderError> { 293 let fetcher = use_context::<crate::fetch::Fetcher>(); 294 - let ident = use_signal(|| ident); 295 let res = use_resource(move || { 296 let fetcher = fetcher.clone(); 297 async move { ··· 303 .flatten() 304 } 305 }); 306 Ok(use_memo(move || { 307 - if let Some(Some(value)) = &*res.read_unchecked() { 308 jacquard::from_json_value::<ProfileDataView>(value.clone()).ok() 309 } else { 310 None ··· 315 /// Fetches notebooks for a specific DID 316 #[cfg(feature = "fullstack-server")] 317 pub fn use_notebooks_for_did( 318 - ident: AtIdentifier<'static>, 319 ) -> Result<Memo<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>>, RenderError> { 320 let fetcher = use_context::<crate::fetch::Fetcher>(); 321 - let ident = use_signal(|| ident); 322 let res = use_server_future(move || { 323 let fetcher = fetcher.clone(); 324 async move { ··· 336 } 337 })?; 338 Ok(use_memo(move || { 339 - if let Some(Some(values)) = &*res.read_unchecked() { 340 values 341 .iter() 342 .map(|v| { ··· 352 /// Fetches notebooks client-side only (no SSR) 353 #[cfg(not(feature = "fullstack-server"))] 354 pub fn use_notebooks_for_did( 355 - ident: AtIdentifier<'static>, 356 ) -> Result<Memo<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>>, RenderError> { 357 let fetcher = use_context::<crate::fetch::Fetcher>(); 358 - let ident = use_signal(|| ident); 359 let res = use_resource(move || { 360 let fetcher = fetcher.clone(); 361 async move { ··· 372 .flatten() 373 } 374 }); 375 Ok(use_memo(move || { 376 - if let Some(Some(values)) = &*res.read_unchecked() { 377 values 378 .iter() 379 .map(|v| { ··· 408 } 409 })?; 410 Ok(use_memo(move || { 411 - if let Some(Some(values)) = &*res.read_unchecked() { 412 values 413 .iter() 414 .map(|v| { ··· 442 .flatten() 443 } 444 }); 445 Ok(use_memo(move || { 446 - if let Some(Some(values)) = &*res.read_unchecked() { 447 values 448 .iter() 449 .map(|v| { ··· 459 /// Fetches notebook metadata with SSR support in fullstack mode 460 #[cfg(feature = "fullstack-server")] 461 pub fn use_notebook( 462 - ident: AtIdentifier<'static>, 463 - book_title: SmolStr, 464 ) -> Result<Memo<Option<(NotebookView<'static>, Vec<StrongRef<'static>>)>>, RenderError> { 465 let fetcher = use_context::<crate::fetch::Fetcher>(); 466 - let ident = use_signal(|| ident); 467 - let book_title = use_signal(|| book_title); 468 let res = use_server_future(move || { 469 let fetcher = fetcher.clone(); 470 async move { ··· 478 } 479 })?; 480 Ok(use_memo(move || { 481 - if let Some(Some(value)) = &*res.read_unchecked() { 482 jacquard::from_json_value::<(NotebookView, Vec<StrongRef>)>(value.clone()).ok() 483 } else { 484 None ··· 489 /// Fetches notebook metadata client-side only (no SSR) 490 #[cfg(not(feature = "fullstack-server"))] 491 pub fn use_notebook( 492 - ident: AtIdentifier<'static>, 493 - book_title: SmolStr, 494 ) -> Result<Memo<Option<(NotebookView<'static>, Vec<StrongRef<'static>>)>>, RenderError> { 495 let fetcher = use_context::<crate::fetch::Fetcher>(); 496 - let ident = use_signal(|| ident); 497 - let book_title = use_signal(|| book_title); 498 let res = use_resource(move || { 499 let fetcher = fetcher.clone(); 500 async move { ··· 507 .flatten() 508 } 509 }); 510 Ok(use_memo(move || { 511 - if let Some(Some(value)) = &*res.read_unchecked() { 512 jacquard::from_json_value::<(NotebookView, Vec<StrongRef>)>(value.clone()).ok() 513 } else { 514 None ··· 519 /// Fetches notebook entries with SSR support in fullstack mode 520 #[cfg(feature = "fullstack-server")] 521 pub fn use_notebook_entries( 522 - ident: AtIdentifier<'static>, 523 - book_title: SmolStr, 524 ) -> Result<Memo<Option<Vec<BookEntryView<'static>>>>, RenderError> { 525 let fetcher = use_context::<crate::fetch::Fetcher>(); 526 - let ident = use_signal(|| ident); 527 - let book_title = use_signal(|| book_title); 528 let res = use_server_future(move || { 529 let fetcher = fetcher.clone(); 530 async move { ··· 542 .flatten() 543 } 544 })?; 545 Ok(use_memo(move || { 546 - if let Some(Some(values)) = &*res.read_unchecked() { 547 values 548 .iter() 549 .map(|v| jacquard::from_json_value::<BookEntryView>(v.clone()).ok()) ··· 557 /// Fetches notebook entries client-side only (no SSR) 558 #[cfg(not(feature = "fullstack-server"))] 559 pub fn use_notebook_entries( 560 - ident: AtIdentifier<'static>, 561 - book_title: SmolStr, 562 ) -> Result<Memo<Option<Vec<BookEntryView<'static>>>>, RenderError> { 563 let fetcher = use_context::<crate::fetch::Fetcher>(); 564 - let r = use_resource(use_reactive!(|(ident, book_title)| { 565 let fetcher = fetcher.clone(); 566 async move { 567 fetcher 568 - .list_notebook_entries(ident, book_title) 569 .await 570 .ok() 571 .flatten() 572 } 573 - })); 574 - Ok(use_memo(move || { 575 - r.read_unchecked().as_ref().and_then(|v| v.clone()) 576 - })) 577 } 578 579 #[cfg(feature = "fullstack-server")]
··· 21 smol_str::SmolStr, 22 types::{cid::Cid, string::AtIdentifier}, 23 }; 24 #[allow(unused_imports)] 25 use std::sync::Arc; 26 use weaver_api::com_atproto::repo::strong_ref::StrongRef; ··· 33 /// Fetches entry data with SSR support in fullstack mode. 34 #[cfg(feature = "fullstack-server")] 35 pub fn use_entry_data( 36 + ident: ReadSignal<AtIdentifier<'static>>, 37 + book_title: ReadSignal<SmolStr>, 38 + title: ReadSignal<SmolStr>, 39 ) -> Result<Memo<Option<(BookEntryView<'static>, Entry<'static>)>>, RenderError> { 40 let fetcher = use_context::<crate::fetch::Fetcher>(); 41 let fetcher = fetcher.clone(); 42 let res = use_server_future(move || { 43 let fetcher = fetcher.clone(); 44 async move { 45 if let Some(entry) = fetcher 46 + .get_entry(ident(), book_title(), title()) 47 .await 48 .ok() 49 .flatten() ··· 51 let (_book_entry_view, entry_record) = (&entry.0, &entry.1); 52 if let Some(embeds) = &entry_record.embeds { 53 if let Some(images) = &embeds.images { 54 + let ident_val = ident(); 55 let images = images.clone(); 56 for image in &images.images { 57 use jacquard::smol_str::ToSmolStr; 58 59 let cid = image.image.blob().cid(); 60 cache_blob( 61 + ident_val.to_smolstr(), 62 cid.to_smolstr(), 63 image.name.as_ref().map(|n| n.to_smolstr()), 64 ) ··· 75 None 76 } 77 } 78 + })?; // Handle the Result first to avoid calling use_memo in a closure 79 + Ok(use_memo(move || { 80 + if let Some(Some((ev, e))) = &*res.read() { 81 + use jacquard::from_json_value; 82 83 + let book_entry = from_json_value::<BookEntryView>(ev.clone()).unwrap(); 84 + let entry = from_json_value::<Entry>(e.clone()).unwrap(); 85 86 + Some((book_entry, entry)) 87 + } else { 88 + None 89 + } 90 + })) 91 } 92 /// Fetches entry data client-side only (no SSR). 93 #[cfg(not(feature = "fullstack-server"))] 94 pub fn use_entry_data( 95 + ident: ReadSignal<AtIdentifier<'static>>, 96 + book_title: ReadSignal<SmolStr>, 97 + title: ReadSignal<SmolStr>, 98 ) -> Result<Memo<Option<(BookEntryView<'static>, Entry<'static>)>>, RenderError> { 99 let fetcher = use_context::<crate::fetch::Fetcher>(); 100 let fetcher = fetcher.clone(); 101 let res = use_resource(move || { 102 let fetcher = fetcher.clone(); 103 async move { 104 fetcher 105 + .get_entry(ident(), book_title(), title()) 106 .await 107 .ok() 108 .flatten() 109 .map(|arc| { 110 ( 111 + serde_json::to_value(arc.0.clone()).unwrap(), 112 + serde_json::to_value(arc.1.clone()).unwrap(), 113 ) 114 }) 115 } 116 }); 117 + res.suspend()?; 118 + Ok(use_memo(move || { 119 + if let Some(Some((ev, e))) = &*res.read() { 120 + use jacquard::from_json_value; 121 122 + let book_entry = from_json_value::<BookEntryView>(ev.clone()).unwrap(); 123 + let entry = from_json_value::<Entry>(e.clone()).unwrap(); 124 125 + Some((book_entry, entry)) 126 + } else { 127 + None 128 + } 129 + })) 130 } 131 132 + pub fn use_get_handle(did: Did<'static>) -> AtIdentifier<'static> { 133 + let ident = use_signal(use_reactive!(|did| AtIdentifier::Did(did.clone()))); 134 + use_handle(ident.into()) 135 .read() 136 .as_ref() 137 + .unwrap_or(&Ok(ident.read().clone())) 138 .as_ref() 139 .unwrap() 140 .clone() 141 } 142 143 pub fn use_handle( 144 + ident: ReadSignal<AtIdentifier<'static>>, 145 ) -> Resource<Result<AtIdentifier<'static>, IdentityError>> { 146 let fetcher = use_context::<crate::fetch::Fetcher>(); 147 let fetcher = fetcher.clone(); 148 use_resource(move || { 149 let client = fetcher.get_client(); 150 async move { ··· 172 173 pub fn use_notebook_handle(ident: Signal<Option<AtIdentifier<'static>>>) -> NotebookHandle { 174 let ident = if let Some(ident) = &*ident.read() { 175 + let _ident = Signal::new(ident.clone()); 176 + if let Some(Ok(handle)) = &*use_handle(_ident.into()).read() { 177 Some(handle.clone()) 178 } else { 179 Some(ident.clone()) ··· 184 use_context_provider(|| NotebookHandle(Arc::new(ident))) 185 } 186 187 + /// Hook to render markdown with SSR support. 188 #[cfg(feature = "fullstack-server")] 189 pub fn use_rendered_markdown( 190 + content: ReadSignal<Entry<'static>>, 191 + ident: ReadSignal<AtIdentifier<'static>>, 192 ) -> Result<Resource<Option<String>>, RenderError> { 193 let fetcher = use_context::<crate::fetch::Fetcher>(); 194 Ok(use_server_future(move || { 195 let client = fetcher.get_client(); ··· 206 /// Hook to render markdown client-side only (no SSR). 207 #[cfg(not(feature = "fullstack-server"))] 208 pub fn use_rendered_markdown( 209 + content: ReadSignal<Entry<'static>>, 210 + ident: ReadSignal<AtIdentifier<'static>>, 211 ) -> Result<Resource<Option<String>>, RenderError> { 212 let fetcher = use_context::<crate::fetch::Fetcher>(); 213 Ok(use_resource(move || { 214 let client = fetcher.get_client(); ··· 246 /// Fetches profile data for a given identifier 247 #[cfg(feature = "fullstack-server")] 248 pub fn use_profile_data( 249 + ident: ReadSignal<AtIdentifier<'static>>, 250 ) -> Result<Memo<Option<ProfileDataView<'static>>>, RenderError> { 251 let fetcher = use_context::<crate::fetch::Fetcher>(); 252 let res = use_server_future(move || { 253 let fetcher = fetcher.clone(); 254 async move { ··· 261 } 262 })?; 263 Ok(use_memo(move || { 264 + if let Some(Some(value)) = &*res.read() { 265 jacquard::from_json_value::<ProfileDataView>(value.clone()).ok() 266 } else { 267 None ··· 272 /// Fetches profile data client-side only (no SSR) 273 #[cfg(not(feature = "fullstack-server"))] 274 pub fn use_profile_data( 275 + ident: ReadSignal<AtIdentifier<'static>>, 276 ) -> Result<Memo<Option<ProfileDataView<'static>>>, RenderError> { 277 let fetcher = use_context::<crate::fetch::Fetcher>(); 278 let res = use_resource(move || { 279 let fetcher = fetcher.clone(); 280 async move { ··· 286 .flatten() 287 } 288 }); 289 + res.suspend()?; 290 Ok(use_memo(move || { 291 + if let Some(Some(value)) = &*res.read() { 292 jacquard::from_json_value::<ProfileDataView>(value.clone()).ok() 293 } else { 294 None ··· 299 /// Fetches notebooks for a specific DID 300 #[cfg(feature = "fullstack-server")] 301 pub fn use_notebooks_for_did( 302 + ident: ReadSignal<AtIdentifier<'static>>, 303 ) -> Result<Memo<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>>, RenderError> { 304 let fetcher = use_context::<crate::fetch::Fetcher>(); 305 let res = use_server_future(move || { 306 let fetcher = fetcher.clone(); 307 async move { ··· 319 } 320 })?; 321 Ok(use_memo(move || { 322 + if let Some(Some(values)) = &*res.read() { 323 values 324 .iter() 325 .map(|v| { ··· 335 /// Fetches notebooks client-side only (no SSR) 336 #[cfg(not(feature = "fullstack-server"))] 337 pub fn use_notebooks_for_did( 338 + ident: ReadSignal<AtIdentifier<'static>>, 339 ) -> Result<Memo<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>>, RenderError> { 340 let fetcher = use_context::<crate::fetch::Fetcher>(); 341 let res = use_resource(move || { 342 let fetcher = fetcher.clone(); 343 async move { ··· 354 .flatten() 355 } 356 }); 357 + res.suspend()?; 358 Ok(use_memo(move || { 359 + if let Some(Some(values)) = &*res.read() { 360 values 361 .iter() 362 .map(|v| { ··· 391 } 392 })?; 393 Ok(use_memo(move || { 394 + if let Some(Some(values)) = &*res.read() { 395 values 396 .iter() 397 .map(|v| { ··· 425 .flatten() 426 } 427 }); 428 + res.suspend()?; 429 Ok(use_memo(move || { 430 + if let Some(Some(values)) = &*res.read() { 431 values 432 .iter() 433 .map(|v| { ··· 443 /// Fetches notebook metadata with SSR support in fullstack mode 444 #[cfg(feature = "fullstack-server")] 445 pub fn use_notebook( 446 + ident: ReadSignal<AtIdentifier<'static>>, 447 + book_title: ReadSignal<SmolStr>, 448 ) -> Result<Memo<Option<(NotebookView<'static>, Vec<StrongRef<'static>>)>>, RenderError> { 449 let fetcher = use_context::<crate::fetch::Fetcher>(); 450 let res = use_server_future(move || { 451 let fetcher = fetcher.clone(); 452 async move { ··· 460 } 461 })?; 462 Ok(use_memo(move || { 463 + if let Some(Some(value)) = &*res.read() { 464 jacquard::from_json_value::<(NotebookView, Vec<StrongRef>)>(value.clone()).ok() 465 } else { 466 None ··· 471 /// Fetches notebook metadata client-side only (no SSR) 472 #[cfg(not(feature = "fullstack-server"))] 473 pub fn use_notebook( 474 + ident: ReadSignal<AtIdentifier<'static>>, 475 + book_title: ReadSignal<SmolStr>, 476 ) -> Result<Memo<Option<(NotebookView<'static>, Vec<StrongRef<'static>>)>>, RenderError> { 477 let fetcher = use_context::<crate::fetch::Fetcher>(); 478 let res = use_resource(move || { 479 let fetcher = fetcher.clone(); 480 async move { ··· 487 .flatten() 488 } 489 }); 490 + res.suspend()?; 491 Ok(use_memo(move || { 492 + if let Some(Some(value)) = &*res.read() { 493 jacquard::from_json_value::<(NotebookView, Vec<StrongRef>)>(value.clone()).ok() 494 } else { 495 None ··· 500 /// Fetches notebook entries with SSR support in fullstack mode 501 #[cfg(feature = "fullstack-server")] 502 pub fn use_notebook_entries( 503 + ident: ReadSignal<AtIdentifier<'static>>, 504 + book_title: ReadSignal<SmolStr>, 505 ) -> Result<Memo<Option<Vec<BookEntryView<'static>>>>, RenderError> { 506 let fetcher = use_context::<crate::fetch::Fetcher>(); 507 let res = use_server_future(move || { 508 let fetcher = fetcher.clone(); 509 async move { ··· 521 .flatten() 522 } 523 })?; 524 + res.suspend()?; 525 Ok(use_memo(move || { 526 + if let Some(Some(values)) = &*res.read() { 527 values 528 .iter() 529 .map(|v| jacquard::from_json_value::<BookEntryView>(v.clone()).ok()) ··· 537 /// Fetches notebook entries client-side only (no SSR) 538 #[cfg(not(feature = "fullstack-server"))] 539 pub fn use_notebook_entries( 540 + ident: ReadSignal<AtIdentifier<'static>>, 541 + book_title: ReadSignal<SmolStr>, 542 ) -> Result<Memo<Option<Vec<BookEntryView<'static>>>>, RenderError> { 543 let fetcher = use_context::<crate::fetch::Fetcher>(); 544 + let r = use_resource(move || { 545 let fetcher = fetcher.clone(); 546 async move { 547 fetcher 548 + .list_notebook_entries(ident(), book_title()) 549 .await 550 .ok() 551 .flatten() 552 } 553 + }); 554 + r.suspend()?; 555 + Ok(use_memo(move || r.read().as_ref().and_then(|v| v.clone()))) 556 } 557 558 #[cfg(feature = "fullstack-server")]
+40 -28
crates/weaver-app/src/views/navbar.rs
··· 1 use crate::Route; 2 use crate::components::button::{Button, ButtonVariant}; 3 use crate::components::login::LoginModal; 4 - use crate::data::{get_handle, use_notebook_handle}; 5 use crate::fetch::Fetcher; 6 use dioxus::prelude::*; 7 8 const THEME_DEFAULTS_CSS: Asset = asset!("/assets/styling/theme-defaults.css"); 9 const NAVBAR_CSS: Asset = asset!("/assets/styling/navbar.css"); ··· 16 #[component] 17 pub fn Navbar() -> Element { 18 let route = use_route::<Route>(); 19 - let mut auth_state = use_context::<Signal<crate::auth::AuthState>>(); 20 - let mut show_login_modal = use_signal(|| false); 21 - let fetcher = use_context::<Fetcher>(); 22 let route_handle = use_signal(|| match &route { 23 Route::EntryPage { ident, .. } => Some(ident.clone()), 24 Route::RepositoryIndex { ident } => Some(ident.clone()), ··· 82 _ => rsx! {} 83 } 84 } 85 - if auth_state.read().is_authenticated() { 86 - if let Some(did) = &auth_state.read().did { 87 - Button { 88 - variant: ButtonVariant::Ghost, 89 - onclick: move |_| { 90 - let fetcher = fetcher.clone(); 91 - auth_state.write().clear(); 92 - async move { 93 - fetcher.downgrade_to_unauthenticated().await; 94 - } 95 - }, 96 - span { class: "auth-handle", "@{get_handle(did.clone())}" } 97 - } 98 - } 99 100 - } else { 101 - div { 102 - class: "auth-button", 103 - Button { 104 - variant: ButtonVariant::Ghost, 105 - onclick: move |_| show_login_modal.set(true), 106 - span { class: "auth-handle", "Sign In" } 107 } 108 } 109 - 110 } 111 LoginModal { 112 open: show_login_modal 113 } 114 } 115 - 116 - Outlet::<Route> {} 117 } 118 }
··· 1 use crate::Route; 2 use crate::components::button::{Button, ButtonVariant}; 3 use crate::components::login::LoginModal; 4 + use crate::data::{use_get_handle, use_notebook_handle}; 5 use crate::fetch::Fetcher; 6 use dioxus::prelude::*; 7 + use jacquard::types::did::Did; 8 9 const THEME_DEFAULTS_CSS: Asset = asset!("/assets/styling/theme-defaults.css"); 10 const NAVBAR_CSS: Asset = asset!("/assets/styling/navbar.css"); ··· 17 #[component] 18 pub fn Navbar() -> Element { 19 let route = use_route::<Route>(); 20 + 21 let route_handle = use_signal(|| match &route { 22 Route::EntryPage { ident, .. } => Some(ident.clone()), 23 Route::RepositoryIndex { ident } => Some(ident.clone()), ··· 81 _ => rsx! {} 82 } 83 } 84 + LoginButton {} 85 + 86 + } 87 + 88 + Outlet::<Route> {} 89 + } 90 + } 91 92 + #[component] 93 + fn LoginButton() -> Element { 94 + let fetcher = use_context::<Fetcher>(); 95 + let mut show_login_modal = use_signal(|| false); 96 + let mut auth_state = use_context::<Signal<crate::auth::AuthState>>(); 97 + let did_signal = use_signal(|| auth_state.read().did.clone()); 98 + if let Some(did) = &*did_signal.read() { 99 + rsx! { 100 + Button { 101 + variant: ButtonVariant::Ghost, 102 + onclick: move |_| { 103 + let fetcher = fetcher.clone(); 104 + auth_state.write().clear(); 105 + async move { 106 + fetcher.downgrade_to_unauthenticated().await; 107 } 108 + }, 109 + span { class: "auth-handle", "@{use_get_handle(did.clone())}" } 110 + } 111 + LoginModal { 112 + open: show_login_modal 113 + } 114 + } 115 + } else { 116 + rsx! { 117 + div { 118 + class: "auth-button", 119 + Button { 120 + variant: ButtonVariant::Ghost, 121 + onclick: move |_| show_login_modal.set(true), 122 + span { class: "auth-handle", "Sign In" } 123 } 124 } 125 LoginModal { 126 open: show_login_modal 127 } 128 } 129 } 130 }
+6 -3
crates/weaver-app/src/views/notebook.rs
··· 29 book_title: ReadSignal<SmolStr>, 30 ) -> Element { 31 // Fetch full notebook metadata with SSR support 32 - let notebook_data = data::use_notebook(ident(), book_title())?; 33 34 - // Fetch entries with SSR support 35 - let entries_resource = data::use_notebook_entries(ident(), book_title())?; 36 37 rsx! { 38 document::Link { rel: "stylesheet", href: ENTRY_CARD_CSS }
··· 29 book_title: ReadSignal<SmolStr>, 30 ) -> Element { 31 // Fetch full notebook metadata with SSR support 32 + // IMPORTANT: Call ALL hooks before any ? early returns to maintain hook order 33 + let notebook_data = data::use_notebook(ident, book_title); 34 + let entries_resource = data::use_notebook_entries(ident, book_title); 35 36 + // Now check for errors 37 + let notebook_data = notebook_data?; 38 + let entries_resource = entries_resource?; 39 40 rsx! { 41 document::Link { rel: "stylesheet", href: ENTRY_CARD_CSS }