at main 2233 lines 80 kB view raw
1//! Feature-gated data fetching layer that abstracts over SSR and client-only modes. 2//! 3//! In fullstack-server mode, hooks use `use_server_future` with inline async closures. 4//! In client-only mode, hooks use `use_resource` with context-provided fetchers. 5 6use crate::auth::AuthState; 7#[cfg(feature = "server")] 8use crate::blobcache::BlobCache; 9use dioxus::prelude::*; 10#[cfg(feature = "fullstack-server")] 11#[allow(unused_imports)] 12use dioxus::{CapturedError, fullstack::extract::Extension}; 13use jacquard::{ 14 IntoStatic, 15 types::{aturi::AtUri, did::Did, string::Handle}, 16}; 17#[allow(unused_imports)] 18use jacquard::{ 19 client::AgentSessionExt, 20 identity::resolver::IdentityError, 21 prelude::IdentityResolver, 22 smol_str::{SmolStr, format_smolstr}, 23 types::{cid::Cid, string::AtIdentifier}, 24}; 25#[allow(unused_imports)] 26use std::sync::Arc; 27use weaver_api::com_atproto::repo::strong_ref::StrongRef; 28use weaver_api::sh_weaver::actor::ProfileDataView; 29use weaver_api::sh_weaver::notebook::{BookEntryView, EntryView, NotebookView, entry::Entry}; 30use weaver_common::ResolvedContent; 31// ============================================================================ 32// Wrapper Hooks (feature-gated) 33// ============================================================================ 34 35/// Fetches entry data with SSR support in fullstack mode. 36#[cfg(feature = "fullstack-server")] 37pub fn use_entry_data( 38 ident: ReadSignal<AtIdentifier<'static>>, 39 book_title: ReadSignal<SmolStr>, 40 title: ReadSignal<SmolStr>, 41) -> ( 42 Result<Resource<Option<(serde_json::Value, serde_json::Value)>>, RenderError>, 43 Memo<Option<(BookEntryView<'static>, Entry<'static>)>>, 44) { 45 let fetcher = use_context::<crate::fetch::Fetcher>(); 46 let fetcher = fetcher.clone(); 47 let res = use_server_future(use_reactive!(|(ident, book_title, title)| { 48 let fetcher = fetcher.clone(); 49 async move { 50 let fetch_result = fetcher.get_entry(ident(), book_title(), title()).await; 51 52 match fetch_result { 53 Ok(Some(entry)) => { 54 let (_book_entry_view, entry_record) = (&entry.0, &entry.1); 55 if let Some(embeds) = &entry_record.embeds { 56 if let Some(images) = &embeds.images { 57 let ident_val = ident.clone(); 58 let images = images.clone(); 59 for image in &images.images { 60 use jacquard::smol_str::ToSmolStr; 61 62 let cid = image.image.blob().cid(); 63 cache_blob( 64 ident_val.to_smolstr(), 65 cid.to_smolstr(), 66 image.name.as_ref().map(|n| n.to_smolstr()), 67 ) 68 .await 69 .ok(); 70 } 71 } 72 } 73 Some(( 74 serde_json::to_value(entry.0.clone()).unwrap(), 75 serde_json::to_value(entry.1.clone()).unwrap(), 76 )) 77 } 78 Ok(None) => None, 79 Err(e) => { 80 tracing::error!( 81 "[use_entry_data] fetch error for {}/{}/{}: {:?}", 82 ident(), 83 book_title(), 84 title(), 85 e 86 ); 87 None 88 } 89 } 90 } 91 })); 92 let memo = use_memo(use_reactive!(|res| { 93 let res = res.as_ref().ok()?; 94 if let Some(Some((ev, e))) = &*res.read() { 95 use jacquard::from_json_value; 96 97 let book_entry = from_json_value::<BookEntryView>(ev.clone()).unwrap(); 98 let entry = from_json_value::<Entry>(e.clone()).unwrap(); 99 Some((book_entry, entry)) 100 } else { 101 None 102 } 103 })); 104 (res, memo) 105} 106/// Fetches entry data client-side only (no SSR). 107#[cfg(not(feature = "fullstack-server"))] 108pub fn use_entry_data( 109 ident: ReadSignal<AtIdentifier<'static>>, 110 book_title: ReadSignal<SmolStr>, 111 title: ReadSignal<SmolStr>, 112) -> ( 113 Resource<Option<(BookEntryView<'static>, Entry<'static>)>>, 114 Memo<Option<(BookEntryView<'static>, Entry<'static>)>>, 115) { 116 let fetcher = use_context::<crate::fetch::Fetcher>(); 117 let fetcher = fetcher.clone(); 118 let res = use_resource(move || { 119 let fetcher = fetcher.clone(); 120 async move { 121 if let Some(entry) = fetcher 122 .get_entry(ident(), book_title(), title()) 123 .await 124 .ok() 125 .flatten() 126 { 127 let (_book_entry_view, entry_record) = (&entry.0, &entry.1); 128 if let Some(embeds) = &entry_record.embeds { 129 if let Some(images) = &embeds.images { 130 #[cfg(all(target_family = "wasm", target_os = "unknown",))] 131 { 132 let _ = crate::service_worker::register_entry_blobs( 133 &ident(), 134 book_title().as_str(), 135 images, 136 &fetcher, 137 ) 138 .await; 139 } 140 } 141 } 142 Some((entry.0.clone(), entry.1.clone())) 143 } else { 144 None 145 } 146 } 147 }); 148 let memo = use_memo(move || res.read().clone().flatten()); 149 (res, memo) 150} 151 152#[cfg(feature = "fullstack-server")] 153pub fn use_get_handle(did: Did<'static>) -> Memo<AtIdentifier<'static>> { 154 let ident = use_signal(use_reactive!(|did| AtIdentifier::Did(did.clone()))); 155 let old_ident = ident.read().clone(); 156 let fetcher = use_context::<crate::fetch::Fetcher>(); 157 let fetcher = fetcher.clone(); 158 let res = use_resource(move || { 159 let client = fetcher.get_client(); 160 let old_ident = old_ident.clone(); 161 async move { 162 client 163 .resolve_ident_owned(&*ident.read()) 164 .await 165 .map(|doc| { 166 doc.handles() 167 .first() 168 .map(|h| AtIdentifier::Handle(h.clone()).into_static()) 169 }) 170 .ok() 171 .flatten() 172 .unwrap_or(old_ident) 173 } 174 }); 175 use_memo(move || { 176 if let Some(value) = &*res.read() { 177 value.clone() 178 } else { 179 ident.read().clone() 180 } 181 }) 182} 183 184#[cfg(not(feature = "fullstack-server"))] 185pub fn use_get_handle(did: Did<'static>) -> Memo<AtIdentifier<'static>> { 186 let ident = use_signal(use_reactive!(|did| AtIdentifier::Did(did.clone()))); 187 let old_ident = ident.read().clone(); 188 let fetcher = use_context::<crate::fetch::Fetcher>(); 189 let fetcher = fetcher.clone(); 190 let res = use_resource(move || { 191 let client = fetcher.get_client(); 192 let old_ident = old_ident.clone(); 193 async move { 194 client 195 .resolve_ident_owned(&*ident.read()) 196 .await 197 .map(|doc| { 198 doc.handles() 199 .first() 200 .map(|h| AtIdentifier::Handle(h.clone()).into_static()) 201 }) 202 .ok() 203 .flatten() 204 .unwrap_or(old_ident) 205 } 206 }); 207 use_memo(move || { 208 if let Some(value) = &*res.read() { 209 value.clone() 210 } else { 211 ident.read().clone() 212 } 213 }) 214} 215 216#[cfg(feature = "fullstack-server")] 217pub fn use_load_handle( 218 ident: Option<AtIdentifier<'static>>, 219) -> ( 220 Result<Resource<Option<SmolStr>>, RenderError>, 221 Memo<Option<AtIdentifier<'static>>>, 222) { 223 let ident = use_signal(use_reactive!(|ident| ident.clone())); 224 let fetcher = use_context::<crate::fetch::Fetcher>(); 225 let fetcher = fetcher.clone(); 226 let res = use_server_future(use_reactive!(|ident| { 227 let client = fetcher.get_client(); 228 async move { 229 if let Some(ident) = &*ident.read() { 230 use jacquard::smol_str::ToSmolStr; 231 232 client 233 .resolve_ident_owned(ident) 234 .await 235 .map(|doc| doc.handles().first().map(|h| h.to_smolstr())) 236 .unwrap_or(Some(ident.to_smolstr())) 237 } else { 238 None 239 } 240 } 241 })); 242 243 let memo = use_memo(use_reactive!(|res| { 244 if let Ok(res) = res { 245 if let Some(value) = &*res.read() { 246 if let Some(handle) = value { 247 AtIdentifier::new_owned(handle.clone()).ok() 248 } else { 249 ident.read().clone() 250 } 251 } else { 252 ident.read().clone() 253 } 254 } else { 255 ident.read().clone() 256 } 257 })); 258 259 (res, memo) 260} 261 262#[cfg(not(feature = "fullstack-server"))] 263pub fn use_load_handle( 264 ident: Option<AtIdentifier<'static>>, 265) -> ( 266 Resource<Option<AtIdentifier<'static>>>, 267 Memo<Option<AtIdentifier<'static>>>, 268) { 269 let ident = use_signal(use_reactive!(|ident| ident.clone())); 270 let fetcher = use_context::<crate::fetch::Fetcher>(); 271 let fetcher = fetcher.clone(); 272 let res = use_resource(move || { 273 let client = fetcher.get_client(); 274 async move { 275 if let Some(ident) = &*ident.read() { 276 client 277 .resolve_ident_owned(ident) 278 .await 279 .map(|doc| { 280 doc.handles() 281 .first() 282 .map(|h| AtIdentifier::Handle(h.clone()).into_static()) 283 }) 284 .unwrap_or(Some(ident.clone())) 285 } else { 286 None 287 } 288 } 289 }); 290 291 let memo = use_memo(move || { 292 if let Some(value) = &*res.read() { 293 value.clone() 294 } else { 295 ident.read().clone() 296 } 297 }); 298 299 (res, memo) 300} 301#[cfg(not(feature = "fullstack-server"))] 302pub fn use_handle( 303 ident: ReadSignal<AtIdentifier<'static>>, 304) -> (Resource<AtIdentifier<'static>>, Memo<AtIdentifier<'static>>) { 305 let old_ident = ident.read().clone(); 306 let fetcher = use_context::<crate::fetch::Fetcher>(); 307 let fetcher = fetcher.clone(); 308 let res = use_resource(move || { 309 let client = fetcher.get_client(); 310 let old_ident = old_ident.clone(); 311 async move { 312 client 313 .resolve_ident_owned(&*ident.read()) 314 .await 315 .map(|doc| { 316 doc.handles() 317 .first() 318 .map(|h| AtIdentifier::Handle(h.clone()).into_static()) 319 }) 320 .ok() 321 .flatten() 322 .unwrap_or(old_ident) 323 } 324 }); 325 326 let memo = use_memo(move || { 327 if let Some(value) = &*res.read() { 328 value.clone() 329 } else { 330 ident.read().clone() 331 } 332 }); 333 334 (res, memo) 335} 336#[cfg(feature = "fullstack-server")] 337pub fn use_handle( 338 ident: ReadSignal<AtIdentifier<'static>>, 339) -> ( 340 Result<Resource<SmolStr>, RenderError>, 341 Memo<AtIdentifier<'static>>, 342) { 343 let old_ident = ident.read().clone(); 344 let fetcher = use_context::<crate::fetch::Fetcher>(); 345 let fetcher = fetcher.clone(); 346 let res = use_server_future(use_reactive!(|ident| { 347 let client = fetcher.get_client(); 348 let old_ident = old_ident.clone(); 349 async move { 350 use jacquard::smol_str::ToSmolStr; 351 352 client 353 .resolve_ident_owned(&ident()) 354 .await 355 .map(|doc| { 356 use jacquard::smol_str::ToSmolStr; 357 358 doc.handles().first().map(|h| h.to_smolstr()) 359 }) 360 .ok() 361 .flatten() 362 .unwrap_or(old_ident.to_smolstr()) 363 } 364 })); 365 366 let memo = use_memo(use_reactive!(|res| { 367 if let Ok(res) = res { 368 if let Some(value) = &*res.read() { 369 AtIdentifier::new_owned(value).unwrap() 370 } else { 371 ident.read().clone() 372 } 373 } else { 374 ident.read().clone() 375 } 376 })); 377 378 (res, memo) 379} 380 381/// Hook to render markdown with SSR support. 382#[cfg(feature = "fullstack-server")] 383pub fn use_rendered_markdown( 384 content: ReadSignal<Entry<'static>>, 385 ident: ReadSignal<AtIdentifier<'static>>, 386) -> ( 387 Result<Resource<Option<String>>, RenderError>, 388 Memo<Option<String>>, 389) { 390 let fetcher = use_context::<crate::fetch::Fetcher>(); 391 let fetcher = fetcher.clone(); 392 let res = use_server_future(use_reactive!(|(content, ident)| { 393 let fetcher = fetcher.clone(); 394 async move { 395 let entry = content(); 396 let did = match ident.read().clone() { 397 AtIdentifier::Did(d) => d, 398 AtIdentifier::Handle(h) => fetcher.get_client().resolve_handle(&h).await.ok()?, 399 }; 400 401 let resolved_content = prefetch_embeds(&entry, &fetcher).await; 402 403 Some(render_markdown_impl(entry, did, resolved_content).await) 404 } 405 })); 406 let memo = use_memo(use_reactive!(|res| { 407 let res = res.as_ref().ok()?; 408 if let Some(Some(value)) = &*res.read() { 409 Some(value.clone()) 410 } else { 411 None 412 } 413 })); 414 (res, memo) 415} 416 417/// Hook to render markdown client-side only (no SSR). 418#[cfg(not(feature = "fullstack-server"))] 419pub fn use_rendered_markdown( 420 content: ReadSignal<Entry<'static>>, 421 ident: ReadSignal<AtIdentifier<'static>>, 422) -> (Resource<Option<String>>, Memo<Option<String>>) { 423 let fetcher = use_context::<crate::fetch::Fetcher>(); 424 let fetcher = fetcher.clone(); 425 let res = use_resource(use_reactive!(|(content, ident)| { 426 let fetcher = fetcher.clone(); 427 async move { 428 let entry = content(); 429 let did = match ident() { 430 AtIdentifier::Did(d) => d, 431 AtIdentifier::Handle(h) => fetcher.get_client().resolve_handle(&h).await.ok()?, 432 }; 433 434 let resolved_content = prefetch_embeds(&entry, &fetcher).await; 435 436 Some(render_markdown_impl(entry, did, resolved_content).await) 437 } 438 })); 439 let memo = use_memo(use_reactive!(|res| { 440 if let Some(Some(value)) = &*res.read() { 441 Some(value.clone()) 442 } else { 443 None 444 } 445 })); 446 (res, memo) 447} 448 449/// Extract AT URIs for embeds from stored records or by parsing markdown. 450/// 451/// Tries stored `embeds.records` first, falls back to parsing markdown content. 452fn extract_embed_uris(entry: &Entry<'_>) -> Vec<AtUri<'static>> { 453 use jacquard::IntoStatic; 454 455 // Try stored records first 456 if let Some(ref embeds) = entry.embeds { 457 if let Some(ref records) = embeds.records { 458 let stored_uris: Vec<_> = records 459 .records 460 .iter() 461 .map(|r| r.record.uri.clone().into_static()) 462 .collect(); 463 if !stored_uris.is_empty() { 464 return stored_uris; 465 } 466 } 467 } 468 469 // Fall back to parsing markdown for at:// URIs 470 use regex_lite::Regex; 471 use std::sync::LazyLock; 472 473 static AT_URI_REGEX: LazyLock<Regex> = 474 LazyLock::new(|| Regex::new(r"at://[^\s\)\]]+").unwrap()); 475 476 let uris: Vec<_> = AT_URI_REGEX 477 .find_iter(&entry.content) 478 .filter_map(|m| AtUri::new(m.as_str()).ok().map(|u| u.into_static())) 479 .collect(); 480 uris 481} 482 483/// Pre-fetch embed content for all AT URIs in an entry. 484async fn prefetch_embeds( 485 entry: &Entry<'static>, 486 fetcher: &crate::fetch::Fetcher, 487) -> weaver_common::ResolvedContent { 488 use weaver_renderer::atproto::fetch_and_render; 489 490 let mut resolved = weaver_common::ResolvedContent::new(); 491 let uris = extract_embed_uris(entry); 492 493 for uri in uris { 494 match fetch_and_render(&uri, fetcher).await { 495 Ok(html) => { 496 resolved.add_embed(uri, html, None); 497 } 498 Err(e) => { 499 tracing::warn!("[prefetch_embeds] Failed to fetch {}: {}", uri, e); 500 } 501 } 502 } 503 504 resolved 505} 506 507/// Internal implementation of markdown rendering. 508async fn render_markdown_impl( 509 content: Entry<'static>, 510 did: Did<'static>, 511 resolved_content: weaver_common::ResolvedContent, 512) -> String { 513 use n0_future::stream::StreamExt; 514 use weaver_renderer::{ 515 ContextIterator, NotebookProcessor, 516 atproto::{ClientContext, ClientWriter}, 517 }; 518 519 let ctx = ClientContext::<()>::new(content.clone(), did); 520 let parser = 521 markdown_weaver::Parser::new_ext(&content.content, weaver_renderer::default_md_options()) 522 .into_offset_iter(); 523 let iter = ContextIterator::default(parser); 524 let processor = NotebookProcessor::new(ctx, iter); 525 526 let events: Vec<_> = StreamExt::collect(processor).await; 527 528 let mut html_buf = String::new(); 529 let writer = ClientWriter::<_, _, ()>::new(events.into_iter(), &mut html_buf, &content.content) 530 .with_embed_provider(resolved_content); 531 writer.run().ok(); 532 html_buf 533} 534 535/// Fetches profile data for a given identifier 536#[cfg(feature = "fullstack-server")] 537pub fn use_profile_data( 538 ident: ReadSignal<AtIdentifier<'static>>, 539) -> ( 540 Result<Resource<Option<serde_json::Value>>, RenderError>, 541 Memo<Option<ProfileDataView<'static>>>, 542) { 543 let fetcher = use_context::<crate::fetch::Fetcher>(); 544 let res = use_server_future(use_reactive!(|ident| { 545 let fetcher = fetcher.clone(); 546 async move { 547 fetcher 548 .fetch_profile(&ident()) 549 .await 550 .ok() 551 .map(|arc| serde_json::to_value(&*arc).ok()) 552 .flatten() 553 } 554 })); 555 let memo = use_memo(use_reactive!(|res| { 556 let res = res.as_ref().ok()?; 557 if let Some(Some(value)) = &*res.read() { 558 jacquard::from_json_value::<ProfileDataView>(value.clone()).ok() 559 } else { 560 None 561 } 562 })); 563 (res, memo) 564} 565 566/// Fetches profile data client-side only (no SSR) 567#[cfg(not(feature = "fullstack-server"))] 568pub fn use_profile_data( 569 ident: ReadSignal<AtIdentifier<'static>>, 570) -> ( 571 Resource<Option<ProfileDataView<'static>>>, 572 Memo<Option<ProfileDataView<'static>>>, 573) { 574 let fetcher = use_context::<crate::fetch::Fetcher>(); 575 let res = use_resource(move || { 576 let fetcher = fetcher.clone(); 577 async move { 578 fetcher 579 .fetch_profile(&ident()) 580 .await 581 .ok() 582 .map(|arc| (*arc).clone()) 583 } 584 }); 585 let memo = use_memo(move || res.read().clone().flatten()); 586 (res, memo) 587} 588 589/// Fetches notebooks for a specific DID 590#[cfg(feature = "fullstack-server")] 591pub fn use_notebooks_for_did( 592 ident: ReadSignal<AtIdentifier<'static>>, 593) -> ( 594 Result<Resource<Option<Vec<serde_json::Value>>>, RenderError>, 595 Memo<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>>, 596) { 597 let fetcher = use_context::<crate::fetch::Fetcher>(); 598 let res = use_server_future(use_reactive!(|ident| { 599 let fetcher = fetcher.clone(); 600 async move { 601 fetcher 602 .fetch_notebooks_for_did(&ident()) 603 .await 604 .ok() 605 .map(|notebooks| { 606 notebooks 607 .iter() 608 .map(|arc| serde_json::to_value(arc.as_ref()).ok()) 609 .collect::<Option<Vec<_>>>() 610 }) 611 .flatten() 612 } 613 })); 614 let memo = use_memo(use_reactive!(|res| { 615 let res = res.as_ref().ok()?; 616 if let Some(Some(values)) = &*res.read() { 617 values 618 .iter() 619 .map(|v| { 620 jacquard::from_json_value::<(NotebookView, Vec<StrongRef>)>(v.clone()).ok() 621 }) 622 .collect::<Option<Vec<_>>>() 623 } else { 624 None 625 } 626 })); 627 (res, memo) 628} 629 630/// Fetches notebooks client-side only (no SSR) 631#[cfg(not(feature = "fullstack-server"))] 632pub fn use_notebooks_for_did( 633 ident: ReadSignal<AtIdentifier<'static>>, 634) -> ( 635 Resource<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>>, 636 Memo<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>>, 637) { 638 let fetcher = use_context::<crate::fetch::Fetcher>(); 639 let res = use_resource(move || { 640 let fetcher = fetcher.clone(); 641 async move { 642 fetcher 643 .fetch_notebooks_for_did(&ident()) 644 .await 645 .ok() 646 .map(|notebooks| { 647 notebooks 648 .iter() 649 .map(|arc| arc.as_ref().clone()) 650 .collect::<Vec<_>>() 651 }) 652 } 653 }); 654 let memo = use_memo(move || res.read().clone().flatten()); 655 (res, memo) 656} 657 658/// Fetches all entries for a specific DID with SSR support 659#[cfg(feature = "fullstack-server")] 660pub fn use_entries_for_did( 661 ident: ReadSignal<AtIdentifier<'static>>, 662) -> ( 663 Result<Resource<Option<Vec<(serde_json::Value, serde_json::Value)>>>, RenderError>, 664 Memo<Option<Vec<(EntryView<'static>, Entry<'static>)>>>, 665) { 666 let fetcher = use_context::<crate::fetch::Fetcher>(); 667 let res = use_server_future(use_reactive!(|ident| { 668 let fetcher = fetcher.clone(); 669 async move { 670 fetcher 671 .fetch_entries_for_did(&ident()) 672 .await 673 .ok() 674 .map(|entries| { 675 entries 676 .iter() 677 .filter_map(|arc| { 678 let (view, entry) = arc.as_ref(); 679 let view_json = serde_json::to_value(view).ok()?; 680 let entry_json = serde_json::to_value(entry).ok()?; 681 Some((view_json, entry_json)) 682 }) 683 .collect::<Vec<_>>() 684 }) 685 } 686 })); 687 let memo = use_memo(use_reactive!(|res| { 688 let res = res.as_ref().ok()?; 689 if let Some(Some(values)) = &*res.read() { 690 let result: Vec<_> = values 691 .iter() 692 .filter_map(|(view_json, entry_json)| { 693 let view = jacquard::from_json_value::<EntryView>(view_json.clone()).ok()?; 694 let entry = jacquard::from_json_value::<Entry>(entry_json.clone()).ok()?; 695 Some((view, entry)) 696 }) 697 .collect(); 698 Some(result) 699 } else { 700 None 701 } 702 })); 703 (res, memo) 704} 705 706/// Fetches all entries for a specific DID client-side only (no SSR) 707#[cfg(not(feature = "fullstack-server"))] 708pub fn use_entries_for_did( 709 ident: ReadSignal<AtIdentifier<'static>>, 710) -> ( 711 Resource<Option<Vec<(EntryView<'static>, Entry<'static>)>>>, 712 Memo<Option<Vec<(EntryView<'static>, Entry<'static>)>>>, 713) { 714 let fetcher = use_context::<crate::fetch::Fetcher>(); 715 let res = use_resource(move || { 716 let fetcher = fetcher.clone(); 717 async move { 718 fetcher 719 .fetch_entries_for_did(&ident()) 720 .await 721 .ok() 722 .map(|entries| { 723 entries 724 .iter() 725 .map(|arc| arc.as_ref().clone()) 726 .collect::<Vec<_>>() 727 }) 728 } 729 }); 730 let memo = use_memo(move || res.read().clone().flatten()); 731 (res, memo) 732} 733 734// ============================================================================ 735// Client-only versions (bypass SSR issues on profile page) 736// ============================================================================ 737 738/// Fetches profile data client-side only - use when SSR causes issues 739pub fn use_profile_data_client( 740 ident: ReadSignal<AtIdentifier<'static>>, 741) -> ( 742 Resource<Option<ProfileDataView<'static>>>, 743 Memo<Option<ProfileDataView<'static>>>, 744) { 745 let fetcher = use_context::<crate::fetch::Fetcher>(); 746 let res = use_resource(move || { 747 let fetcher = fetcher.clone(); 748 async move { 749 fetcher 750 .fetch_profile(&ident()) 751 .await 752 .ok() 753 .map(|arc| (*arc).clone()) 754 } 755 }); 756 let memo = use_memo(move || res.read().clone().flatten()); 757 (res, memo) 758} 759 760/// Fetches notebooks client-side only - use when SSR causes issues 761pub fn use_notebooks_for_did_client( 762 ident: ReadSignal<AtIdentifier<'static>>, 763) -> ( 764 Resource<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>>, 765 Memo<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>>, 766) { 767 let fetcher = use_context::<crate::fetch::Fetcher>(); 768 let res = use_resource(move || { 769 let fetcher = fetcher.clone(); 770 async move { 771 fetcher 772 .fetch_notebooks_for_did(&ident()) 773 .await 774 .ok() 775 .map(|notebooks| { 776 notebooks 777 .iter() 778 .map(|arc| arc.as_ref().clone()) 779 .collect::<Vec<_>>() 780 }) 781 } 782 }); 783 let memo = use_memo(move || res.read().clone().flatten()); 784 (res, memo) 785} 786 787/// Fetches all entries client-side only - use when SSR causes issues 788pub fn use_entries_for_did_client( 789 ident: ReadSignal<AtIdentifier<'static>>, 790) -> ( 791 Resource<Option<Vec<(EntryView<'static>, Entry<'static>)>>>, 792 Memo<Option<Vec<(EntryView<'static>, Entry<'static>)>>>, 793) { 794 let fetcher = use_context::<crate::fetch::Fetcher>(); 795 let res = use_resource(move || { 796 let fetcher = fetcher.clone(); 797 async move { 798 fetcher 799 .fetch_entries_for_did(&ident()) 800 .await 801 .ok() 802 .map(|entries| { 803 entries 804 .iter() 805 .map(|arc| arc.as_ref().clone()) 806 .collect::<Vec<_>>() 807 }) 808 } 809 }); 810 let memo = use_memo(move || res.read().clone().flatten()); 811 (res, memo) 812} 813 814/// Fetches notebooks from UFOS with SSR support in fullstack mode 815#[cfg(feature = "fullstack-server")] 816pub fn use_notebooks_from_ufos() -> ( 817 Result<Resource<Option<Vec<serde_json::Value>>>, RenderError>, 818 Memo<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>>, 819) { 820 let fetcher = use_context::<crate::fetch::Fetcher>(); 821 let res = use_server_future(move || { 822 let fetcher = fetcher.clone(); 823 async move { 824 fetcher 825 .fetch_notebooks_from_ufos() 826 .await 827 .ok() 828 .map(|notebooks| { 829 notebooks 830 .iter() 831 .map(|arc| serde_json::to_value(arc.as_ref()).ok()) 832 .collect::<Option<Vec<_>>>() 833 }) 834 .flatten() 835 } 836 }); 837 let memo = use_memo(use_reactive!(|res| { 838 let res = res.as_ref().ok()?; 839 if let Some(Some(values)) = &*res.read() { 840 values 841 .iter() 842 .map(|v| { 843 jacquard::from_json_value::<(NotebookView, Vec<StrongRef>)>(v.clone()).ok() 844 }) 845 .collect::<Option<Vec<_>>>() 846 } else { 847 None 848 } 849 })); 850 (res, memo) 851} 852 853/// Fetches notebooks from UFOS client-side only (no SSR) 854#[cfg(not(feature = "fullstack-server"))] 855pub fn use_notebooks_from_ufos() -> ( 856 Resource<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>>, 857 Memo<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>>, 858) { 859 let fetcher = use_context::<crate::fetch::Fetcher>(); 860 let res = use_resource(move || { 861 let fetcher = fetcher.clone(); 862 async move { 863 fetcher 864 .fetch_notebooks_from_ufos() 865 .await 866 .ok() 867 .map(|notebooks| { 868 notebooks 869 .iter() 870 .map(|arc| arc.as_ref().clone()) 871 .collect::<Vec<_>>() 872 }) 873 } 874 }); 875 let memo = use_memo(move || res.read().clone().flatten()); 876 (res, memo) 877} 878 879/// Fetches entries from UFOS with SSR support in fullstack mode 880#[cfg(feature = "fullstack-server")] 881pub fn use_entries_from_ufos() -> ( 882 Result<Resource<Option<Vec<(serde_json::Value, serde_json::Value, u64)>>>, RenderError>, 883 Memo<Option<Vec<(EntryView<'static>, Entry<'static>, u64)>>>, 884) { 885 let fetcher = use_context::<crate::fetch::Fetcher>(); 886 let res = use_server_future(move || { 887 let fetcher = fetcher.clone(); 888 async move { 889 match fetcher.fetch_entries_from_ufos().await { 890 Ok(entries) => { 891 // Cache blobs for each entry's embedded images 892 for arc in &entries { 893 let (view, entry, _) = arc.as_ref(); 894 if let Some(embeds) = &entry.embeds { 895 if let Some(images) = &embeds.images { 896 use jacquard::smol_str::ToSmolStr; 897 use jacquard::types::aturi::AtUri; 898 // Extract ident from the entry's at-uri 899 if let Ok(at_uri) = AtUri::new(view.uri.as_ref()) { 900 let ident = at_uri.authority(); 901 for image in &images.images { 902 let cid = image.image.blob().cid(); 903 cache_blob( 904 ident.clone().to_smolstr(), 905 cid.to_smolstr(), 906 image.name.as_ref().map(|n| n.to_smolstr()), 907 ) 908 .await 909 .ok(); 910 } 911 } 912 } 913 } 914 } 915 Some( 916 entries 917 .iter() 918 .filter_map(|arc| { 919 let (view, entry, time) = arc.as_ref(); 920 let view_json = serde_json::to_value(view).ok()?; 921 let entry_json = serde_json::to_value(entry).ok()?; 922 Some((view_json, entry_json, *time)) 923 }) 924 .collect::<Vec<_>>(), 925 ) 926 } 927 Err(e) => { 928 tracing::error!("[use_entries_from_ufos] fetch failed: {:?}", e); 929 None 930 } 931 } 932 } 933 }); 934 let memo = use_memo(use_reactive!(|res| { 935 let res = res.as_ref().ok()?; 936 if let Some(Some(values)) = &*res.read() { 937 let result: Vec<_> = values 938 .iter() 939 .filter_map(|(view_json, entry_json, time)| { 940 let view = jacquard::from_json_value::<EntryView>(view_json.clone()).ok()?; 941 let entry = jacquard::from_json_value::<Entry>(entry_json.clone()).ok()?; 942 Some((view, entry, *time)) 943 }) 944 .collect(); 945 Some(result) 946 } else { 947 None 948 } 949 })); 950 (res, memo) 951} 952 953/// Fetches entries from UFOS client-side only (no SSR) 954#[cfg(not(feature = "fullstack-server"))] 955pub fn use_entries_from_ufos() -> ( 956 Resource<Option<Vec<(EntryView<'static>, Entry<'static>, u64)>>>, 957 Memo<Option<Vec<(EntryView<'static>, Entry<'static>, u64)>>>, 958) { 959 let fetcher = use_context::<crate::fetch::Fetcher>(); 960 let res = use_resource(move || { 961 let fetcher = fetcher.clone(); 962 async move { 963 fetcher.fetch_entries_from_ufos().await.ok().map(|entries| { 964 entries 965 .iter() 966 .map(|arc| arc.as_ref().clone()) 967 .collect::<Vec<_>>() 968 }) 969 } 970 }); 971 let memo = use_memo(move || res.read().clone().flatten()); 972 (res, memo) 973} 974 975/// Fetches notebook metadata with SSR support in fullstack mode 976#[cfg(feature = "fullstack-server")] 977pub fn use_notebook( 978 ident: ReadSignal<AtIdentifier<'static>>, 979 book_title: ReadSignal<SmolStr>, 980) -> ( 981 Result<Resource<Option<serde_json::Value>>, RenderError>, 982 Memo<Option<(NotebookView<'static>, Vec<BookEntryView<'static>>)>>, 983) { 984 let fetcher = use_context::<crate::fetch::Fetcher>(); 985 let res = use_server_future(use_reactive!(|(ident, book_title)| { 986 let fetcher = fetcher.clone(); 987 async move { 988 fetcher 989 .get_notebook(ident(), book_title()) 990 .await 991 .ok() 992 .flatten() 993 .map(|arc| serde_json::to_value(arc.as_ref()).ok()) 994 .flatten() 995 } 996 })); 997 let memo = use_memo(use_reactive!(|res| { 998 let res = res.as_ref().ok()?; 999 if let Some(Some(value)) = &*res.read() { 1000 jacquard::from_json_value::<(NotebookView, Vec<BookEntryView>)>(value.clone()).ok() 1001 } else { 1002 None 1003 } 1004 })); 1005 (res, memo) 1006} 1007 1008/// Fetches notebook metadata client-side only (no SSR) 1009#[cfg(not(feature = "fullstack-server"))] 1010pub fn use_notebook( 1011 ident: ReadSignal<AtIdentifier<'static>>, 1012 book_title: ReadSignal<SmolStr>, 1013) -> ( 1014 Resource<Option<(NotebookView<'static>, Vec<BookEntryView<'static>>)>>, 1015 Memo<Option<(NotebookView<'static>, Vec<BookEntryView<'static>>)>>, 1016) { 1017 let fetcher = use_context::<crate::fetch::Fetcher>(); 1018 let res = use_resource(move || { 1019 let fetcher = fetcher.clone(); 1020 async move { 1021 fetcher 1022 .get_notebook(ident(), book_title()) 1023 .await 1024 .ok() 1025 .flatten() 1026 .map(|arc| arc.as_ref().clone()) 1027 } 1028 }); 1029 let memo = use_memo(move || res.read().clone().flatten()); 1030 (res, memo) 1031} 1032 1033/// Fetches notebook entries with SSR support in fullstack mode 1034#[cfg(feature = "fullstack-server")] 1035pub fn use_notebook_entries( 1036 ident: ReadSignal<AtIdentifier<'static>>, 1037 book_title: ReadSignal<SmolStr>, 1038) -> ( 1039 Result<Resource<Option<Vec<serde_json::Value>>>, RenderError>, 1040 Memo<Option<Vec<BookEntryView<'static>>>>, 1041) { 1042 let fetcher = use_context::<crate::fetch::Fetcher>(); 1043 let res = use_server_future(use_reactive!(|(ident, book_title)| { 1044 let fetcher = fetcher.clone(); 1045 async move { 1046 fetcher 1047 .list_notebook_entries(ident(), book_title()) 1048 .await 1049 .ok() 1050 .flatten() 1051 .map(|entries| { 1052 entries 1053 .iter() 1054 .map(|e| serde_json::to_value(e).ok()) 1055 .collect::<Option<Vec<_>>>() 1056 }) 1057 .flatten() 1058 } 1059 })); 1060 let memo = use_memo(use_reactive!(|res| { 1061 let res = res.as_ref().ok()?; 1062 if let Some(Some(values)) = &*res.read() { 1063 values 1064 .iter() 1065 .map(|v| jacquard::from_json_value::<BookEntryView>(v.clone()).ok()) 1066 .collect::<Option<Vec<_>>>() 1067 } else { 1068 None 1069 } 1070 })); 1071 1072 (res, memo) 1073} 1074 1075/// Fetches notebook entries client-side only (no SSR) 1076#[cfg(not(feature = "fullstack-server"))] 1077pub fn use_notebook_entries( 1078 ident: ReadSignal<AtIdentifier<'static>>, 1079 book_title: ReadSignal<SmolStr>, 1080) -> ( 1081 Resource<Option<Vec<BookEntryView<'static>>>>, 1082 Memo<Option<Vec<BookEntryView<'static>>>>, 1083) { 1084 let fetcher = use_context::<crate::fetch::Fetcher>(); 1085 let r = use_resource(move || { 1086 let fetcher = fetcher.clone(); 1087 async move { 1088 fetcher 1089 .list_notebook_entries(ident(), book_title()) 1090 .await 1091 .ok() 1092 .flatten() 1093 } 1094 }); 1095 let memo = use_memo(move || r.read().as_ref().and_then(|v| v.clone())); 1096 (r, memo) 1097} 1098 1099// ============================================================================ 1100// Ownership Checking 1101// ============================================================================ 1102 1103/// Check if the current authenticated user owns a resource identified by an AtIdentifier. 1104/// 1105/// Returns a memo that is: 1106/// - `Some(true)` if the user is authenticated and their DID matches the resource owner 1107/// - `Some(false)` if the user is authenticated but doesn't match, or resource is a handle 1108/// - `None` if the user is not authenticated 1109/// 1110/// For handles, this does a synchronous check that returns `false` since we can't resolve 1111/// handles synchronously. Use `use_is_owner_async` for handle resolution. 1112pub fn use_is_owner(resource_owner: ReadSignal<AtIdentifier<'static>>) -> Memo<Option<bool>> { 1113 let auth_state = use_context::<Signal<AuthState>>(); 1114 1115 use_memo(move || { 1116 let current_did = auth_state.read().did.clone()?; 1117 let owner = resource_owner(); 1118 1119 match owner { 1120 AtIdentifier::Did(did) => Some(did == current_did), 1121 AtIdentifier::Handle(_) => Some(false), // Can't resolve synchronously 1122 } 1123 }) 1124} 1125 1126/// Check ownership with async handle resolution. 1127/// 1128/// Returns a resource that resolves to: 1129/// - `Some(true)` if the user owns the resource 1130/// - `Some(false)` if the user doesn't own the resource 1131/// - `None` if the user is not authenticated 1132#[cfg(feature = "fullstack-server")] 1133pub fn use_is_owner_async( 1134 resource_owner: ReadSignal<AtIdentifier<'static>>, 1135) -> Resource<Option<bool>> { 1136 let auth_state = use_context::<Signal<AuthState>>(); 1137 let fetcher = use_context::<crate::fetch::Fetcher>(); 1138 1139 use_resource(move || { 1140 let fetcher = fetcher.clone(); 1141 let owner = resource_owner(); 1142 async move { 1143 let current_did = auth_state.read().did.clone()?; 1144 1145 match owner { 1146 AtIdentifier::Did(did) => Some(did == current_did), 1147 AtIdentifier::Handle(handle) => match fetcher.resolve_handle(&handle).await { 1148 Ok(resolved_did) => Some(resolved_did == current_did), 1149 Err(_) => Some(false), 1150 }, 1151 } 1152 } 1153 }) 1154} 1155 1156/// Check ownership with async handle resolution (client-only mode). 1157#[cfg(not(feature = "fullstack-server"))] 1158pub fn use_is_owner_async( 1159 resource_owner: ReadSignal<AtIdentifier<'static>>, 1160) -> Resource<Option<bool>> { 1161 let auth_state = use_context::<Signal<AuthState>>(); 1162 let fetcher = use_context::<crate::fetch::Fetcher>(); 1163 1164 use_resource(move || { 1165 let fetcher = fetcher.clone(); 1166 let owner = resource_owner(); 1167 async move { 1168 let current_did = auth_state.read().did.clone()?; 1169 1170 match owner { 1171 AtIdentifier::Did(did) => Some(did == current_did), 1172 AtIdentifier::Handle(handle) => match fetcher.resolve_handle(&handle).await { 1173 Ok(resolved_did) => Some(resolved_did == current_did), 1174 Err(_) => Some(false), 1175 }, 1176 } 1177 } 1178 }) 1179} 1180 1181// ============================================================================ 1182// Edit Access Checking (Ownership + Collaboration) 1183// ============================================================================ 1184 1185use weaver_api::sh_weaver::actor::ProfileDataViewInner; 1186use weaver_api::sh_weaver::notebook::{AuthorListView, PermissionsState}; 1187 1188/// Extract DID from a ProfileDataView by matching on the inner variant. 1189pub fn extract_did_from_author(author: &AuthorListView<'_>) -> Option<Did<'static>> { 1190 match &author.record.inner { 1191 ProfileDataViewInner::ProfileView(p) => Some(p.did.clone().into_static()), 1192 ProfileDataViewInner::ProfileViewDetailed(p) => Some(p.did.clone().into_static()), 1193 ProfileDataViewInner::TangledProfileView(p) => Some(p.did.clone().into_static()), 1194 _ => None, 1195 } 1196} 1197 1198/// Check if the current user can edit a resource based on the permissions state. 1199/// 1200/// Returns a memo that is: 1201/// - `Some(true)` if the user is authenticated and their DID is in permissions.editors 1202/// - `Some(false)` if the user is authenticated but not in editors 1203/// - `None` if the user is not authenticated or permissions not yet loaded 1204/// 1205/// This checks the ACL-based permissions (who CAN edit), not authors (who contributed). 1206pub fn use_can_edit(permissions: Memo<Option<PermissionsState<'static>>>) -> Memo<Option<bool>> { 1207 let auth_state = use_context::<Signal<AuthState>>(); 1208 1209 use_memo(move || { 1210 let current_did = auth_state.read().did.clone()?; 1211 let perms = permissions()?; 1212 1213 // Check if current user's DID is in the editors list 1214 let can_edit = perms.editors.iter().any(|grant| grant.did == current_did); 1215 1216 Some(can_edit) 1217 }) 1218} 1219 1220/// Legacy: Check if the current user can edit based on authors list. 1221/// 1222/// Use `use_can_edit` with permissions instead when available. 1223/// This is kept for backwards compatibility during transition. 1224pub fn use_can_edit_from_authors( 1225 authors: Memo<Vec<AuthorListView<'static>>>, 1226) -> Memo<Option<bool>> { 1227 let auth_state = use_context::<Signal<AuthState>>(); 1228 1229 use_memo(move || { 1230 let current_did = auth_state.read().did.clone()?; 1231 let author_list = authors(); 1232 1233 let can_edit = author_list 1234 .iter() 1235 .filter_map(extract_did_from_author) 1236 .any(|did| did == current_did); 1237 1238 Some(can_edit) 1239 }) 1240} 1241 1242/// Check edit access for a resource URI using the WeaverExt trait methods. 1243/// 1244/// This performs an async check that queries Constellation for collaboration records. 1245/// Use this when you have a resource URI but not the pre-populated authors list. 1246pub fn use_can_edit_resource(resource_uri: ReadSignal<AtUri<'static>>) -> Resource<Option<bool>> { 1247 let auth_state = use_context::<Signal<AuthState>>(); 1248 let fetcher = use_context::<crate::fetch::Fetcher>(); 1249 1250 use_resource(move || { 1251 let fetcher = fetcher.clone(); 1252 let uri = resource_uri(); 1253 async move { 1254 use weaver_common::agent::WeaverExt; 1255 1256 let current_did = auth_state.read().did.clone()?; 1257 1258 // Check ownership first (fast path) 1259 if let AtIdentifier::Did(owner_did) = uri.authority() { 1260 if *owner_did == current_did { 1261 return Some(true); 1262 } 1263 } 1264 1265 // Check collaboration via Constellation 1266 match fetcher.can_user_edit_resource(&uri, &current_did).await { 1267 Ok(can_edit) => Some(can_edit), 1268 Err(_) => Some(false), 1269 } 1270 } 1271 }) 1272} 1273 1274// ============================================================================ 1275// Standalone Entry by Rkey Hooks 1276// ============================================================================ 1277 1278/// Fetches standalone entry data by rkey with SSR support. 1279/// Returns entry + optional notebook context if entry is in exactly one notebook. 1280#[cfg(feature = "fullstack-server")] 1281pub fn use_standalone_entry_data( 1282 ident: ReadSignal<AtIdentifier<'static>>, 1283 rkey: ReadSignal<SmolStr>, 1284) -> ( 1285 Result< 1286 Resource< 1287 Option<( 1288 serde_json::Value, 1289 serde_json::Value, 1290 Option<(serde_json::Value, serde_json::Value)>, 1291 )>, 1292 >, 1293 RenderError, 1294 >, 1295 Memo<Option<crate::fetch::StandaloneEntryData>>, 1296) { 1297 let fetcher = use_context::<crate::fetch::Fetcher>(); 1298 let res = use_server_future(use_reactive!(|(ident, rkey)| { 1299 let fetcher = fetcher.clone(); 1300 async move { 1301 match fetcher.get_entry_by_rkey(ident(), rkey()).await { 1302 Ok(Some(data)) => { 1303 // Cache blobs for embedded images 1304 if let Some(embeds) = &data.entry.embeds { 1305 if let Some(images) = &embeds.images { 1306 use jacquard::smol_str::ToSmolStr; 1307 use jacquard::types::aturi::AtUri; 1308 if let Ok(at_uri) = AtUri::new(data.entry_view.uri.as_ref()) { 1309 let ident_str = at_uri.authority().to_smolstr(); 1310 #[cfg(all(target_family = "wasm", target_os = "unknown"))] 1311 { 1312 tracing::debug!("Registering standalone entry blobs"); 1313 let _ = crate::service_worker::register_standalone_entry_blobs( 1314 &ident(), 1315 rkey().as_str(), 1316 images, 1317 &fetcher, 1318 ) 1319 .await; 1320 } 1321 for image in &images.images { 1322 let cid = image.image.blob().cid(); 1323 cache_blob( 1324 ident_str.clone(), 1325 cid.to_smolstr(), 1326 image.name.as_ref().map(|n| n.to_smolstr()), 1327 ) 1328 .await 1329 .ok(); 1330 } 1331 } 1332 } 1333 } 1334 let entry_json = serde_json::to_value(&data.entry).ok()?; 1335 let entry_view_json = serde_json::to_value(&data.entry_view).ok()?; 1336 let notebook_ctx_json = data 1337 .notebook_context 1338 .as_ref() 1339 .map(|ctx| { 1340 let notebook_json = serde_json::to_value(&ctx.notebook).ok()?; 1341 let book_entry_json = 1342 serde_json::to_value(&ctx.book_entry_view).ok()?; 1343 Some((notebook_json, book_entry_json)) 1344 }) 1345 .flatten(); 1346 Some((entry_json, entry_view_json, notebook_ctx_json)) 1347 } 1348 Ok(None) => None, 1349 Err(e) => { 1350 tracing::error!("[use_standalone_entry_data] fetch error: {:?}", e); 1351 None 1352 } 1353 } 1354 } 1355 })); 1356 1357 let memo = use_memo(use_reactive!(|res| { 1358 use crate::fetch::{NotebookContext, StandaloneEntryData}; 1359 use weaver_api::sh_weaver::notebook::{ 1360 BookEntryView, EntryView, NotebookView, entry::Entry, 1361 }; 1362 1363 let res = res.as_ref().ok()?; 1364 let Some(Some((entry_json, entry_view_json, notebook_ctx_json))) = res.read().clone() 1365 else { 1366 return None; 1367 }; 1368 1369 let entry: Entry<'static> = jacquard::from_json_value::<Entry>(entry_json).ok()?; 1370 let entry_view: EntryView<'static> = 1371 jacquard::from_json_value::<EntryView>(entry_view_json).ok()?; 1372 let notebook_context = notebook_ctx_json 1373 .map(|(notebook_json, book_entry_json)| { 1374 let notebook: NotebookView<'static> = 1375 jacquard::from_json_value::<NotebookView>(notebook_json).ok()?; 1376 let book_entry_view: BookEntryView<'static> = 1377 jacquard::from_json_value::<BookEntryView>(book_entry_json).ok()?; 1378 Some(NotebookContext { 1379 notebook, 1380 book_entry_view, 1381 }) 1382 }) 1383 .flatten(); 1384 1385 Some(StandaloneEntryData { 1386 entry, 1387 entry_view, 1388 notebook_context, 1389 }) 1390 })); 1391 1392 (res, memo) 1393} 1394 1395/// Fetches standalone entry data client-side only (no SSR) 1396#[cfg(not(feature = "fullstack-server"))] 1397pub fn use_standalone_entry_data( 1398 ident: ReadSignal<AtIdentifier<'static>>, 1399 rkey: ReadSignal<SmolStr>, 1400) -> ( 1401 Resource<Option<crate::fetch::StandaloneEntryData>>, 1402 Memo<Option<crate::fetch::StandaloneEntryData>>, 1403) { 1404 let fetcher = use_context::<crate::fetch::Fetcher>(); 1405 let res = use_resource(move || { 1406 let fetcher = fetcher.clone(); 1407 async move { 1408 fetcher 1409 .get_entry_by_rkey(ident(), rkey()) 1410 .await 1411 .ok() 1412 .flatten() 1413 .map(|arc| (*arc).clone()) 1414 } 1415 }); 1416 let memo = use_memo(move || res.read().clone().flatten()); 1417 (res, memo) 1418} 1419 1420/// Fetches notebook entry by rkey with SSR support. 1421#[cfg(feature = "fullstack-server")] 1422pub fn use_notebook_entry_by_rkey( 1423 ident: ReadSignal<AtIdentifier<'static>>, 1424 book_title: ReadSignal<SmolStr>, 1425 rkey: ReadSignal<SmolStr>, 1426) -> ( 1427 Result<Resource<Option<(serde_json::Value, serde_json::Value)>>, RenderError>, 1428 Memo<Option<(BookEntryView<'static>, Entry<'static>)>>, 1429) { 1430 let fetcher = use_context::<crate::fetch::Fetcher>(); 1431 let res = use_server_future(use_reactive!(|(ident, book_title, rkey)| { 1432 let fetcher = fetcher.clone(); 1433 async move { 1434 match fetcher 1435 .get_notebook_entry_by_rkey(ident(), book_title(), rkey()) 1436 .await 1437 { 1438 Ok(Some(data)) => { 1439 let book_entry_json = serde_json::to_value(&data.0).ok()?; 1440 let entry_json = serde_json::to_value(&data.1).ok()?; 1441 Some((book_entry_json, entry_json)) 1442 } 1443 Ok(None) => None, 1444 Err(e) => { 1445 tracing::error!("[use_notebook_entry_by_rkey] fetch error: {:?}", e); 1446 None 1447 } 1448 } 1449 } 1450 })); 1451 1452 let memo = use_memo(use_reactive!(|res| { 1453 let res = res.as_ref().ok()?; 1454 if let Some(Some((book_entry_json, entry_json))) = &*res.read() { 1455 let book_entry: BookEntryView<'static> = 1456 jacquard::from_json_value::<BookEntryView>(book_entry_json.clone()).ok()?; 1457 let entry: Entry<'static> = 1458 jacquard::from_json_value::<Entry>(entry_json.clone()).ok()?; 1459 Some((book_entry, entry)) 1460 } else { 1461 None 1462 } 1463 })); 1464 1465 (res, memo) 1466} 1467 1468/// Fetches notebook entry by rkey client-side only (no SSR) 1469#[cfg(not(feature = "fullstack-server"))] 1470pub fn use_notebook_entry_by_rkey( 1471 ident: ReadSignal<AtIdentifier<'static>>, 1472 book_title: ReadSignal<SmolStr>, 1473 rkey: ReadSignal<SmolStr>, 1474) -> ( 1475 Resource<Option<(BookEntryView<'static>, Entry<'static>)>>, 1476 Memo<Option<(BookEntryView<'static>, Entry<'static>)>>, 1477) { 1478 let fetcher = use_context::<crate::fetch::Fetcher>(); 1479 let res = use_resource(move || { 1480 let fetcher = fetcher.clone(); 1481 async move { 1482 fetcher 1483 .get_notebook_entry_by_rkey(ident(), book_title(), rkey()) 1484 .await 1485 .ok() 1486 .flatten() 1487 .map(|arc| (*arc).clone()) 1488 } 1489 }); 1490 let memo = use_memo(move || res.read().clone().flatten()); 1491 (res, memo) 1492} 1493 1494/// Fetches WhiteWind entry by rkey (SSR) 1495#[cfg(feature = "fullstack-server")] 1496pub fn use_whitewind_entry_data( 1497 ident: ReadSignal<AtIdentifier<'static>>, 1498 rkey: ReadSignal<SmolStr>, 1499) -> ( 1500 Result<Resource<Option<(serde_json::Value, serde_json::Value)>>, RenderError>, 1501 Memo<Option<crate::fetch::WhiteWindEntryData>>, 1502) { 1503 use weaver_api::com_whtwnd::blog::entry::Entry as WhiteWindEntry; 1504 1505 let fetcher = use_context::<crate::fetch::Fetcher>(); 1506 1507 let res = use_server_future(move || { 1508 let fetcher = fetcher.clone(); 1509 async move { 1510 use jacquard::client::AgentSessionExt; 1511 1512 let ident = ident(); 1513 let rkey = rkey(); 1514 1515 let uri_str = format!("at://{}/com.whtwnd.blog.entry/{}", ident, rkey); 1516 let uri = WhiteWindEntry::uri(&uri_str).ok()?; 1517 let record = fetcher.fetch_record(&uri).await.ok()?; 1518 1519 let profile = fetcher.fetch_profile(&ident).await.ok()?; 1520 1521 Some(( 1522 serde_json::to_value(&record.value).ok()?, 1523 serde_json::to_value(&*profile).ok()?, 1524 )) 1525 } 1526 }); 1527 1528 let memo = use_memo(use_reactive!(|res| { 1529 use weaver_api::com_whtwnd::blog::entry::Entry as WhiteWindEntry; 1530 use weaver_api::sh_weaver::actor::ProfileDataView; 1531 1532 let res = res.as_ref().ok()?; 1533 if let Some(Some((entry_json, profile_json))) = &*res.read() { 1534 let entry = jacquard::from_json_value::<WhiteWindEntry>(entry_json.clone()).ok()?; 1535 let profile = 1536 jacquard::from_json_value::<ProfileDataView>(profile_json.clone()).ok()?; 1537 Some(crate::fetch::WhiteWindEntryData { entry, profile }) 1538 } else { 1539 None 1540 } 1541 })); 1542 (res, memo) 1543} 1544 1545/// Fetches WhiteWind entry by rkey (client-only) 1546#[cfg(not(feature = "fullstack-server"))] 1547pub fn use_whitewind_entry_data( 1548 ident: ReadSignal<AtIdentifier<'static>>, 1549 rkey: ReadSignal<SmolStr>, 1550) -> ( 1551 Resource<Option<crate::fetch::WhiteWindEntryData>>, 1552 Memo<Option<crate::fetch::WhiteWindEntryData>>, 1553) { 1554 use jacquard::IntoStatic; 1555 use weaver_api::com_whtwnd::blog::entry::Entry as WhiteWindEntry; 1556 1557 let fetcher = use_context::<crate::fetch::Fetcher>(); 1558 1559 let res = use_resource(move || { 1560 let fetcher = fetcher.clone(); 1561 async move { 1562 let ident = ident(); 1563 let rkey = rkey(); 1564 1565 let uri_str = format!("at://{}/com.whtwnd.blog.entry/{}", ident, rkey); 1566 let uri = WhiteWindEntry::uri(&uri_str).ok()?; 1567 let record = fetcher.fetch_record(&uri).await.ok()?; 1568 1569 let profile = fetcher.fetch_profile(&ident).await.ok()?; 1570 1571 Some(crate::fetch::WhiteWindEntryData { 1572 entry: record.value.into_static(), 1573 profile: (*profile).clone(), 1574 }) 1575 } 1576 }); 1577 1578 let memo = use_memo(move || res.read().clone().flatten()); 1579 (res, memo) 1580} 1581 1582/// Fetches Leaflet document by rkey (SSR) 1583#[cfg(feature = "fullstack-server")] 1584pub fn use_leaflet_document_data( 1585 ident: ReadSignal<AtIdentifier<'static>>, 1586 rkey: ReadSignal<SmolStr>, 1587) -> ( 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 >, 1599 Memo<Option<crate::fetch::LeafletDocumentData>>, 1600) { 1601 use weaver_api::pub_leaflet::document::Document; 1602 1603 let fetcher = use_context::<crate::fetch::Fetcher>(); 1604 1605 let res = use_server_future(move || { 1606 let fetcher = fetcher.clone(); 1607 async move { 1608 use jacquard::IntoStatic; 1609 use jacquard::client::AgentSessionExt; 1610 use jacquard::prelude::IdentityResolver; 1611 use weaver_api::pub_leaflet::document::DocumentPagesItem; 1612 use weaver_api::pub_leaflet::publication::Publication; 1613 use weaver_renderer::leaflet::{LeafletRenderContext, render_linear_document}; 1614 1615 let ident = ident(); 1616 let rkey = rkey(); 1617 1618 let uri_str = format!("at://{}/pub.leaflet.document/{}", ident, rkey); 1619 let uri = Document::uri(&uri_str).ok()?; 1620 let record = fetcher.fetch_record(&uri).await.ok()?; 1621 1622 // Fetch publication to get base_path if document has one 1623 let publication_base_path = if let Some(pub_uri) = &record.value.publication { 1624 tracing::debug!("Leaflet doc has publication: {}", pub_uri.as_ref()); 1625 match Publication::uri(pub_uri.as_ref()) { 1626 Ok(typed_uri) => { 1627 tracing::debug!("Parsed publication URI successfully"); 1628 match fetcher.fetch_record(&typed_uri).await { 1629 Ok(pub_record) => { 1630 // Try typed field first, fall back to extra_data (handles snake_case mismatch) 1631 let bp = pub_record 1632 .value 1633 .base_path 1634 .as_ref() 1635 .map(|p| p.as_ref().to_string()) 1636 .or_else(|| { 1637 pub_record 1638 .value 1639 .extra_data 1640 .as_ref() 1641 .and_then(|m| m.get("base_path")) 1642 .and_then(|v| jacquard::from_data::<String>(v).ok()) 1643 }); 1644 tracing::debug!("Publication base_path: {:?}", bp); 1645 bp 1646 } 1647 Err(e) => { 1648 tracing::warn!("Failed to fetch publication: {:?}", e); 1649 None 1650 } 1651 } 1652 } 1653 Err(e) => { 1654 tracing::warn!("Failed to parse publication URI: {:?}", e); 1655 None 1656 } 1657 } 1658 } else { 1659 tracing::debug!("Leaflet doc has no publication"); 1660 None 1661 }; 1662 1663 // Render HTML 1664 let rendered_html = { 1665 let author_did = match &record.value.author { 1666 AtIdentifier::Did(d) => d.clone().into_static(), 1667 AtIdentifier::Handle(h) => fetcher.resolve_handle(h).await.ok()?.into_static(), 1668 }; 1669 let ctx = LeafletRenderContext::new(author_did); 1670 let mut html = String::new(); 1671 for page in &record.value.pages { 1672 match page { 1673 DocumentPagesItem::LinearDocument(linear_doc) => { 1674 html.push_str( 1675 &render_linear_document(linear_doc, &ctx, &fetcher).await, 1676 ); 1677 } 1678 DocumentPagesItem::Canvas(_) => { 1679 html.push_str("<div class=\"embed-video-placeholder\">[Canvas layout not yet supported]</div>"); 1680 } 1681 DocumentPagesItem::Unknown(_) => { 1682 html.push_str( 1683 "<div class=\"embed-video-placeholder\">[Unknown page type]</div>", 1684 ); 1685 } 1686 } 1687 } 1688 Some(html) 1689 }; 1690 1691 let profile = fetcher.fetch_profile(&ident).await.ok()?; 1692 1693 Some(( 1694 serde_json::to_value(&record.value).ok()?, 1695 serde_json::to_value(&*profile).ok()?, 1696 publication_base_path, 1697 rendered_html, 1698 )) 1699 } 1700 }); 1701 1702 let memo = use_memo(use_reactive!(|res| { 1703 use weaver_api::pub_leaflet::document::Document; 1704 use weaver_api::sh_weaver::actor::ProfileDataView; 1705 1706 let res = res.as_ref().ok()?; 1707 if let Some(Some((doc_json, profile_json, base_path, rendered_html))) = &*res.read() { 1708 let document = jacquard::from_json_value::<Document>(doc_json.clone()).ok()?; 1709 let profile = 1710 jacquard::from_json_value::<ProfileDataView>(profile_json.clone()).ok()?; 1711 Some(crate::fetch::LeafletDocumentData { 1712 document, 1713 profile, 1714 publication_base_path: base_path.clone(), 1715 rendered_html: rendered_html.clone(), 1716 }) 1717 } else { 1718 None 1719 } 1720 })); 1721 (res, memo) 1722} 1723 1724/// Fetches Leaflet document by rkey (client-only) 1725#[cfg(not(feature = "fullstack-server"))] 1726pub fn use_leaflet_document_data( 1727 ident: ReadSignal<AtIdentifier<'static>>, 1728 rkey: ReadSignal<SmolStr>, 1729) -> ( 1730 Resource<Option<crate::fetch::LeafletDocumentData>>, 1731 Memo<Option<crate::fetch::LeafletDocumentData>>, 1732) { 1733 use jacquard::IntoStatic; 1734 use jacquard::prelude::IdentityResolver; 1735 use weaver_api::pub_leaflet::document::{Document, DocumentPagesItem}; 1736 use weaver_api::pub_leaflet::publication::Publication; 1737 use weaver_renderer::leaflet::{LeafletRenderContext, render_linear_document}; 1738 1739 let fetcher = use_context::<crate::fetch::Fetcher>(); 1740 1741 let res = use_resource(move || { 1742 let fetcher = fetcher.clone(); 1743 async move { 1744 let ident = ident(); 1745 let rkey = rkey(); 1746 1747 let uri_str = format!("at://{}/pub.leaflet.document/{}", ident, rkey); 1748 let uri = Document::uri(&uri_str).ok()?; 1749 let record = fetcher.fetch_record(&uri).await.ok()?; 1750 1751 // Fetch publication to get base_path if document has one 1752 let publication_base_path = if let Some(pub_uri) = &record.value.publication { 1753 if let Ok(typed_uri) = Publication::uri(pub_uri.as_ref()) { 1754 if let Ok(pub_record) = fetcher.fetch_record(&typed_uri).await { 1755 // Try typed field first, fall back to extra_data (handles snake_case mismatch) 1756 pub_record 1757 .value 1758 .base_path 1759 .as_ref() 1760 .map(|p| p.as_ref().to_string()) 1761 .or_else(|| { 1762 pub_record 1763 .value 1764 .extra_data 1765 .as_ref() 1766 .and_then(|m| m.get("base_path")) 1767 .and_then(|v| jacquard::from_data::<String>(v).ok()) 1768 }) 1769 } else { 1770 None 1771 } 1772 } else { 1773 None 1774 } 1775 } else { 1776 None 1777 }; 1778 1779 // Render HTML 1780 let rendered_html = { 1781 let author_did = match &record.value.author { 1782 AtIdentifier::Did(d) => d.clone().into_static(), 1783 AtIdentifier::Handle(h) => fetcher.resolve_handle(h).await.ok()?.into_static(), 1784 }; 1785 let ctx = LeafletRenderContext::new(author_did); 1786 let mut html = String::new(); 1787 for page in &record.value.pages { 1788 match page { 1789 DocumentPagesItem::LinearDocument(linear_doc) => { 1790 html.push_str( 1791 &render_linear_document(linear_doc, &ctx, &fetcher).await, 1792 ); 1793 } 1794 DocumentPagesItem::Canvas(_) => { 1795 html.push_str("<div class=\"embed-video-placeholder\">[Canvas layout not yet supported]</div>"); 1796 } 1797 DocumentPagesItem::Unknown(_) => { 1798 html.push_str( 1799 "<div class=\"embed-video-placeholder\">[Unknown page type]</div>", 1800 ); 1801 } 1802 } 1803 } 1804 Some(html) 1805 }; 1806 1807 let profile = fetcher.fetch_profile(&ident).await.ok()?; 1808 1809 Some(crate::fetch::LeafletDocumentData { 1810 document: record.value.into_static(), 1811 profile: (*profile).clone(), 1812 publication_base_path, 1813 rendered_html, 1814 }) 1815 } 1816 }); 1817 1818 let memo = use_memo(move || res.read().clone().flatten()); 1819 (res, memo) 1820} 1821 1822/// Fetches site.standard/blog.pckt document by rkey (SSR) 1823/// 1824/// Supports both `site.standard.document` and `blog.pckt.document` collections. 1825/// For blog.pckt.document, unwraps the inner site.standard.document. 1826#[cfg(all(feature = "fullstack-server", feature = "pckt"))] 1827pub fn use_pckt_document_data( 1828 ident: ReadSignal<AtIdentifier<'static>>, 1829 rkey: ReadSignal<SmolStr>, 1830) -> ( 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 >, 1842 Memo<Option<crate::fetch::PcktDocumentData>>, 1843) { 1844 let fetcher = use_context::<crate::fetch::Fetcher>(); 1845 1846 let res = use_server_future(move || { 1847 let fetcher = fetcher.clone(); 1848 async move { 1849 use jacquard::IntoStatic; 1850 use jacquard::client::AgentSessionExt; 1851 use jacquard::prelude::IdentityResolver; 1852 use weaver_api::site_standard::document::Document as SiteStandardDocument; 1853 use weaver_api::site_standard::publication::Publication; 1854 use weaver_renderer::pckt::{PcktRenderContext, render_document_content}; 1855 1856 let ident = ident(); 1857 let rkey = rkey(); 1858 1859 // Try site.standard.document first, then blog.pckt.document 1860 use jacquard::types::aturi::AtUri; 1861 1862 let doc = { 1863 let uri_str = format!("at://{}/site.standard.document/{}", ident, rkey); 1864 if let Ok(uri) = AtUri::new(&uri_str) { 1865 if let Ok(output) = fetcher.fetch_record_slingshot(&uri).await { 1866 jacquard::from_data::<SiteStandardDocument>(&output.value) 1867 .ok() 1868 .map(|d| d.into_static()) 1869 } else { 1870 None 1871 } 1872 } else { 1873 None 1874 } 1875 }; 1876 1877 let doc = if let Some(d) = doc { 1878 d 1879 } else { 1880 // Try blog.pckt.document 1881 use weaver_api::blog_pckt::document::Document as PcktDocument; 1882 let uri_str = format!("at://{}/blog.pckt.document/{}", ident, rkey); 1883 let uri = PcktDocument::uri(&uri_str).ok()?; 1884 let record = fetcher.fetch_record(&uri).await.ok()?; 1885 record.value.document.into_static() 1886 }; 1887 1888 // Fetch publication to get base URL 1889 use jacquard::types::string::Uri; 1890 let publication_url = if let Uri::At(site_uri) = &doc.site { 1891 if let Ok(pub_record) = fetcher.fetch_record_slingshot(site_uri).await { 1892 jacquard::from_data::<Publication>(&pub_record.value) 1893 .ok() 1894 .map(|p| p.url.as_ref().to_string()) 1895 } else { 1896 None 1897 } 1898 } else { 1899 // Site is an HTTPS URL, use it directly 1900 Some(doc.site.as_str().to_string()) 1901 }; 1902 1903 // Render HTML 1904 let rendered_html = { 1905 let author_did = match &ident { 1906 AtIdentifier::Did(d) => d.clone().into_static(), 1907 AtIdentifier::Handle(h) => fetcher.resolve_handle(h).await.ok()?.into_static(), 1908 }; 1909 let ctx = PcktRenderContext::new(author_did); 1910 if let Some(content) = &doc.content { 1911 Some(render_document_content(content, &ctx, &fetcher).await) 1912 } else { 1913 Some(String::from("<p>No content</p>")) 1914 } 1915 }; 1916 1917 let profile = fetcher.fetch_profile(&ident).await.ok()?; 1918 1919 Some(( 1920 serde_json::to_value(&doc).ok()?, 1921 serde_json::to_value(&*profile).ok()?, 1922 publication_url, 1923 rendered_html, 1924 )) 1925 } 1926 }); 1927 1928 let memo = use_memo(use_reactive!(|res| { 1929 use weaver_api::sh_weaver::actor::ProfileDataView; 1930 use weaver_api::site_standard::document::Document as SiteStandardDocument; 1931 1932 let res = res.as_ref().ok()?; 1933 if let Some(Some((doc_json, profile_json, publication_url, rendered_html))) = &*res.read() { 1934 let document = 1935 jacquard::from_json_value::<SiteStandardDocument>(doc_json.clone()).ok()?; 1936 let profile = 1937 jacquard::from_json_value::<ProfileDataView>(profile_json.clone()).ok()?; 1938 Some(crate::fetch::PcktDocumentData { 1939 document, 1940 profile, 1941 publication_url: publication_url.clone(), 1942 rendered_html: rendered_html.clone(), 1943 }) 1944 } else { 1945 None 1946 } 1947 })); 1948 (res, memo) 1949} 1950 1951/// Fetches site.standard/blog.pckt document by rkey (client-only) 1952#[cfg(all(not(feature = "fullstack-server"), feature = "pckt"))] 1953pub fn use_pckt_document_data( 1954 ident: ReadSignal<AtIdentifier<'static>>, 1955 rkey: ReadSignal<SmolStr>, 1956) -> ( 1957 Resource<Option<crate::fetch::PcktDocumentData>>, 1958 Memo<Option<crate::fetch::PcktDocumentData>>, 1959) { 1960 use jacquard::IntoStatic; 1961 use jacquard::prelude::IdentityResolver; 1962 use weaver_api::site_standard::document::Document as SiteStandardDocument; 1963 use weaver_api::site_standard::publication::Publication; 1964 use weaver_renderer::pckt::{PcktRenderContext, render_document_content}; 1965 1966 let fetcher = use_context::<crate::fetch::Fetcher>(); 1967 1968 let res = use_resource(move || { 1969 let fetcher = fetcher.clone(); 1970 async move { 1971 let ident = ident(); 1972 let rkey = rkey(); 1973 1974 // Try site.standard.document first, then blog.pckt.document 1975 use jacquard::types::aturi::AtUri; 1976 1977 let doc = { 1978 let uri_str = format!("at://{}/site.standard.document/{}", ident, rkey); 1979 if let Ok(uri) = AtUri::new(&uri_str) { 1980 if let Ok(output) = fetcher.fetch_record_slingshot(&uri).await { 1981 jacquard::from_data::<SiteStandardDocument>(&output.value) 1982 .ok() 1983 .map(|d| d.into_static()) 1984 } else { 1985 None 1986 } 1987 } else { 1988 None 1989 } 1990 }; 1991 1992 let doc = if let Some(d) = doc { 1993 d 1994 } else { 1995 // Try blog.pckt.document 1996 use weaver_api::blog_pckt::document::Document as PcktDocument; 1997 let uri_str = format!("at://{}/blog.pckt.document/{}", ident, rkey); 1998 let uri = PcktDocument::uri(&uri_str).ok()?; 1999 let record = fetcher.fetch_record(&uri).await.ok()?; 2000 record.value.document.into_static() 2001 }; 2002 2003 // Fetch publication to get base URL 2004 use jacquard::types::string::Uri; 2005 let publication_url = if let Uri::At(site_uri) = &doc.site { 2006 if let Ok(pub_record) = fetcher.fetch_record_slingshot(site_uri).await { 2007 jacquard::from_data::<Publication>(&pub_record.value) 2008 .ok() 2009 .map(|p| p.url.as_ref().to_string()) 2010 } else { 2011 None 2012 } 2013 } else { 2014 // Site is an HTTPS URL, use it directly 2015 Some(doc.site.as_str().to_string()) 2016 }; 2017 2018 // Render HTML 2019 let rendered_html = { 2020 let author_did = match &ident { 2021 AtIdentifier::Did(d) => d.clone().into_static(), 2022 AtIdentifier::Handle(h) => fetcher.resolve_handle(h).await.ok()?.into_static(), 2023 }; 2024 let ctx = PcktRenderContext::new(author_did); 2025 if let Some(content) = &doc.content { 2026 Some(render_document_content(content, &ctx, &fetcher).await) 2027 } else { 2028 Some(String::from("<p>No content</p>")) 2029 } 2030 }; 2031 2032 let profile = fetcher.fetch_profile(&ident).await.ok()?; 2033 2034 Some(crate::fetch::PcktDocumentData { 2035 document: doc, 2036 profile: (*profile).clone(), 2037 publication_url, 2038 rendered_html, 2039 }) 2040 } 2041 }); 2042 2043 let memo = use_memo(move || res.read().clone().flatten()); 2044 (res, memo) 2045} 2046 2047#[cfg(feature = "fullstack-server")] 2048#[put("/cache/{ident}/{cid}?name", cache: Extension<Arc<BlobCache>>)] 2049pub async fn cache_blob(ident: SmolStr, cid: SmolStr, name: Option<SmolStr>) -> Result<()> { 2050 let ident = AtIdentifier::new_owned(ident)?; 2051 let cid = Cid::new_owned(cid.as_bytes())?; 2052 cache.cache(ident, cid, name).await 2053} 2054 2055/// Cache blob bytes directly (for pre-warming after upload). 2056/// If `notebook` is provided, uses scoped cache key `{notebook}_{name}`. 2057#[cfg(feature = "fullstack-server")] 2058#[put("/cache-bytes/{cid}?name&notebook", cache: Extension<Arc<BlobCache>>)] 2059pub async fn cache_blob_bytes( 2060 cid: SmolStr, 2061 name: Option<SmolStr>, 2062 notebook: Option<SmolStr>, 2063 body: jacquard::bytes::Bytes, 2064) -> Result<()> { 2065 let cid = Cid::new_owned(cid.as_bytes())?; 2066 let cache_key = match (&notebook, &name) { 2067 (Some(nb), Some(n)) => Some(format_smolstr!("{}_{}", nb, n)), 2068 (None, Some(n)) => Some(n.clone()), 2069 _ => None, 2070 }; 2071 cache.insert_bytes(cid, body, cache_key); 2072 Ok(()) 2073} 2074 2075// ============================================================================ 2076// Custom Domain Document Resolution 2077// ============================================================================ 2078 2079use weaver_api::sh_weaver::domain::DocumentView; 2080 2081/// Typed data returned from custom domain document resolution. 2082#[derive(Clone, Debug, PartialEq)] 2083pub struct CustomDomainDocumentData { 2084 pub document: DocumentView<'static>, 2085 pub rendered_html: String, 2086} 2087 2088#[cfg(feature = "fullstack-server")] 2089pub fn use_custom_domain_document_data( 2090 ident: ReadSignal<AtIdentifier<'static>>, 2091 publication_rkey: ReadSignal<SmolStr>, 2092 path: ReadSignal<String>, 2093) -> ( 2094 Result<Resource<Option<(serde_json::Value, String)>>, RenderError>, 2095 Memo<Option<CustomDomainDocumentData>>, 2096) { 2097 let fetcher = use_context::<crate::fetch::Fetcher>(); 2098 let fetcher = fetcher.clone(); 2099 2100 let res = use_server_future(use_reactive!(|(ident, publication_rkey, path)| { 2101 let fetcher = fetcher.clone(); 2102 async move { 2103 use jacquard::prelude::XrpcClient; 2104 use jacquard::smol_str::format_smolstr; 2105 use jacquard::types::aturi::AtUri; 2106 use weaver_api::sh_weaver::domain::resolve_document::ResolveDocument; 2107 use weaver_api::site_standard::document::Document; 2108 use weaver_renderer::pckt::{PcktRenderContext, render_document_content}; 2109 2110 let ident_val = ident(); 2111 let rkey = publication_rkey(); 2112 let path_val = path(); 2113 2114 let author_did = match &ident_val { 2115 AtIdentifier::Did(d) => d.clone().into_static(), 2116 AtIdentifier::Handle(h) => fetcher.resolve_handle(h).await.ok()?.into_static(), 2117 }; 2118 2119 let pub_uri_str = 2120 format_smolstr!("at://{}/site.standard.publication/{}", author_did, rkey); 2121 let pub_uri = AtUri::new(&pub_uri_str).ok()?; 2122 2123 let output = fetcher 2124 .send( 2125 ResolveDocument::new() 2126 .publication(pub_uri) 2127 .path(&path_val) 2128 .build(), 2129 ) 2130 .await 2131 .ok()? 2132 .into_output() 2133 .ok()?; 2134 2135 let doc_view = output.document; 2136 let document = jacquard::from_data::<Document>(&doc_view.record).ok()?; 2137 2138 let rendered_html = if let Some(content) = &document.content { 2139 let ctx = PcktRenderContext::new(author_did); 2140 render_document_content(content, &ctx, &*fetcher.get_client()).await 2141 } else { 2142 String::new() 2143 }; 2144 2145 Some((serde_json::to_value(&doc_view).ok()?, rendered_html)) 2146 } 2147 })); 2148 2149 let memo = use_memo(use_reactive!(|res| { 2150 let res = res.as_ref().ok()?; 2151 if let Some(Some((doc_json, html))) = &*res.read() { 2152 let document = jacquard::from_json_value::<DocumentView>(doc_json.clone()).ok()?; 2153 Some(CustomDomainDocumentData { 2154 document, 2155 rendered_html: html.clone(), 2156 }) 2157 } else { 2158 None 2159 } 2160 })); 2161 2162 (res, memo) 2163} 2164 2165#[cfg(not(feature = "fullstack-server"))] 2166pub fn use_custom_domain_document_data( 2167 ident: ReadSignal<AtIdentifier<'static>>, 2168 publication_rkey: ReadSignal<SmolStr>, 2169 path: ReadSignal<String>, 2170) -> ( 2171 Resource<Option<CustomDomainDocumentData>>, 2172 Memo<Option<CustomDomainDocumentData>>, 2173) { 2174 let fetcher = use_context::<crate::fetch::Fetcher>(); 2175 let fetcher = fetcher.clone(); 2176 2177 let res = use_resource(move || { 2178 let fetcher = fetcher.clone(); 2179 async move { 2180 use jacquard::IntoStatic; 2181 use jacquard::prelude::XrpcClient; 2182 use jacquard::smol_str::format_smolstr; 2183 use jacquard::types::aturi::AtUri; 2184 use weaver_api::sh_weaver::domain::resolve_document::ResolveDocument; 2185 use weaver_api::site_standard::document::Document; 2186 use weaver_renderer::pckt::{PcktRenderContext, render_document_content}; 2187 2188 let ident_val = ident(); 2189 let rkey = publication_rkey(); 2190 let path_val = path(); 2191 2192 let author_did = match &ident_val { 2193 AtIdentifier::Did(d) => d.clone().into_static(), 2194 AtIdentifier::Handle(h) => fetcher.resolve_handle(h).await.ok()?.into_static(), 2195 }; 2196 2197 let pub_uri_str = 2198 format_smolstr!("at://{}/site.standard.publication/{}", author_did, rkey); 2199 let pub_uri = AtUri::new(&pub_uri_str).ok()?; 2200 2201 let output = fetcher 2202 .send( 2203 ResolveDocument::new() 2204 .publication(pub_uri) 2205 .path(&path_val) 2206 .build(), 2207 ) 2208 .await 2209 .ok()? 2210 .into_output() 2211 .ok()?; 2212 2213 let doc_view = output.document.into_static(); 2214 let document = jacquard::from_data::<Document>(&doc_view.record).ok()?; 2215 2216 let rendered_html = if let Some(content) = &document.content { 2217 let ctx = PcktRenderContext::new(author_did); 2218 render_document_content(content, &ctx, &*fetcher.get_client()).await 2219 } else { 2220 String::new() 2221 }; 2222 2223 Some(CustomDomainDocumentData { 2224 document: doc_view.clone(), 2225 rendered_html, 2226 }) 2227 } 2228 }); 2229 2230 let memo = use_memo(move || res.cloned().flatten()); 2231 2232 (res, memo) 2233}