at main 151 lines 5.5 kB view raw
1//! Collaborator avatars display for the editor meta row. 2 3use std::sync::Arc; 4 5use crate::auth::AuthState; 6use crate::fetch::Fetcher; 7use dioxus::prelude::*; 8use jacquard::types::ident::AtIdentifier; 9use jacquard::types::string::AtUri; 10use weaver_api::sh_weaver::actor::{ProfileDataView, ProfileDataViewInner}; 11 12use super::api::find_all_participants; 13use super::CollaboratorsPanel; 14 15/// Props for the CollaboratorAvatars component. 16#[derive(Props, Clone, PartialEq)] 17pub struct CollaboratorAvatarsProps { 18 /// The resource URI to show collaborators for. 19 pub resource_uri: AtUri<'static>, 20 /// CID of the resource. 21 pub resource_cid: String, 22 /// Optional title for display in the panel. 23 #[props(default)] 24 pub resource_title: Option<String>, 25} 26 27/// Shows collaborator avatars with a button to manage collaborators. 28/// Displays all participants (collaborators with accepted invites) regardless of 29/// whether you're the owner or a collaborator. 30#[component] 31pub fn CollaboratorAvatars(props: CollaboratorAvatarsProps) -> Element { 32 let auth_state = use_context::<Signal<AuthState>>(); 33 let fetcher = use_context::<Fetcher>(); 34 let mut show_panel = use_signal(|| false); 35 36 let resource_uri = props.resource_uri.clone(); 37 38 // Fetch all participants (owner + collaborators) with their profiles 39 let collaborators = { 40 let fetcher = fetcher.clone(); 41 let resource_uri = resource_uri.clone(); 42 use_resource(move || { 43 let fetcher = fetcher.clone(); 44 let resource_uri = resource_uri.clone(); 45 let _auth = auth_state.read().did.clone(); // Reactivity trigger 46 async move { 47 let dids = find_all_participants(&fetcher, &resource_uri) 48 .await 49 .unwrap_or_default(); 50 51 // Fetch profile for each participant 52 let mut profiles = Vec::new(); 53 for did in dids { 54 let ident = AtIdentifier::Did(did); 55 if let Ok(profile) = fetcher.fetch_profile(&ident).await { 56 profiles.push(profile); 57 } 58 } 59 profiles 60 } 61 }) 62 }; 63 64 let collabs: Vec<Arc<ProfileDataView<'static>>> = collaborators().unwrap_or_default(); 65 let collab_count = collabs.len(); 66 67 rsx! { 68 div { class: "collaborator-avatars", 69 onclick: move |_| show_panel.set(true), 70 71 // Show up to 3 avatar circles 72 for (i, profile) in collabs.iter().take(3).enumerate() { 73 { 74 let (avatar, display_name, handle) = match &profile.inner { 75 ProfileDataViewInner::ProfileView(p) => ( 76 p.avatar.as_ref(), 77 p.display_name.as_ref().map(|s| s.as_ref()), 78 p.handle.as_ref(), 79 ), 80 ProfileDataViewInner::ProfileViewDetailed(p) => ( 81 p.avatar.as_ref(), 82 p.display_name.as_ref().map(|s| s.as_ref()), 83 p.handle.as_ref(), 84 ), 85 ProfileDataViewInner::TangledProfileView(p) => ( 86 None, 87 None, 88 p.handle.as_ref(), 89 ), 90 _ => (None, None, "unknown"), 91 }; 92 let title = display_name.unwrap_or(handle); 93 let initials = get_initials(display_name, handle); 94 95 rsx! { 96 div { 97 class: "collab-avatar", 98 style: "z-index: {3 - i}", 99 title: "{title}", 100 101 if let Some(avatar_url) = avatar { 102 img { 103 class: "collab-avatar-img", 104 src: avatar_url.as_ref(), 105 alt: "{title}", 106 } 107 } else { 108 "{initials}" 109 } 110 } 111 } 112 } 113 } 114 115 // Show +N if more than 3 116 if collab_count > 3 { 117 div { class: "collab-avatar collab-overflow", 118 "+{collab_count - 3}" 119 } 120 } 121 122 // Always show the add button 123 div { class: "collab-avatar collab-add", 124 title: "Manage collaborators", 125 "+" 126 } 127 } 128 129 if show_panel() { 130 CollaboratorsPanel { 131 resource_uri: props.resource_uri.clone(), 132 resource_cid: props.resource_cid.clone(), 133 resource_title: props.resource_title.clone(), 134 on_close: move |_| show_panel.set(false), 135 } 136 } 137 } 138} 139 140/// Get initials from display name or handle. 141fn get_initials(display_name: Option<&str>, handle: &str) -> String { 142 if let Some(name) = display_name { 143 name.split_whitespace() 144 .take(2) 145 .filter_map(|w| w.chars().next()) 146 .collect::<String>() 147 .to_uppercase() 148 } else { 149 handle.chars().next().unwrap_or('?').to_uppercase().to_string() 150 } 151}