diff --git a/crates/weaver-app/src/components/author_list/author.css b/crates/weaver-app/src/components/author_list/author.css new file mode 100644 index 0000000..37ec4da --- /dev/null +++ b/crates/weaver-app/src/components/author_list/author.css @@ -0,0 +1,105 @@ +/* AuthorList Component Styles */ + +.author-list { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + position: relative; +} + +.author-list-full { + flex-direction: row; + align-items: center; + gap: 1rem; +} + +.author-list-compact, +.author-list-collapsed { + flex-direction: row; + align-items: center; + cursor: pointer; +} + +.author-list-compact:hover, +.author-list-collapsed:hover { + text-decoration: underline; + text-decoration-color: var(--color-muted); +} + +/* Author block (full display with avatar) */ +.author-block { + display: flex; + align-items: center; + gap: 0.5rem; + text-decoration: none; + color: inherit; +} + +.author-block:hover .embed-author-name { + color: var(--color-secondary); +} + +/* Inline author name (compact mode) */ +.author-inline { + color: var(--color-text); + text-decoration: none; +} + +.author-inline:hover { + color: var(--color-secondary); +} + +/* Separator between authors */ +.author-separator { + color: var(--color-muted); +} + +/* Et al. text */ +.author-et-al { + color: var(--color-muted); + font-style: italic; + cursor: pointer; +} + +.author-et-al:hover { + color: var(--color-secondary); +} + +/* Dropdown overlay */ +.author-list-dropdown-overlay { + position: fixed; + inset: 0; + z-index: 100; + background: rgba(0, 0, 0, 0.1); + display: flex; + align-items: flex-start; + justify-content: center; + padding-top: 10vh; +} + +.author-list-dropdown-content { + background: var(--color-surface); + border: 1px solid var(--color-border); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + border-radius: 0.5rem; + padding: 1rem; + max-height: 60vh; + overflow-y: auto; + min-width: 200px; + max-width: 400px; +} + +.author-list-dropdown { + display: flex; + flex-direction: column; + gap: 1rem; +} + +@media (prefers-color-scheme: dark) { + .author-list-dropdown-overlay { + background: rgba(0, 0, 0, 0.4); + } + .author-list-dropdown-content { + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); + } +} diff --git a/crates/weaver-app/src/components/author_list/component.rs b/crates/weaver-app/src/components/author_list/component.rs new file mode 100644 index 0000000..b5cbe2b --- /dev/null +++ b/crates/weaver-app/src/components/author_list/component.rs @@ -0,0 +1,279 @@ +//! AuthorList component for displaying multiple authors with progressive disclosure. + +use crate::Route; +use dioxus::prelude::*; +use jacquard::IntoStatic; +use jacquard::types::ident::AtIdentifier; +use jacquard::types::string::{Did, Handle, Uri}; +use weaver_api::sh_weaver::actor::ProfileDataViewInner; +use weaver_api::sh_weaver::notebook::AuthorListView; + +const AUTHOR_CSS: Asset = asset!("./author.css"); + +/// Normalized author data extracted from ProfileDataViewInner variants. +#[derive(Clone, PartialEq)] +pub struct AuthorInfo { + pub did: Did<'static>, + pub handle: Handle<'static>, + pub display_name: Option, + pub avatar_url: Option>, +} + +impl AuthorInfo { + /// Check if this author matches an AtIdentifier (comparing DID or handle as appropriate). + pub fn matches_ident(&self, ident: &AtIdentifier<'_>) -> bool { + match ident { + AtIdentifier::Did(did) => self.did == *did, + AtIdentifier::Handle(handle) => self.handle == *handle, + } + } +} + +/// Extract normalized author info from ProfileDataViewInner. +/// Returns None for unknown/unhandled variants. +pub fn extract_author_info(inner: &ProfileDataViewInner<'_>) -> Option { + match inner { + ProfileDataViewInner::ProfileView(p) => Some(AuthorInfo { + did: p.did.clone().into_static(), + handle: p.handle.clone().into_static(), + display_name: p.display_name.as_ref().map(|n| n.to_string()), + avatar_url: p.avatar.clone().map(|u| u.into_static()), + }), + ProfileDataViewInner::ProfileViewDetailed(p) => Some(AuthorInfo { + did: p.did.clone().into_static(), + handle: p.handle.clone().into_static(), + display_name: p.display_name.as_ref().map(|n| n.to_string()), + avatar_url: p.avatar.clone().map(|u| u.into_static()), + }), + ProfileDataViewInner::TangledProfileView(p) => Some(AuthorInfo { + did: p.did.clone().into_static(), + handle: p.handle.clone().into_static(), + display_name: None, + avatar_url: None, + }), + _ => None, + } +} + +#[derive(Clone, Copy, PartialEq)] +enum DisplayMode { + Hidden, + Full, + Compact, + Collapsed, +} + +fn determine_display_mode( + author_infos: &[AuthorInfo], + profile_ident: &Option>, +) -> DisplayMode { + let count = author_infos.len(); + + // Context-aware: single author matching profile ident = hidden + if count == 1 { + if let Some(pident) = profile_ident { + if author_infos[0].matches_ident(pident) { + return DisplayMode::Hidden; + } + } + } + + match count { + 0 => DisplayMode::Hidden, + 1 | 2 => DisplayMode::Full, + 3 | 4 => DisplayMode::Compact, + _ => DisplayMode::Collapsed, + } +} + +#[derive(Props, Clone, PartialEq)] +pub struct AuthorListProps { + /// The authors to display. + pub authors: Vec>, + + /// Optional profile identity for context-aware visibility. + /// If set and there's only 1 author matching this identity, render nothing. + #[props(default)] + pub profile_ident: Option>, + + /// Optional resource owner identity - this author will be sorted first. + #[props(default)] + pub owner_ident: Option>, + + /// Avatar size in the full block display (default: 42). + #[props(default = 42)] + pub avatar_size: u32, + + /// Additional CSS class for the container. + #[props(default)] + pub class: Option, +} + +/// Displays a list of authors with progressive disclosure based on count. +/// +/// - 1-2 authors: Full block (avatar + name + handle) +/// - 3-4 authors: Compact (names only, comma-separated) +/// - 5+ authors: Collapsed ("Name, Name, et al.") +/// +/// Compact/collapsed modes expand on click to show full dropdown. +#[component] +pub fn AuthorList(props: AuthorListProps) -> Element { + let mut expanded = use_signal(|| false); + + let container_class = props.class.as_deref().unwrap_or(""); + + // Pre-extract all author infos, filtering out unknown variants + let mut author_infos: Vec = props + .authors + .iter() + .filter_map(|a| extract_author_info(&a.record.inner)) + .collect(); + + // Sort owner first if specified + if let Some(ref owner) = props.owner_ident { + author_infos.sort_by_key(|info| if info.matches_ident(owner) { 0 } else { 1 }); + } + + let mode = determine_display_mode(&author_infos, &props.profile_ident); + + match mode { + DisplayMode::Hidden => rsx! {}, + + DisplayMode::Full => rsx! { + document::Stylesheet { href: AUTHOR_CSS } + div { class: "author-list author-list-full {container_class}", + for info in author_infos.iter() { + AuthorBlock { info: info.clone(), avatar_size: props.avatar_size } + } + } + }, + + DisplayMode::Compact => rsx! { + document::Stylesheet { href: AUTHOR_CSS } + div { + class: "author-list author-list-compact {container_class}", + onclick: move |_| expanded.set(true), + for (i, info) in author_infos.iter().enumerate() { + if i > 0 { + span { class: "author-separator", ", " } + } + AuthorInline { info: info.clone() } + } + + if expanded() { + AuthorDropdown { + authors: author_infos.clone(), + avatar_size: props.avatar_size, + on_close: move |_| expanded.set(false), + } + } + } + }, + + DisplayMode::Collapsed => { + let first_two: Vec<_> = author_infos.iter().take(2).cloned().collect(); + let remaining = author_infos.len().saturating_sub(2); + + rsx! { + document::Stylesheet { href: AUTHOR_CSS } + div { + class: "author-list author-list-collapsed {container_class}", + onclick: move |_| expanded.set(true), + for (i, info) in first_two.iter().enumerate() { + if i > 0 { + span { class: "author-separator", ", " } + } + AuthorInline { info: info.clone() } + } + span { class: "author-et-al", " et al. ({remaining} more)" } + + if expanded() { + AuthorDropdown { + authors: author_infos.clone(), + avatar_size: props.avatar_size, + on_close: move |_| expanded.set(false), + } + } + } + } + } + } +} + +/// Full author display with avatar, name, and handle (as a link). +#[component] +fn AuthorBlock(info: AuthorInfo, avatar_size: u32) -> Element { + let display = info + .display_name + .as_deref() + .unwrap_or_else(|| info.handle.as_ref()); + let handle_display = info.handle.as_ref(); + + rsx! { + Link { + to: Route::RepositoryIndex { + ident: AtIdentifier::Handle(info.handle.clone()) + }, + class: "embed-author author-block", + if let Some(ref avatar) = info.avatar_url { + img { + class: "embed-avatar", + src: avatar.as_ref(), + alt: "", + width: "{avatar_size}", + height: "{avatar_size}", + } + } + span { class: "embed-author-info", + span { class: "embed-author-name", "{display}" } + span { class: "embed-author-handle", "@{handle_display}" } + } + } + } +} + +/// Inline author name only (as a link), for compact display. +#[component] +fn AuthorInline(info: AuthorInfo) -> Element { + let display = info + .display_name + .as_deref() + .unwrap_or_else(|| info.handle.as_ref()); + + rsx! { + Link { + to: Route::RepositoryIndex { + ident: AtIdentifier::Handle(info.handle.clone()) + }, + class: "author-inline", + "{display}" + } + } +} + +/// Dropdown overlay showing all authors in full block display. +#[component] +fn AuthorDropdown( + authors: Vec, + avatar_size: u32, + on_close: EventHandler<()>, +) -> Element { + rsx! { + div { + class: "author-list-dropdown-overlay", + onclick: move |e| { + e.stop_propagation(); + on_close.call(()); + }, + div { + class: "author-list-dropdown-content", + onclick: move |e| e.stop_propagation(), + div { class: "author-list-dropdown", + for info in authors.iter() { + AuthorBlock { info: info.clone(), avatar_size } + } + } + } + } + } +} diff --git a/crates/weaver-app/src/components/author_list/mod.rs b/crates/weaver-app/src/components/author_list/mod.rs new file mode 100644 index 0000000..dde8e15 --- /dev/null +++ b/crates/weaver-app/src/components/author_list/mod.rs @@ -0,0 +1,5 @@ +//! Author list component for displaying multiple authors with progressive disclosure. + +mod component; + +pub use component::{AuthorInfo, AuthorList, AuthorListProps, extract_author_info}; diff --git a/crates/weaver-app/src/components/entry.rs b/crates/weaver-app/src/components/entry.rs index 95e08d3..4dee5c2 100644 --- a/crates/weaver-app/src/components/entry.rs +++ b/crates/weaver-app/src/components/entry.rs @@ -3,6 +3,7 @@ use crate::Route; #[cfg(feature = "server")] use crate::blobcache::BlobCache; +use crate::components::AuthorList; use crate::{components::EntryActions, data::use_handle}; use dioxus::prelude::*; use jacquard::types::aturi::AtUri; @@ -416,13 +417,8 @@ pub fn EntryCard( let entry_uri = entry_view.uri.clone().into_static(); - // Only show author if notebook has multiple authors + // Show author list if notebook has multiple authors let show_author = author_count > 1; - let first_author = if show_author { - entry_view.authors.first() - } else { - None - }; // Render preview from truncated entry content let preview_html = parsed_entry.as_ref().map(|entry| { @@ -460,60 +456,11 @@ pub fn EntryCard( } } } - if let Some(author) = first_author { - { - use weaver_api::sh_weaver::actor::ProfileDataViewInner; - - match &author.record.inner { - ProfileDataViewInner::ProfileView(profile) => { - let display_name = profile.display_name.as_ref().map(|n| n.as_ref()).unwrap_or("Unknown"); - let handle = profile.handle.clone(); - rsx! { - span { class: "embed-author entry-card-author", - if let Some(ref avatar_url) = profile.avatar { - img { class: "embed-avatar", src: avatar_url.as_ref(), alt: "", width: "42", height: "42" } - } - span { class: "embed-author-info", - span { class: "embed-author-name", "{display_name}" } - span { class: "embed-author-handle", "@{handle}" } - } - } - } - } - ProfileDataViewInner::ProfileViewDetailed(profile) => { - let display_name = profile.display_name.as_ref().map(|n| n.as_ref()).unwrap_or("Unknown"); - let handle = profile.handle.clone(); - rsx! { - span { class: "embed-author entry-card-author", - if let Some(ref avatar_url) = profile.avatar { - img { class: "embed-avatar", src: avatar_url.as_ref(), alt: "", width: "42", height: "42" } - } - span { class: "embed-author-info", - span { class: "embed-author-name", "{display_name}" } - span { class: "embed-author-handle", "@{handle}" } - } - } - } - } - ProfileDataViewInner::TangledProfileView(profile) => { - rsx! { - span { class: "embed-author entry-card-author", - span { class: "embed-author-info", - span { class: "embed-author-handle", "@{profile.handle.as_ref()}" } - } - } - } - } - _ => { - rsx! { - span { class: "embed-author entry-card-author", - span { class: "embed-author-info", - span { class: "embed-author-name", "Unknown" } - } - } - } - } - } + if show_author && !entry_view.authors.is_empty() { + AuthorList { + authors: entry_view.authors.clone(), + owner_ident: Some(ident.clone()), + class: Some("entry-card-author".to_string()), } } } @@ -543,12 +490,12 @@ pub fn FeedEntryCard( #[props(default = false)] show_actions: bool, #[props(default = false)] is_pinned: bool, #[props(default = true)] show_author: bool, + /// Profile identity for context-aware author visibility (hides single author on their own profile) + #[props(default)] profile_ident: Option>, #[props(default)] on_pinned_changed: Option>, ) -> Element { use crate::Route; use crate::auth::AuthState; - use jacquard::from_data; - use weaver_api::sh_weaver::actor::ProfileDataViewInner; let title = entry_view .title @@ -573,12 +520,8 @@ pub fn FeedEntryCard( // Format date from record's created_at let formatted_date = entry.created_at.as_ref().format("%B %d, %Y").to_string(); - // Get first author if we're showing it - let first_author = if show_author { - entry_view.authors.first() - } else { - None - }; + // Whether to show authors + let has_authors = show_author && !entry_view.authors.is_empty(); // Check edit access via permissions let auth_state = use_context::>(); @@ -621,7 +564,7 @@ pub fn FeedEntryCard( h3 { class: "entry-card-title", "{title}" } } // Date inline with title when no author shown - if first_author.is_none() { + if !has_authors { div { class: "entry-card-date", time { datetime: "{entry.created_at.as_str()}", "{formatted_date}" } } @@ -639,67 +582,14 @@ pub fn FeedEntryCard( } } - // Byline: author + date (only when author shown) - if let Some(author) = first_author { + // Byline: author + date (only when authors shown) + if has_authors { div { class: "entry-card-byline", - { - match &author.record.inner { - ProfileDataViewInner::ProfileView(profile) => { - let display_name = profile.display_name.as_ref().map(|n| n.as_ref()).unwrap_or("Unknown"); - let handle = profile.handle.clone(); - rsx! { - Link { - to: Route::RepositoryIndex { ident: AtIdentifier::Handle(handle.clone()) }, - class: "embed-author entry-card-author", - if let Some(ref avatar_url) = profile.avatar { - img { class: "embed-avatar", src: avatar_url.as_ref(), alt: "", width: "42", height: "42" } - } - span { class: "embed-author-info", - span { class: "embed-author-name", "{display_name}" } - span { class: "embed-author-handle", "@{handle}" } - } - } - } - } - ProfileDataViewInner::ProfileViewDetailed(profile) => { - let display_name = profile.display_name.as_ref().map(|n| n.as_ref()).unwrap_or("Unknown"); - let handle = profile.handle.clone(); - rsx! { - Link { - to: Route::RepositoryIndex { ident: AtIdentifier::Handle(handle.clone()) }, - class: "embed-author entry-card-author", - if let Some(ref avatar_url) = profile.avatar { - img { class: "embed-avatar", src: avatar_url.as_ref(), alt: "", width: "42", height: "42" } - } - span { class: "embed-author-info", - span { class: "embed-author-name", "{display_name}" } - span { class: "embed-author-handle", "@{handle}" } - } - } - } - } - ProfileDataViewInner::TangledProfileView(profile) => { - let handle = profile.handle.clone(); - rsx! { - Link { - to: Route::RepositoryIndex { ident: AtIdentifier::Handle(handle.clone()) }, - class: "embed-author entry-card-author", - span { class: "embed-author-info", - span { class: "embed-author-handle", "@{handle}" } - } - } - } - } - _ => { - rsx! { - span { class: "embed-author entry-card-author", - span { class: "embed-author-info", - span { class: "embed-author-name", "Unknown" } - } - } - } - } - } + AuthorList { + authors: entry_view.authors.clone(), + profile_ident: profile_ident.clone(), + owner_ident: Some(ident.clone()), + class: Some("entry-card-author".to_string()), } div { class: "entry-card-date", time { datetime: "{entry.created_at.as_str()}", "{formatted_date}" } @@ -771,67 +661,9 @@ pub fn EntryMetadata( // Authors if !entry_view.authors.is_empty() { div { class: "entry-authors", - for (i, author) in entry_view.authors.iter().enumerate() { - if i > 0 { span { " " } } - { - use weaver_api::sh_weaver::actor::ProfileDataViewInner; - - match &author.record.inner { - ProfileDataViewInner::ProfileView(profile) => { - let display_name = profile.display_name.as_ref().map(|n| n.as_ref()).unwrap_or("Unknown"); - let handle = profile.handle.clone(); - - rsx! { - Link { - to: Route::RepositoryIndex { ident: AtIdentifier::Handle(handle.clone()) }, - class: "embed-author", - if let Some(ref avatar_url) = profile.avatar { - img { class: "embed-avatar", src: avatar_url.as_ref(), alt: "", width: "42", height: "42" } - } - span { class: "embed-author-info", - span { class: "embed-author-name", "{display_name}" } - span { class: "embed-author-handle", "@{handle}" } - } - } - } - } - ProfileDataViewInner::ProfileViewDetailed(profile) => { - let display_name = profile.display_name.as_ref().map(|n| n.as_ref()).unwrap_or("Unknown"); - let handle = profile.handle.clone(); - rsx! { - Link { - to: Route::RepositoryIndex { ident: AtIdentifier::Handle(handle.clone()) }, - class: "embed-author", - if let Some(ref avatar_url) = profile.avatar { - img { class: "embed-avatar", src: avatar_url.as_ref(), alt: "", width: "42", height: "42" } - } - span { class: "embed-author-info", - span { class: "embed-author-name", "{display_name}" } - span { class: "embed-author-handle", "@{handle}" } - } - } - } - } - ProfileDataViewInner::TangledProfileView(profile) => { - rsx! { - span { class: "embed-author", - span { class: "embed-author-info", - span { class: "embed-author-handle", "@{profile.handle.as_ref()}" } - } - } - } - } - _ => { - rsx! { - span { class: "embed-author", - span { class: "embed-author-info", - span { class: "embed-author-name", "Unknown" } - } - } - } - } - } - } + AuthorList { + authors: entry_view.authors.clone(), + owner_ident: Some(ident.clone()), } } } diff --git a/crates/weaver-app/src/components/identity.rs b/crates/weaver-app/src/components/identity.rs index ca03052..490e8e9 100644 --- a/crates/weaver-app/src/components/identity.rs +++ b/crates/weaver-app/src/components/identity.rs @@ -1,6 +1,6 @@ use crate::auth::AuthState; use crate::components::css::DefaultNotebookCss; -use crate::components::{FeedEntryCard, ProfileActions, ProfileActionsMenubar}; +use crate::components::{AuthorList, FeedEntryCard, ProfileActions, ProfileActionsMenubar}; use crate::{Route, data, fetch}; use dioxus::prelude::*; use jacquard::{smol_str::SmolStr, types::ident::AtIdentifier}; @@ -358,7 +358,8 @@ pub fn RepositoryIndex(ident: ReadSignal>) -> Element { NotebookCard { notebook: notebook.clone(), entry_refs: entries.clone(), - is_pinned: true + is_pinned: true, + profile_ident: Some(ident()), } } } @@ -373,7 +374,7 @@ pub fn RepositoryIndex(ident: ReadSignal>) -> Element { entry: entry.clone(), show_actions: true, is_pinned: true, - show_author: false + profile_ident: Some(ident()), } } } @@ -405,7 +406,8 @@ pub fn RepositoryIndex(ident: ReadSignal>) -> Element { NotebookCard { notebook: notebook.clone(), entry_refs: entries.clone(), - is_pinned: false + is_pinned: false, + profile_ident: Some(ident()), } } } @@ -420,7 +422,7 @@ pub fn RepositoryIndex(ident: ReadSignal>) -> Element { entry: entry.clone(), show_actions: true, is_pinned: false, - show_author: false + profile_ident: Some(ident()), } } } @@ -446,6 +448,8 @@ pub fn NotebookCard( entry_refs: Vec>, #[props(default = false)] is_pinned: bool, #[props(default)] show_author: Option, + /// Profile identity for context-aware author visibility (hides single author on their own profile) + #[props(default)] profile_ident: Option>, #[props(default)] on_pinned_changed: Option>, #[props(default)] on_deleted: Option>, ) -> Element { @@ -542,41 +546,13 @@ pub fn NotebookCard( } } - // Show authors only if multiple + // Show authors if show_authors { div { class: "notebook-card-authors", - for (i, author) in notebook.authors.iter().enumerate() { - if i > 0 { span { class: "author-separator", ", " } } - { - use weaver_api::sh_weaver::actor::ProfileDataViewInner; - - match &author.record.inner { - ProfileDataViewInner::ProfileView(profile) => { - let display_name = profile.display_name.as_ref() - .map(|n| n.as_ref()) - .unwrap_or("Unknown"); - rsx! { - span { class: "author-name", "{display_name}" } - } - } - ProfileDataViewInner::ProfileViewDetailed(profile) => { - let display_name = profile.display_name.as_ref() - .map(|n| n.as_ref()) - .unwrap_or("Unknown"); - rsx! { - span { class: "author-name", "{display_name}" } - } - } - ProfileDataViewInner::TangledProfileView(profile) => { - rsx! { - span { class: "author-name", "@{profile.handle.as_ref()}" } - } - } - _ => rsx! { - span { class: "author-name", "Unknown" } - } - } - } + AuthorList { + authors: notebook.authors.clone(), + profile_ident: profile_ident.clone(), + owner_ident: Some(ident.clone()), } } } diff --git a/crates/weaver-app/src/components/mod.rs b/crates/weaver-app/src/components/mod.rs index e67613d..de2cda5 100644 --- a/crates/weaver-app/src/components/mod.rs +++ b/crates/weaver-app/src/components/mod.rs @@ -31,6 +31,9 @@ pub mod record_view; pub mod collab; pub use collab::{CollaboratorAvatars, CollaboratorsPanel, InviteDialog, InvitesList}; +pub mod author_list; +pub use author_list::AuthorList; + use dioxus::prelude::*; #[derive(PartialEq, Props, Clone)] diff --git a/crates/weaver-app/src/components/notebook_cover.rs b/crates/weaver-app/src/components/notebook_cover.rs index 9622a04..1280909 100644 --- a/crates/weaver-app/src/components/notebook_cover.rs +++ b/crates/weaver-app/src/components/notebook_cover.rs @@ -1,8 +1,10 @@ #![allow(non_snake_case)] use crate::Route; +use crate::components::AuthorList; use crate::components::button::{Button, ButtonVariant}; use dioxus::prelude::*; +use jacquard::IntoStatic; use jacquard::smol_str::SmolStr; use jacquard::types::ident::AtIdentifier; use weaver_api::sh_weaver::notebook::NotebookView; @@ -41,8 +43,17 @@ pub fn NotebookCover( // Authors section if !notebook.authors.is_empty() { - div { class: "notebook-cover-authors", - NotebookAuthors { authors: notebook.authors.clone() } + { + let owner = notebook.uri.authority().clone().into_static(); + rsx! { + div { class: "notebook-cover-authors", + AuthorList { + authors: notebook.authors.clone(), + owner_ident: Some(owner), + avatar_size: 48, + } + } + } } } @@ -99,80 +110,3 @@ pub fn NotebookCover( } } } - -#[component] -fn NotebookAuthors( - authors: Vec>, -) -> Element { - rsx! { - div { class: "notebook-authors-list", - for (i, author) in authors.iter().enumerate() { - if i > 0 { span { class: "author-separator", ", " } } - NotebookAuthor { author: author.clone() } - } - } - } -} - -#[component] -fn NotebookAuthor(author: weaver_api::sh_weaver::notebook::AuthorListView<'static>) -> Element { - use weaver_api::sh_weaver::actor::ProfileDataViewInner; - - // Author already has profile data hydrated - match &author.record.inner { - ProfileDataViewInner::ProfileView(p) => { - let display_name = p - .display_name - .as_ref() - .map(|n| n.as_ref()) - .unwrap_or("Unknown"); - - rsx! { - span { class: "embed-author notebook-author", - if let Some(ref avatar) = p.avatar { - img { class: "embed-avatar", src: avatar.as_ref(), alt: "", width: "48", height: "48" } - } - span { class: "embed-author-info", - span { class: "embed-author-name", "{display_name}" } - span { class: "embed-author-handle", "@{p.handle}" } - } - } - } - } - ProfileDataViewInner::ProfileViewDetailed(p) => { - let display_name = p - .display_name - .as_ref() - .map(|n| n.as_ref()) - .unwrap_or("Unknown"); - - rsx! { - span { class: "embed-author notebook-author", - if let Some(ref avatar) = p.avatar { - img { class: "embed-avatar", src: avatar.as_ref(), alt: "", width: "48", height: "48" } - } - span { class: "embed-author-info", - span { class: "embed-author-name", "{display_name}" } - span { class: "embed-author-handle", "@{p.handle}" } - } - } - } - } - ProfileDataViewInner::TangledProfileView(p) => { - rsx! { - span { class: "embed-author notebook-author", - span { class: "embed-author-info", - span { class: "embed-author-handle", "@{p.handle.as_ref()}" } - } - } - } - } - _ => rsx! { - span { class: "embed-author notebook-author", - span { class: "embed-author-info", - span { class: "embed-author-name", "Unknown" } - } - } - }, - } -} diff --git a/crates/weaver-app/src/fetch.rs b/crates/weaver-app/src/fetch.rs index 34fd39d..4ec0d67 100644 --- a/crates/weaver-app/src/fetch.rs +++ b/crates/weaver-app/src/fetch.rs @@ -727,7 +727,10 @@ impl Fetcher { } notebooks.push(result); } - Err(_) => continue, // Skip notebooks that fail to load + Err(e) => { + tracing::warn!("fetch_notebooks_for_did: view_notebook failed for {}: {}", record.uri, e); + continue; + } } } } diff --git a/crates/weaver-app/src/main.rs b/crates/weaver-app/src/main.rs index cbf78b1..79c24c0 100644 --- a/crates/weaver-app/src/main.rs +++ b/crates/weaver-app/src/main.rs @@ -148,8 +148,9 @@ fn main() { ); // Filter out noisy crates - let filter = - EnvFilter::new("debug,loro_internal=warn,jacquard_identity=info,jacquard_common=info"); + let filter = EnvFilter::new( + "debug,loro_internal=warn,jacquard_identity=info,jacquard_common=info,iroh=info", + ); let reg = Registry::default() .with(filter) diff --git a/crates/weaver-common/src/agent.rs b/crates/weaver-common/src/agent.rs index c39fe02..9d24664 100644 --- a/crates/weaver-common/src/agent.rs +++ b/crates/weaver-common/src/agent.rs @@ -1601,19 +1601,22 @@ pub trait WeaverExt: AgentSessionExt + XrpcExt + Send + Sync + Sized { } }; - // Fetch the record to get createdAt + // Fetch the record to get createdAt (use untyped fetch to handle any collection) let record = self - .get_record::(resource_uri) + .fetch_record_slingshot(resource_uri) .await - .map_err(|e| WeaverError::from(AgentError::from(e)))? - .into_output() - .map_err(|e| { - WeaverError::from(AgentError::from(ClientError::invalid_request(format!( - "Failed to parse record: {}", - e - )))) + .map_err(|e| WeaverError::from(AgentError::from(e)))?; + let authority_granted_at = record + .value + .query("createdAt") + .first() + .and_then(|v| v.as_str()) + .and_then(|s| s.parse::().ok()) + .ok_or_else(|| { + WeaverError::from(AgentError::from(ClientError::invalid_request( + "Record missing createdAt", + ))) })?; - let authority_granted_at = record.value.created_at; editors.push( PermissionGrant::new()