hmmm x 2

Orual 27f77851 94b6302a

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