diff --git a/Cargo.lock b/Cargo.lock index 5e11320..f1150ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4156,6 +4156,7 @@ dependencies = [ "n0-future", "ouroboros", "p256", + "postcard", "rand 0.9.2", "regex", "regex-lite", diff --git a/crates/weaver-app/.env-dev b/crates/weaver-app/.env-prod similarity index 59% rename from crates/weaver-app/.env-dev rename to crates/weaver-app/.env-prod index 5d82bc4..dc4c3dd 100644 --- a/crates/weaver-app/.env-dev +++ b/crates/weaver-app/.env-prod @@ -1,6 +1,6 @@ -WEAVER_APP_ENV="dev" -WEAVER_APP_HOST="http://localhost" -WEAVER_APP_DOMAIN="" +WEAVER_APP_ENV="prod" +WEAVER_APP_HOST="https://alpha.weaver.sh" +WEAVER_APP_DOMAIN="https://alpha.weaver.sh" WEAVER_PORT=8080 WEAVER_APP_SCOPES="atproto transition:generic" WEAVER_CLIENT_NAME="Weaver" diff --git a/crates/weaver-app/Cargo.toml b/crates/weaver-app/Cargo.toml index b04f998..43a13ed 100644 --- a/crates/weaver-app/Cargo.toml +++ b/crates/weaver-app/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Orual "] edition = "2024" [features] -default = ["web", "fullstack-server", "no-app-index"] +default = ["web", "no-app-index"] # Fullstack mode with SSR and server functions fullstack-server = ["dioxus/fullstack"] wasm-split = ["dioxus/wasm-split"] diff --git a/crates/weaver-app/src/components/entry.rs b/crates/weaver-app/src/components/entry.rs index d3f8b17..56d7a05 100644 --- a/crates/weaver-app/src/components/entry.rs +++ b/crates/weaver-app/src/components/entry.rs @@ -42,7 +42,7 @@ pub fn Entry( title: ReadSignal, ) -> Element { // Use feature-gated hook for SSR support - let entry = crate::data::use_entry_data(ident(), book_title(), title()); + let entry = crate::data::use_entry_data(ident, book_title, title); let handle = use_context::(); let fetcher = use_context::(); @@ -433,11 +433,11 @@ pub struct EntryMarkdownProps { #[allow(unused)] pub fn EntryMarkdown(props: EntryMarkdownProps) -> Element { let processed = crate::data::use_rendered_markdown( - props.content.read().clone(), - props.ident.read().clone(), + props.content, + props.ident, )?; - match &*processed.read_unchecked() { + match &*processed.read() { Some(Some(html_buf)) => rsx! { div { id: "{&*props.id.read()}", @@ -464,9 +464,11 @@ fn EntryMarkdownDirect( ident: AtIdentifier<'static>, ) -> Element { // Use feature-gated hook for SSR support - let processed = crate::data::use_rendered_markdown(content, ident)?; + let content = use_signal(|| content); + let ident = use_signal(|| ident); + let processed = crate::data::use_rendered_markdown(content.into(), ident.into())?; - match &*processed.read_unchecked() { + match &*processed.read() { Some(Some(html_buf)) => rsx! { div { id: "{id}", diff --git a/crates/weaver-app/src/components/identity.rs b/crates/weaver-app/src/components/identity.rs index 4bf4c97..1b873ab 100644 --- a/crates/weaver-app/src/components/identity.rs +++ b/crates/weaver-app/src/components/identity.rs @@ -9,7 +9,7 @@ const NOTEBOOK_CARD_CSS: Asset = asset!("/assets/styling/notebook-card.css"); #[component] pub fn Repository(ident: ReadSignal>) -> Element { // Fetch notebooks for this specific DID with SSR support - let notebooks = data::use_notebooks_for_did(ident())?; + let notebooks = data::use_notebooks_for_did(ident)?; use_context_provider(|| notebooks); rsx! { div { @@ -19,21 +19,22 @@ pub fn Repository(ident: ReadSignal>) -> Element { } pub fn use_repo_notebook_context() --> Option, Vec>)>>>> { - try_use_context::, Vec>)>>>>() +-> Option, Vec>)>>>> { + try_use_context::, Vec>)>>>>() } #[component] pub fn RepositoryIndex(ident: ReadSignal>) -> Element { use crate::components::ProfileDisplay; let notebooks = use_repo_notebook_context(); + let profile = crate::data::use_profile_data(ident)?; rsx! { document::Stylesheet { href: NOTEBOOK_CARD_CSS } div { class: "repository-layout", // Profile sidebar (desktop) / header (mobile) aside { class: "repository-sidebar", - ProfileDisplay { ident: ident() } + ProfileDisplay { profile } } // Main content area @@ -41,7 +42,7 @@ pub fn RepositoryIndex(ident: ReadSignal>) -> Element { div { class: "notebooks-list", if let Some(notebooks) = notebooks { match &*notebooks.read() { - Some(Some(notebook_list)) => rsx! { + Some(notebook_list) => rsx! { for notebook in notebook_list.iter() { { let view = ¬ebook.0; @@ -58,7 +59,7 @@ pub fn RepositoryIndex(ident: ReadSignal>) -> Element { } } }, - _ => rsx! { + None => rsx! { div { "Loading notebooks..." } } } diff --git a/crates/weaver-app/src/components/profile.rs b/crates/weaver-app/src/components/profile.rs index faacb3d..df5bf62 100644 --- a/crates/weaver-app/src/components/profile.rs +++ b/crates/weaver-app/src/components/profile.rs @@ -1,30 +1,31 @@ #![allow(non_snake_case)] +use std::sync::Arc; + use crate::components::{ BskyIcon, TangledIcon, avatar::{Avatar, AvatarImage}, identity::use_repo_notebook_context, }; use dioxus::prelude::*; -use jacquard::types::ident::AtIdentifier; -use weaver_api::sh_weaver::actor::ProfileDataViewInner; +use weaver_api::com_atproto::repo::strong_ref::StrongRef; +use weaver_api::sh_weaver::actor::{ProfileDataView, ProfileDataViewInner}; +use weaver_common::agent::NotebookView; const PROFILE_CSS: Asset = asset!("/assets/styling/profile.css"); #[component] -pub fn ProfileDisplay(ident: ReadSignal>) -> Element { - // Fetch profile data - let profile = crate::data::use_profile_data(ident()); - - match &*profile?.read() { +pub fn ProfileDisplay(profile: Memo>>) -> Element { + let notebooks = use_repo_notebook_context(); + match &*profile.read() { Some(profile_view) => { - let profile_view = use_signal(|| profile_view.clone()); + let profile_view = Arc::new(profile_view.clone()); rsx! { document::Stylesheet { href: PROFILE_CSS } div { class: "profile-display", // Banner if present - {match &profile_view.read().inner { + {match &profile_view.inner { ProfileDataViewInner::ProfileView(p) => { if let Some(ref banner) = p.banner { rsx! { @@ -52,14 +53,14 @@ pub fn ProfileDisplay(ident: ReadSignal>) -> Element { div { class: "profile-content", // Avatar and identity - ProfileIdentity { profile_view, ident } + ProfileIdentity { profile_view: profile_view.clone() } div { class: "profile-extras", // Stats - ProfileStats { ident } + ProfileStats { notebooks: notebooks.unwrap() } // Links - ProfileLinks { profile_view, ident } + ProfileLinks { profile_view } } @@ -76,11 +77,8 @@ pub fn ProfileDisplay(ident: ReadSignal>) -> Element { } #[component] -fn ProfileIdentity( - profile_view: ReadSignal>, - ident: ReadSignal>, -) -> Element { - match &profile_view.read().inner { +fn ProfileIdentity(profile_view: Arc>) -> Element { + match &profile_view.inner { ProfileDataViewInner::ProfileView(profile) => { let display_name = profile .display_name @@ -171,7 +169,7 @@ fn ProfileIdentity( div { class: "profile-identity", div { class: "profile-name-section", h1 { class: "profile-display-name", "@{profile.handle.as_ref()}" } - div { class: "profile-handle", "{ident}" } + //div { class: "profile-handle", "{profile.handle.as_ref()}" } if let Some(ref location) = profile.location { div { class: "profile-location", "{location}" } @@ -193,16 +191,12 @@ fn ProfileIdentity( } #[component] -fn ProfileStats(ident: ReadSignal>) -> Element { +fn ProfileStats( + notebooks: Memo, Vec>)>>>, +) -> Element { // Fetch notebook count - let notebooks = use_repo_notebook_context(); - - let notebook_count = if let Some(notebooks) = notebooks { - if let Some(Some(notebooks)) = &*notebooks.read() { - notebooks.len() - } else { - 0 - } + let notebook_count = if let Some(notebooks) = &*notebooks.read() { + notebooks.len() } else { 0 }; @@ -218,12 +212,8 @@ fn ProfileStats(ident: ReadSignal>) -> Element { } #[component] -fn ProfileLinks( - profile_view: ReadSignal>, - - ident: ReadSignal>, -) -> Element { - match &profile_view.read().inner { +fn ProfileLinks(profile_view: Arc>) -> Element { + match &profile_view.inner { ProfileDataViewInner::ProfileView(profile) => { rsx! { div { class: "profile-links", @@ -243,7 +233,7 @@ fn ProfileLinks( // Platform-specific links if profile.bluesky.unwrap_or(false) { a { - href: "https://bsky.app/profile/{ident}", + href: "https://bsky.app/profile/{profile.did}", target: "_blank", rel: "noopener noreferrer", class: "profile-link profile-link-platform", @@ -254,7 +244,7 @@ fn ProfileLinks( if profile.tangled.unwrap_or(false) { a { - href: "https://tangled.org/@{ident}", + href: "https://tangled.org/{profile.did}", target: "_blank", rel: "noopener noreferrer", class: "profile-link profile-link-platform", @@ -265,7 +255,7 @@ fn ProfileLinks( if profile.streamplace.unwrap_or(false) { a { - href: "https://stream.place/{ident}", + href: "https://stream.place/{profile.did}", target: "_blank", rel: "noopener noreferrer", class: "profile-link profile-link-platform", @@ -275,12 +265,12 @@ fn ProfileLinks( } } } - ProfileDataViewInner::ProfileViewDetailed(_profile) => { + ProfileDataViewInner::ProfileViewDetailed(profile) => { // Bluesky ProfileViewDetailed - doesn't have weaver platform flags rsx! { div { class: "profile-links", a { - href: "https://bsky.app/profile/{ident}", + href: "https://bsky.app/profile/{profile.did}", target: "_blank", rel: "noopener noreferrer", class: "profile-link profile-link-platform", @@ -306,7 +296,7 @@ fn ProfileLinks( } } a { - href: "https://tangled.org/@{ident}", + href: "https://tangled.org/{profile.did}", target: "_blank", rel: "noopener noreferrer", class: "profile-link profile-link-platform", @@ -316,7 +306,7 @@ fn ProfileLinks( if profile.bluesky { a { - href: "https://bsky.app/profile/{ident}", + href: "https://bsky.app/profile/{profile.did}", target: "_blank", rel: "noopener noreferrer", class: "profile-link profile-link-platform", diff --git a/crates/weaver-app/src/data.rs b/crates/weaver-app/src/data.rs index 7a4477d..ae8844b 100644 --- a/crates/weaver-app/src/data.rs +++ b/crates/weaver-app/src/data.rs @@ -21,7 +21,6 @@ use jacquard::{ smol_str::SmolStr, types::{cid::Cid, string::AtIdentifier}, }; -use std::cell::Ref; #[allow(unused_imports)] use std::sync::Arc; use weaver_api::com_atproto::repo::strong_ref::StrongRef; @@ -34,20 +33,17 @@ use weaver_api::sh_weaver::notebook::{BookEntryView, NotebookView, entry::Entry} /// Fetches entry data with SSR support in fullstack mode. #[cfg(feature = "fullstack-server")] pub fn use_entry_data( - ident: AtIdentifier<'static>, - book_title: SmolStr, - title: SmolStr, + ident: ReadSignal>, + book_title: ReadSignal, + title: ReadSignal, ) -> Result, Entry<'static>)>>, RenderError> { let fetcher = use_context::(); let fetcher = fetcher.clone(); - let ident = use_signal(|| ident); - let book_title = use_signal(|| book_title); - let entry_title = use_signal(|| title); let res = use_server_future(move || { let fetcher = fetcher.clone(); async move { if let Some(entry) = fetcher - .get_entry(ident(), book_title(), entry_title()) + .get_entry(ident(), book_title(), title()) .await .ok() .flatten() @@ -55,14 +51,14 @@ pub fn use_entry_data( let (_book_entry_view, entry_record) = (&entry.0, &entry.1); if let Some(embeds) = &entry_record.embeds { if let Some(images) = &embeds.images { - let ident = ident.clone(); + let ident_val = ident(); let images = images.clone(); for image in &images.images { use jacquard::smol_str::ToSmolStr; let cid = image.image.blob().cid(); cache_blob( - ident.to_smolstr(), + ident_val.to_smolstr(), cid.to_smolstr(), image.name.as_ref().map(|n| n.to_smolstr()), ) @@ -79,84 +75,76 @@ pub fn use_entry_data( None } } - }); - res.map(|r| { - use_memo(move || { - if let Some(Some((ev, e))) = &*r.read_unchecked() { - use jacquard::from_json_value; + })?; // Handle the Result first to avoid calling use_memo in a closure + Ok(use_memo(move || { + if let Some(Some((ev, e))) = &*res.read() { + use jacquard::from_json_value; - let book_entry = from_json_value::(ev.clone()).unwrap(); - let entry = from_json_value::(e.clone()).unwrap(); + let book_entry = from_json_value::(ev.clone()).unwrap(); + let entry = from_json_value::(e.clone()).unwrap(); - Some((book_entry, entry)) - } else { - None - } - }) - }) + Some((book_entry, entry)) + } else { + None + } + })) } /// Fetches entry data client-side only (no SSR). #[cfg(not(feature = "fullstack-server"))] pub fn use_entry_data( - ident: AtIdentifier<'static>, - book_title: SmolStr, - title: SmolStr, + ident: ReadSignal>, + book_title: ReadSignal, + title: ReadSignal, ) -> Result, Entry<'static>)>>, RenderError> { let fetcher = use_context::(); let fetcher = fetcher.clone(); - let ident = use_signal(|| ident); - let book_title = use_signal(|| book_title); - let entry_title = use_signal(|| title); let res = use_resource(move || { let fetcher = fetcher.clone(); async move { fetcher - .get_entry(ident(), book_title(), entry_title()) + .get_entry(ident(), book_title(), title()) .await .ok() .flatten() .map(|arc| { ( - serde_json::to_value(entry.0.clone()).unwrap(), - serde_json::to_value(entry.1.clone()).unwrap(), + serde_json::to_value(arc.0.clone()).unwrap(), + serde_json::to_value(arc.1.clone()).unwrap(), ) }) } }); - res.map(|r| { - use_memo(move || { - if let Some(Some((ev, e))) = &*r.read_unchecked() { - use jacquard::from_json_value; + res.suspend()?; + Ok(use_memo(move || { + if let Some(Some((ev, e))) = &*res.read() { + use jacquard::from_json_value; - let book_entry = from_json_value::(ev.clone()).unwrap(); - let entry = from_json_value::(e.clone()).unwrap(); + let book_entry = from_json_value::(ev.clone()).unwrap(); + let entry = from_json_value::(e.clone()).unwrap(); - Some((book_entry, entry)) - } else { - None - } - }) - }) + Some((book_entry, entry)) + } else { + None + } + })) } -pub fn get_handle(did: Did<'static>) -> AtIdentifier<'static> { - let ident = AtIdentifier::Did(did); - use_handle(ident.clone()) +pub fn use_get_handle(did: Did<'static>) -> AtIdentifier<'static> { + let ident = use_signal(use_reactive!(|did| AtIdentifier::Did(did.clone()))); + use_handle(ident.into()) .read() .as_ref() - .unwrap_or(&Ok(ident)) + .unwrap_or(&Ok(ident.read().clone())) .as_ref() .unwrap() .clone() } pub fn use_handle( - ident: AtIdentifier<'static>, + ident: ReadSignal>, ) -> Resource, IdentityError>> { let fetcher = use_context::(); let fetcher = fetcher.clone(); - let ident = use_signal(|| ident); - use_resource(move || { let client = fetcher.get_client(); async move { @@ -184,7 +172,8 @@ impl NotebookHandle { pub fn use_notebook_handle(ident: Signal>>) -> NotebookHandle { let ident = if let Some(ident) = &*ident.read() { - if let Some(Ok(handle)) = &*use_handle(ident.clone()).read() { + let _ident = Signal::new(ident.clone()); + if let Some(Ok(handle)) = &*use_handle(_ident.into()).read() { Some(handle.clone()) } else { Some(ident.clone()) @@ -195,14 +184,12 @@ pub fn use_notebook_handle(ident: Signal>>) -> Note use_context_provider(|| NotebookHandle(Arc::new(ident))) } -/// Hook to render markdown client-side only (no SSR). +/// Hook to render markdown with SSR support. #[cfg(feature = "fullstack-server")] pub fn use_rendered_markdown( - content: Entry<'static>, - ident: AtIdentifier<'static>, + content: ReadSignal>, + ident: ReadSignal>, ) -> Result>, RenderError> { - let ident = use_signal(|| ident); - let content = use_signal(|| content); let fetcher = use_context::(); Ok(use_server_future(move || { let client = fetcher.get_client(); @@ -219,11 +206,9 @@ pub fn use_rendered_markdown( /// Hook to render markdown client-side only (no SSR). #[cfg(not(feature = "fullstack-server"))] pub fn use_rendered_markdown( - content: Entry<'static>, - ident: AtIdentifier<'static>, + content: ReadSignal>, + ident: ReadSignal>, ) -> Result>, RenderError> { - let ident = use_signal(|| ident); - let content = use_signal(|| content); let fetcher = use_context::(); Ok(use_resource(move || { let client = fetcher.get_client(); @@ -261,10 +246,9 @@ async fn render_markdown_impl(content: Entry<'static>, did: Did<'static>) -> Str /// Fetches profile data for a given identifier #[cfg(feature = "fullstack-server")] pub fn use_profile_data( - ident: AtIdentifier<'static>, + ident: ReadSignal>, ) -> Result>>, RenderError> { let fetcher = use_context::(); - let ident = use_signal(|| ident); let res = use_server_future(move || { let fetcher = fetcher.clone(); async move { @@ -277,7 +261,7 @@ pub fn use_profile_data( } })?; Ok(use_memo(move || { - if let Some(Some(value)) = &*res.read_unchecked() { + if let Some(Some(value)) = &*res.read() { jacquard::from_json_value::(value.clone()).ok() } else { None @@ -288,10 +272,9 @@ pub fn use_profile_data( /// Fetches profile data client-side only (no SSR) #[cfg(not(feature = "fullstack-server"))] pub fn use_profile_data( - ident: AtIdentifier<'static>, + ident: ReadSignal>, ) -> Result>>, RenderError> { let fetcher = use_context::(); - let ident = use_signal(|| ident); let res = use_resource(move || { let fetcher = fetcher.clone(); async move { @@ -303,8 +286,9 @@ pub fn use_profile_data( .flatten() } }); + res.suspend()?; Ok(use_memo(move || { - if let Some(Some(value)) = &*res.read_unchecked() { + if let Some(Some(value)) = &*res.read() { jacquard::from_json_value::(value.clone()).ok() } else { None @@ -315,10 +299,9 @@ pub fn use_profile_data( /// Fetches notebooks for a specific DID #[cfg(feature = "fullstack-server")] pub fn use_notebooks_for_did( - ident: AtIdentifier<'static>, + ident: ReadSignal>, ) -> Result, Vec>)>>>, RenderError> { let fetcher = use_context::(); - let ident = use_signal(|| ident); let res = use_server_future(move || { let fetcher = fetcher.clone(); async move { @@ -336,7 +319,7 @@ pub fn use_notebooks_for_did( } })?; Ok(use_memo(move || { - if let Some(Some(values)) = &*res.read_unchecked() { + if let Some(Some(values)) = &*res.read() { values .iter() .map(|v| { @@ -352,10 +335,9 @@ pub fn use_notebooks_for_did( /// Fetches notebooks client-side only (no SSR) #[cfg(not(feature = "fullstack-server"))] pub fn use_notebooks_for_did( - ident: AtIdentifier<'static>, + ident: ReadSignal>, ) -> Result, Vec>)>>>, RenderError> { let fetcher = use_context::(); - let ident = use_signal(|| ident); let res = use_resource(move || { let fetcher = fetcher.clone(); async move { @@ -372,8 +354,9 @@ pub fn use_notebooks_for_did( .flatten() } }); + res.suspend()?; Ok(use_memo(move || { - if let Some(Some(values)) = &*res.read_unchecked() { + if let Some(Some(values)) = &*res.read() { values .iter() .map(|v| { @@ -408,7 +391,7 @@ pub fn use_notebooks_from_ufos() } })?; Ok(use_memo(move || { - if let Some(Some(values)) = &*res.read_unchecked() { + if let Some(Some(values)) = &*res.read() { values .iter() .map(|v| { @@ -442,8 +425,9 @@ pub fn use_notebooks_from_ufos() .flatten() } }); + res.suspend()?; Ok(use_memo(move || { - if let Some(Some(values)) = &*res.read_unchecked() { + if let Some(Some(values)) = &*res.read() { values .iter() .map(|v| { @@ -459,12 +443,10 @@ pub fn use_notebooks_from_ufos() /// Fetches notebook metadata with SSR support in fullstack mode #[cfg(feature = "fullstack-server")] pub fn use_notebook( - ident: AtIdentifier<'static>, - book_title: SmolStr, + ident: ReadSignal>, + book_title: ReadSignal, ) -> Result, Vec>)>>, RenderError> { let fetcher = use_context::(); - let ident = use_signal(|| ident); - let book_title = use_signal(|| book_title); let res = use_server_future(move || { let fetcher = fetcher.clone(); async move { @@ -478,7 +460,7 @@ pub fn use_notebook( } })?; Ok(use_memo(move || { - if let Some(Some(value)) = &*res.read_unchecked() { + if let Some(Some(value)) = &*res.read() { jacquard::from_json_value::<(NotebookView, Vec)>(value.clone()).ok() } else { None @@ -489,12 +471,10 @@ pub fn use_notebook( /// Fetches notebook metadata client-side only (no SSR) #[cfg(not(feature = "fullstack-server"))] pub fn use_notebook( - ident: AtIdentifier<'static>, - book_title: SmolStr, + ident: ReadSignal>, + book_title: ReadSignal, ) -> Result, Vec>)>>, RenderError> { let fetcher = use_context::(); - let ident = use_signal(|| ident); - let book_title = use_signal(|| book_title); let res = use_resource(move || { let fetcher = fetcher.clone(); async move { @@ -507,8 +487,9 @@ pub fn use_notebook( .flatten() } }); + res.suspend()?; Ok(use_memo(move || { - if let Some(Some(value)) = &*res.read_unchecked() { + if let Some(Some(value)) = &*res.read() { jacquard::from_json_value::<(NotebookView, Vec)>(value.clone()).ok() } else { None @@ -519,12 +500,10 @@ pub fn use_notebook( /// Fetches notebook entries with SSR support in fullstack mode #[cfg(feature = "fullstack-server")] pub fn use_notebook_entries( - ident: AtIdentifier<'static>, - book_title: SmolStr, + ident: ReadSignal>, + book_title: ReadSignal, ) -> Result>>>, RenderError> { let fetcher = use_context::(); - let ident = use_signal(|| ident); - let book_title = use_signal(|| book_title); let res = use_server_future(move || { let fetcher = fetcher.clone(); async move { @@ -542,8 +521,9 @@ pub fn use_notebook_entries( .flatten() } })?; + res.suspend()?; Ok(use_memo(move || { - if let Some(Some(values)) = &*res.read_unchecked() { + if let Some(Some(values)) = &*res.read() { values .iter() .map(|v| jacquard::from_json_value::(v.clone()).ok()) @@ -557,23 +537,22 @@ pub fn use_notebook_entries( /// Fetches notebook entries client-side only (no SSR) #[cfg(not(feature = "fullstack-server"))] pub fn use_notebook_entries( - ident: AtIdentifier<'static>, - book_title: SmolStr, + ident: ReadSignal>, + book_title: ReadSignal, ) -> Result>>>, RenderError> { let fetcher = use_context::(); - let r = use_resource(use_reactive!(|(ident, book_title)| { + let r = use_resource(move || { let fetcher = fetcher.clone(); async move { fetcher - .list_notebook_entries(ident, book_title) + .list_notebook_entries(ident(), book_title()) .await .ok() .flatten() } - })); - Ok(use_memo(move || { - r.read_unchecked().as_ref().and_then(|v| v.clone()) - })) + }); + r.suspend()?; + Ok(use_memo(move || r.read().as_ref().and_then(|v| v.clone()))) } #[cfg(feature = "fullstack-server")] diff --git a/crates/weaver-app/src/views/navbar.rs b/crates/weaver-app/src/views/navbar.rs index 299b944..d4f6a46 100644 --- a/crates/weaver-app/src/views/navbar.rs +++ b/crates/weaver-app/src/views/navbar.rs @@ -1,9 +1,10 @@ use crate::Route; use crate::components::button::{Button, ButtonVariant}; use crate::components::login::LoginModal; -use crate::data::{get_handle, use_notebook_handle}; +use crate::data::{use_get_handle, use_notebook_handle}; use crate::fetch::Fetcher; use dioxus::prelude::*; +use jacquard::types::did::Did; const THEME_DEFAULTS_CSS: Asset = asset!("/assets/styling/theme-defaults.css"); const NAVBAR_CSS: Asset = asset!("/assets/styling/navbar.css"); @@ -16,9 +17,7 @@ const NAVBAR_CSS: Asset = asset!("/assets/styling/navbar.css"); #[component] pub fn Navbar() -> Element { let route = use_route::(); - let mut auth_state = use_context::>(); - let mut show_login_modal = use_signal(|| false); - let fetcher = use_context::(); + let route_handle = use_signal(|| match &route { Route::EntryPage { ident, .. } => Some(ident.clone()), Route::RepositoryIndex { ident } => Some(ident.clone()), @@ -82,37 +81,50 @@ pub fn Navbar() -> Element { _ => rsx! {} } } - if auth_state.read().is_authenticated() { - if let Some(did) = &auth_state.read().did { - Button { - variant: ButtonVariant::Ghost, - onclick: move |_| { - let fetcher = fetcher.clone(); - auth_state.write().clear(); - async move { - fetcher.downgrade_to_unauthenticated().await; - } - }, - span { class: "auth-handle", "@{get_handle(did.clone())}" } - } - } + LoginButton {} + + } + + Outlet:: {} + } +} - } else { - div { - class: "auth-button", - Button { - variant: ButtonVariant::Ghost, - onclick: move |_| show_login_modal.set(true), - span { class: "auth-handle", "Sign In" } +#[component] +fn LoginButton() -> Element { + let fetcher = use_context::(); + let mut show_login_modal = use_signal(|| false); + let mut auth_state = use_context::>(); + let did_signal = use_signal(|| auth_state.read().did.clone()); + if let Some(did) = &*did_signal.read() { + rsx! { + Button { + variant: ButtonVariant::Ghost, + onclick: move |_| { + let fetcher = fetcher.clone(); + auth_state.write().clear(); + async move { + fetcher.downgrade_to_unauthenticated().await; } + }, + span { class: "auth-handle", "@{use_get_handle(did.clone())}" } + } + LoginModal { + open: show_login_modal + } + } + } else { + rsx! { + div { + class: "auth-button", + Button { + variant: ButtonVariant::Ghost, + onclick: move |_| show_login_modal.set(true), + span { class: "auth-handle", "Sign In" } } - } LoginModal { open: show_login_modal } } - - Outlet:: {} } } diff --git a/crates/weaver-app/src/views/notebook.rs b/crates/weaver-app/src/views/notebook.rs index 916c468..23a45a0 100644 --- a/crates/weaver-app/src/views/notebook.rs +++ b/crates/weaver-app/src/views/notebook.rs @@ -29,10 +29,13 @@ pub fn NotebookIndex( book_title: ReadSignal, ) -> Element { // Fetch full notebook metadata with SSR support - let notebook_data = data::use_notebook(ident(), book_title())?; + // IMPORTANT: Call ALL hooks before any ? early returns to maintain hook order + let notebook_data = data::use_notebook(ident, book_title); + let entries_resource = data::use_notebook_entries(ident, book_title); - // Fetch entries with SSR support - let entries_resource = data::use_notebook_entries(ident(), book_title())?; + // Now check for errors + let notebook_data = notebook_data?; + let entries_resource = entries_resource?; rsx! { document::Link { rel: "stylesheet", href: ENTRY_CARD_CSS }