atproto Thingiverse but good
10

Configure Feed

Select the types of activity you want to include in your feed.

bunch of styling fixes

Orual (Jun 29, 2026, 7:06 PM EDT) 4cb45a73 27257c70

+191 -96
+40 -4
assets/styling/primitives.css
··· 122 122 min-height: 10rem; 123 123 } 124 124 125 + .thing-card-media-link { 126 + position: relative; 127 + display: block; 128 + width: 100%; 129 + height: 100%; 130 + color: inherit; 131 + text-decoration: none; 132 + } 133 + 134 + .thing-card-media-link::after { 135 + content: ""; 136 + position: absolute; 137 + inset: 0; 138 + pointer-events: none; 139 + background: transparent; 140 + outline: 0 solid transparent; 141 + outline-offset: -1px; 142 + } 143 + 144 + .thing-card-media-link:hover::after { 145 + background: color-mix(in srgb, var(--color-primary) 8%, transparent); 146 + outline-width: 1px; 147 + outline-color: var(--color-primary); 148 + } 149 + 150 + .thing-card-media-link:focus-visible { 151 + outline: 2px solid color-mix(in srgb, var(--color-primary) 35%, transparent); 152 + outline-offset: -3px; 153 + } 154 + 125 155 .thing-card-media img { 126 156 display: block; 127 157 width: 100%; ··· 165 195 .thing-card-creator { 166 196 color: var(--color-subtle); 167 197 font-size: 0.88rem; 198 + } 199 + 200 + .author-byline-handle { 201 + font-family: var(--font-mono); 202 + } 203 + 204 + .author-byline-handle-secondary { 205 + color: var(--color-muted); 168 206 } 169 207 170 208 .thing-card-chips { ··· 274 312 275 313 /* Creator links on cards and thing detail */ 276 314 277 - .thing-card-creator-link, 278 - .thing-detail-creator-link { 315 + .author-byline-link { 279 316 color: var(--color-secondary); 280 317 } 281 318 282 - .thing-card-creator-link:hover, 283 - .thing-detail-creator-link:hover { 319 + .author-byline-link:hover { 284 320 color: var(--color-link); 285 321 } 286 322
+8 -4
crates/polymodel-api/lexicons/space_polymodel_library_defs.json
··· 351 351 "ref": "#actor" 352 352 }, 353 353 "cid": { 354 - "type": "string" 354 + "type": "string", 355 + "format": "cid" 355 356 }, 356 357 "cover": { 357 358 "type": "array", ··· 411 412 "ref": "#actor" 412 413 }, 413 414 "cid": { 414 - "type": "string" 415 + "type": "string", 416 + "format": "cid" 415 417 }, 416 418 "file": { 417 419 "type": "ref", ··· 469 471 "ref": "#actor" 470 472 }, 471 473 "cid": { 472 - "type": "string" 474 + "type": "string", 475 + "format": "cid" 473 476 }, 474 477 "cover": { 475 478 "type": "array", ··· 562 565 "ref": "#actor" 563 566 }, 564 567 "cid": { 565 - "type": "string" 568 + "type": "string", 569 + "format": "cid" 566 570 }, 567 571 "cover": { 568 572 "type": "array",
+29 -17
crates/polymodel-api/src/space_polymodel/library.rs
··· 32 32 use jacquard_common::deps::codegen::unicode_segmentation::UnicodeSegmentation; 33 33 use jacquard_common::deps::smol_str::SmolStr; 34 34 use jacquard_common::types::blob::BlobRef; 35 - use jacquard_common::types::string::{Did, Handle, AtUri, Datetime, UriValue}; 35 + use jacquard_common::types::string::{Did, Handle, AtUri, Cid, Datetime, UriValue}; 36 36 use jacquard_common::types::value::Data; 37 37 use jacquard_derive::IntoStatic; 38 38 use jacquard_lexicon::lexicon::LexiconDoc; ··· 238 238 #[serde(rename_all = "camelCase", bound(deserialize = "S: Deserialize<'de> + BosStr"))] 239 239 pub struct ModelView<S: BosStr = DefaultStr> { 240 240 pub author: library::Actor<S>, 241 - pub cid: S, 241 + pub cid: Cid<S>, 242 242 #[serde(skip_serializing_if = "Option::is_none")] 243 243 pub cover: Option<Vec<library::ImageView<S>>>, 244 244 pub indexed_at: Datetime, ··· 260 260 #[serde(rename_all = "camelCase", bound(deserialize = "S: Deserialize<'de> + BosStr"))] 261 261 pub struct PartView<S: BosStr = DefaultStr> { 262 262 pub author: library::Actor<S>, 263 - pub cid: S, 263 + pub cid: Cid<S>, 264 264 pub file: library::File<S>, 265 265 #[serde(skip_serializing_if = "Option::is_none")] 266 266 pub format: Option<S>, ··· 280 280 #[serde(rename_all = "camelCase", bound(deserialize = "S: Deserialize<'de> + BosStr"))] 281 281 pub struct ThingView<S: BosStr = DefaultStr> { 282 282 pub author: library::Actor<S>, 283 - pub cid: S, 283 + pub cid: Cid<S>, 284 284 #[serde(skip_serializing_if = "Option::is_none")] 285 285 pub cover: Option<Vec<library::ImageView<S>>>, 286 286 #[serde(skip_serializing_if = "Option::is_none")] ··· 314 314 #[serde(rename_all = "camelCase", bound(deserialize = "S: Deserialize<'de> + BosStr"))] 315 315 pub struct ThingViewBasic<S: BosStr = DefaultStr> { 316 316 pub author: library::Actor<S>, 317 - pub cid: S, 317 + pub cid: Cid<S>, 318 318 #[serde(skip_serializing_if = "Option::is_none")] 319 319 pub cover: Option<Vec<library::ImageView<S>>>, 320 320 pub indexed_at: Datetime, ··· 1665 1665 ); 1666 1666 map.insert( 1667 1667 SmolStr::new_static("cid"), 1668 - LexObjectProperty::String(LexString { ..Default::default() }), 1668 + LexObjectProperty::String(LexString { 1669 + format: Some(LexStringFormat::Cid), 1670 + ..Default::default() 1671 + }), 1669 1672 ); 1670 1673 map.insert( 1671 1674 SmolStr::new_static("cover"), ··· 1757 1760 ); 1758 1761 map.insert( 1759 1762 SmolStr::new_static("cid"), 1760 - LexObjectProperty::String(LexString { ..Default::default() }), 1763 + LexObjectProperty::String(LexString { 1764 + format: Some(LexStringFormat::Cid), 1765 + ..Default::default() 1766 + }), 1761 1767 ); 1762 1768 map.insert( 1763 1769 SmolStr::new_static("file"), ··· 1844 1850 ); 1845 1851 map.insert( 1846 1852 SmolStr::new_static("cid"), 1847 - LexObjectProperty::String(LexString { ..Default::default() }), 1853 + LexObjectProperty::String(LexString { 1854 + format: Some(LexStringFormat::Cid), 1855 + ..Default::default() 1856 + }), 1848 1857 ); 1849 1858 map.insert( 1850 1859 SmolStr::new_static("cover"), ··· 1992 2001 ); 1993 2002 map.insert( 1994 2003 SmolStr::new_static("cid"), 1995 - LexObjectProperty::String(LexString { ..Default::default() }), 2004 + LexObjectProperty::String(LexString { 2005 + format: Some(LexStringFormat::Cid), 2006 + ..Default::default() 2007 + }), 1996 2008 ); 1997 2009 map.insert( 1998 2010 SmolStr::new_static("cover"), ··· 3986 3998 _state: PhantomData<fn() -> St>, 3987 3999 _fields: ( 3988 4000 Option<library::Actor<S>>, 3989 - Option<S>, 4001 + Option<Cid<S>>, 3990 4002 Option<Vec<library::ImageView<S>>>, 3991 4003 Option<Datetime>, 3992 4004 Option<S>, ··· 4063 4075 /// Set the `cid` field (required) 4064 4076 pub fn cid( 4065 4077 mut self, 4066 - value: impl Into<S>, 4078 + value: impl Into<Cid<S>>, 4067 4079 ) -> ModelViewBuilder<model_view_state::SetCid<St>, S> { 4068 4080 self._fields.1 = Option::Some(value.into()); 4069 4081 ModelViewBuilder { ··· 4447 4459 _state: PhantomData<fn() -> St>, 4448 4460 _fields: ( 4449 4461 Option<library::Actor<S>>, 4450 - Option<S>, 4462 + Option<Cid<S>>, 4451 4463 Option<library::File<S>>, 4452 4464 Option<S>, 4453 4465 Option<Datetime>, ··· 4523 4535 /// Set the `cid` field (required) 4524 4536 pub fn cid( 4525 4537 mut self, 4526 - value: impl Into<S>, 4538 + value: impl Into<Cid<S>>, 4527 4539 ) -> PartViewBuilder<part_view_state::SetCid<St>, S> { 4528 4540 self._fields.1 = Option::Some(value.into()); 4529 4541 PartViewBuilder { ··· 5002 5014 _state: PhantomData<fn() -> St>, 5003 5015 _fields: ( 5004 5016 Option<library::Actor<S>>, 5005 - Option<S>, 5017 + Option<Cid<S>>, 5006 5018 Option<Vec<library::ImageView<S>>>, 5007 5019 Option<StrongRef<S>>, 5008 5020 Option<Datetime>, ··· 5124 5136 /// Set the `cid` field (required) 5125 5137 pub fn cid( 5126 5138 mut self, 5127 - value: impl Into<S>, 5139 + value: impl Into<Cid<S>>, 5128 5140 ) -> ThingViewBuilder<thing_view_state::SetCid<St>, S> { 5129 5141 self._fields.1 = Option::Some(value.into()); 5130 5142 ThingViewBuilder { ··· 5704 5716 _state: PhantomData<fn() -> St>, 5705 5717 _fields: ( 5706 5718 Option<library::Actor<S>>, 5707 - Option<S>, 5719 + Option<Cid<S>>, 5708 5720 Option<Vec<library::ImageView<S>>>, 5709 5721 Option<Datetime>, 5710 5722 Option<i64>, ··· 5811 5823 /// Set the `cid` field (required) 5812 5824 pub fn cid( 5813 5825 mut self, 5814 - value: impl Into<S>, 5826 + value: impl Into<Cid<S>>, 5815 5827 ) -> ThingViewBasicBuilder<thing_view_basic_state::SetCid<St>, S> { 5816 5828 self._fields.1 = Option::Some(value.into()); 5817 5829 ThingViewBasicBuilder {
+8 -4
lexicons/library/defs.json
··· 210 210 "format": "at-uri" 211 211 }, 212 212 "cid": { 213 - "type": "string" 213 + "type": "string", 214 + "format": "cid" 214 215 }, 215 216 "author": { 216 217 "type": "ref", ··· 303 304 "format": "at-uri" 304 305 }, 305 306 "cid": { 306 - "type": "string" 307 + "type": "string", 308 + "format": "cid" 307 309 }, 308 310 "name": { 309 311 "type": "string" ··· 365 367 "format": "at-uri" 366 368 }, 367 369 "cid": { 368 - "type": "string" 370 + "type": "string", 371 + "format": "cid" 369 372 }, 370 373 "author": { 371 374 "type": "ref", ··· 416 419 "format": "at-uri" 417 420 }, 418 421 "cid": { 419 - "type": "string" 422 + "type": "string", 423 + "format": "cid" 420 424 }, 421 425 "author": { 422 426 "type": "ref",
+9 -4
src/appview/views.rs
··· 24 24 use jacquard::identity::resolver::IdentityResolver; 25 25 use jacquard::oauth::client::OAuthSession; 26 26 use jacquard_common::deps::smol_str::SmolStr; 27 + use jacquard_common::types::cid::Cid; 27 28 use jacquard_common::types::string::{AtUri, Datetime, Did, Handle, UriValue}; 28 29 use jacquard_common::types::{blob::BlobRef, value::Data}; 29 30 use polymodel_api::space_polymodel::actor::ProfileListView; ··· 49 50 50 51 fn at_uri(uri: &str) -> AppResult<AtUri> { 51 52 AtUri::new_owned(uri).map_err(|e| invalid_request(format!("invalid at-uri {uri:?}: {e}"))) 53 + } 54 + 55 + fn cid_of(cid: &str) -> AppResult<Cid> { 56 + Cid::new_owned(cid.as_bytes()).map_err(|e| internal(format!("invalid cid {cid:?}: {e}"))) 52 57 } 53 58 54 59 fn did_of(did: &str) -> AppResult<Did> { ··· 423 428 let record = thing_record(row.record_json.as_deref())?; 424 429 Ok(ThingView { 425 430 uri: at_uri(&row.uri)?, 426 - cid: s(row.cid), 431 + cid: cid_of(&row.cid)?, 427 432 author, 428 433 record: record_data(row.record_json.as_deref())?, 429 434 indexed_at: datetime_nanos(row.indexed_at), ··· 452 457 let record = thing_record(row.record_json.as_deref())?; 453 458 Ok(ThingViewBasic { 454 459 uri: at_uri(&row.uri)?, 455 - cid: s(row.cid), 460 + cid: cid_of(&row.cid)?, 456 461 name: s(row.name), 457 462 author: actor_for_did(state, &row.did).await?, 458 463 indexed_at: datetime_nanos(row.indexed_at), ··· 478 483 let record = model_record(row.record_json.as_deref())?; 479 484 Ok(ModelView { 480 485 uri: at_uri(&row.uri)?, 481 - cid: s(row.cid), 486 + cid: cid_of(&row.cid)?, 482 487 author, 483 488 record: record_data(row.record_json.as_deref())?, 484 489 indexed_at: datetime_nanos(row.indexed_at), ··· 502 507 let record = part_record(row.record_json.as_deref())?; 503 508 Ok(PartView { 504 509 uri: at_uri(&row.uri)?, 505 - cid: s(row.cid), 510 + cid: cid_of(&row.cid)?, 506 511 author, 507 512 record: record_data(row.record_json.as_deref())?, 508 513 indexed_at: datetime_nanos(row.indexed_at),
+64
src/author_byline.rs
··· 1 + use dioxus::prelude::*; 2 + use polymodel_api::space_polymodel::library; 3 + 4 + #[component] 5 + pub(crate) fn AuthorByline(actor: library::Actor, class: String) -> Element { 6 + let href = profile_href(&actor); 7 + let display_name = clean_text(actor.display_name.as_ref().map(AsRef::as_ref)); 8 + let handle = actor.handle.as_ref(); 9 + 10 + rsx! { 11 + p { class, 12 + a { class: "author-byline-link", href, 13 + if let Some(display_name) = display_name { 14 + span { class: "author-byline-name", "{display_name}" } 15 + span { class: "author-byline-handle author-byline-handle-secondary", " @{handle}" } 16 + } else { 17 + span { class: "author-byline-handle", "@{handle}" } 18 + } 19 + } 20 + } 21 + } 22 + } 23 + 24 + pub(crate) fn profile_href(actor: &library::Actor) -> String { 25 + let handle = actor.handle.as_ref(); 26 + if !handle.is_empty() { 27 + format!("/profile/{handle}") 28 + } else { 29 + format!("/profile/{}", actor.did.as_ref()) 30 + } 31 + } 32 + 33 + fn clean_text(value: Option<&str>) -> Option<&str> { 34 + value.map(str::trim).filter(|value| !value.is_empty()) 35 + } 36 + 37 + #[cfg(test)] 38 + mod tests { 39 + use jacquard_common::types::string::{Did, Handle}; 40 + 41 + use super::*; 42 + 43 + fn fixture_actor(did: &str, handle: &str) -> library::Actor { 44 + library::Actor { 45 + avatar: None, 46 + description: None, 47 + did: Did::new_owned(did).expect("fixture DID is valid"), 48 + display_name: Some("Maker Name".into()), 49 + follower_count: None, 50 + following_count: None, 51 + handle: Handle::new_owned(handle).expect("fixture handle is valid"), 52 + pronouns: None, 53 + thing_count: None, 54 + viewer: None, 55 + extra_data: None, 56 + } 57 + } 58 + 59 + #[test] 60 + fn profile_href_prefers_handle() { 61 + let actor = fixture_actor("did:plc:alice", "alice.test"); 62 + assert_eq!(profile_href(&actor), "/profile/alice.test"); 63 + } 64 + }
+1 -3
src/browse.rs
··· 4 4 5 5 use crate::Route; 6 6 use crate::examples::{HERO_SAMPLE_HANDLES, first_hero_sample_handle}; 7 - use crate::profile::profile_href; 8 7 use crate::session::SessionIdentity; 9 8 use crate::thing_card::ThingCard; 10 9 use crate::thing_detail::thing_detail_href; ··· 211 210 #[component] 212 211 fn BrowseThingCard(thing: library::ThingViewBasic) -> Element { 213 212 let detail_href = thing_detail_href(&thing.uri); 214 - let creator_href = profile_href(&thing.author); 215 - rsx! { ThingCard { thing, detail_href, creator_href } } 213 + rsx! { ThingCard { thing, detail_href } } 216 214 } 217 215 218 216 #[component]
+1
src/main.rs
··· 5 5 use polymodel_api::space_polymodel::actor::get_session::{GetSession, GetSessionOutput}; 6 6 7 7 mod about; 8 + mod author_byline; 8 9 mod auth; 9 10 mod browse; 10 11 mod client;
+1 -19
src/profile.rs
··· 175 175 } 176 176 } 177 177 178 - /// Canonical profile route for an actor. Prefers the handle (always present 179 - /// and validated on `library::Actor`); falls back to the DID defensively. 180 - pub(crate) fn profile_href(actor: &library::Actor) -> Option<String> { 181 - let handle = actor.handle.as_ref(); 182 - if !handle.is_empty() { 183 - Some(format!("/profile/{handle}")) 184 - } else { 185 - Some(format!("/profile/{}", actor.did.as_ref())) 186 - } 187 - } 188 - 189 178 fn avatar_initial(header: &ProfileHeader) -> char { 190 179 let name = header 191 180 .display_name ··· 687 676 #[component] 688 677 fn ProfileThingCard(thing: library::ThingViewBasic) -> Element { 689 678 let detail_href = thing_detail_href(&thing.uri); 690 - let creator_href = profile_href(&thing.author); 691 - rsx! { ThingCard { thing, detail_href, creator_href } } 679 + rsx! { ThingCard { thing, detail_href } } 692 680 } 693 681 694 682 fn list_item_count_label(count: i64) -> String { ··· 932 920 ProfileFeedState::Populated(items) => assert_eq!(items.len(), fixtures.len()), 933 921 other => panic!("expected populated, got {other:?}"), 934 922 } 935 - } 936 - 937 - #[test] 938 - fn profile_href_prefers_handle_then_did() { 939 - let actor = fixture_actor("did:plc:alice", "alice.test"); 940 - assert_eq!(profile_href(&actor).as_deref(), Some("/profile/alice.test")); 941 923 } 942 924 943 925 #[test]
+1 -3
src/search.rs
··· 2 2 use polymodel_api::space_polymodel::library; 3 3 4 4 use crate::Route; 5 - use crate::profile::profile_href; 6 5 use crate::thing_card::ThingCard; 7 6 use crate::thing_detail::thing_detail_href; 8 7 ··· 221 220 #[component] 222 221 fn SearchThingCard(thing: library::ThingViewBasic) -> Element { 223 222 let detail_href = thing_detail_href(&thing.uri); 224 - let creator_href = profile_href(&thing.author); 225 - rsx! { ThingCard { thing, detail_href, creator_href } } 223 + rsx! { ThingCard { thing, detail_href } } 226 224 } 227 225 228 226 fn search_pagination(
+27 -24
src/thing_card.rs
··· 1 1 use dioxus::prelude::*; 2 - use jacquard_common::types::string::Cid; 3 2 #[cfg(test)] 4 3 use jacquard_common::types::{ 5 4 string::{AtUri, Datetime, Did, Handle}, ··· 13 12 }; 14 13 use polymodel_api::space_polymodel::library; 15 14 15 + use crate::author_byline::AuthorByline; 16 16 use crate::client::PolymodelClient; 17 17 use crate::session::SessionIdentity; 18 18 ··· 129 129 pub fn ThingCard( 130 130 thing: library::ThingViewBasic, 131 131 detail_href: Option<String>, 132 - #[props(default)] creator_href: Option<String>, 133 132 ) -> Element { 134 133 let client = use_context::<PolymodelClient>(); 135 134 let session = use_context::<Signal<SessionIdentity>>(); 136 135 let title = thing_title(&thing); 137 - let creator = creator_label(&thing.author); 138 136 let media = select_card_media(thing.cover.as_deref(), thing.previews.as_deref(), title); 139 137 let stats = stats(&thing); 140 138 let mut like_uri = use_signal(|| thing.viewer.like.clone()); ··· 156 154 let can_write = matches!(&*session.read(), SessionIdentity::Authenticated(_)); 157 155 let subject = StrongRef { 158 156 uri: thing.uri.clone(), 159 - cid: Cid::new_owned(thing.cid.as_str().as_bytes()).expect("thing cid is server-validated"), 157 + cid: thing.cid.clone(), 160 158 extra_data: None, 161 159 }; 162 160 let like_client = client.clone(); ··· 167 165 rsx! { 168 166 article { class: "thing-card card-surface", aria_label: "Thing card for {title}", 169 167 div { class: "thing-card-media card-media", 170 - if let Some(media) = &media { 171 - img { src: "{media.url}", alt: "{media.alt}" } 168 + if let Some(href) = &detail_href { 169 + a { class: "thing-card-media-link", href: "{href}", aria_label: "Open {title}", 170 + if let Some(media) = &media { 171 + img { src: "{media.url}", alt: "{media.alt}" } 172 + } else { 173 + div { class: "thing-card-placeholder blueprint-media", role: "img", aria_label: "Blueprint placeholder for missing preview media", 174 + span { class: "blueprint-axis blueprint-axis-x", "X" } 175 + span { class: "blueprint-axis blueprint-axis-y", "Y" } 176 + span { class: "blueprint-dimension", "preview pending" } 177 + } 178 + } 179 + } 172 180 } else { 173 - div { class: "thing-card-placeholder blueprint-media", role: "img", aria_label: "Blueprint placeholder for missing preview media", 174 - span { class: "blueprint-axis blueprint-axis-x", "X" } 175 - span { class: "blueprint-axis blueprint-axis-y", "Y" } 176 - span { class: "blueprint-dimension", "preview pending" } 181 + if let Some(media) = &media { 182 + img { src: "{media.url}", alt: "{media.alt}" } 183 + } else { 184 + div { class: "thing-card-placeholder blueprint-media", role: "img", aria_label: "Blueprint placeholder for missing preview media", 185 + span { class: "blueprint-axis blueprint-axis-x", "X" } 186 + span { class: "blueprint-axis blueprint-axis-y", "Y" } 187 + span { class: "blueprint-dimension", "preview pending" } 188 + } 177 189 } 178 190 } 179 191 } ··· 186 198 "{title}" 187 199 } 188 200 } 189 - p { class: "thing-card-creator", 190 - if let Some(href) = &creator_href { 191 - a { class: "thing-card-creator-link", href: "{href}", "by {creator}" } 192 - } else { 193 - "by {creator}" 194 - } 195 - } 201 + AuthorByline { actor: thing.author.clone(), class: "thing-card-creator".to_string() } 196 202 } 197 203 div { class: "thing-card-details", 198 204 if let Some(tags) = hydrated_tags(&thing) { ··· 308 314 309 315 fn clean_text(value: Option<&str>) -> Option<&str> { 310 316 value.map(str::trim).filter(|value| !value.is_empty()) 311 - } 312 - 313 - fn creator_label(actor: &library::Actor) -> &str { 314 - clean_text(actor.display_name.as_ref().map(AsRef::as_ref)) 315 - .unwrap_or_else(|| actor.handle.as_ref()) 316 317 } 317 318 318 319 fn hydrated_tags(thing: &library::ThingViewBasic) -> Option<&[jacquard_common::bos::DefaultStr]> { ··· 479 480 thing_title(&view), 480 481 ) 481 482 .unwrap(); 482 - assert_eq!(creator_label(&view.author), "Maker Name"); 483 + assert_eq!(view.author.display_name.as_ref().map(AsRef::as_ref), Some("Maker Name")); 484 + assert_eq!(view.author.handle.as_ref(), "maker.tools"); 483 485 assert_eq!(media.url, COMPLETE_PREVIEW_SVG); 484 486 assert_eq!(media.alt, "Cover alt"); 485 487 } ··· 494 496 }); 495 497 496 498 assert_eq!(thing_title(&view), "Untitled thing"); 497 - assert_eq!(creator_label(&view.author), "maker.tools"); 499 + assert_eq!(view.author.display_name, None); 500 + assert_eq!(view.author.handle.as_ref(), "maker.tools"); 498 501 assert!( 499 502 select_card_media( 500 503 view.cover.as_deref(),
+2 -14
src/thing_detail.rs
··· 14 14 use polymodel_renderer_protocol::MeshFormat; 15 15 16 16 use crate::Route; 17 + use crate::author_byline::AuthorByline; 17 18 use crate::client::PolymodelClient; 18 - use crate::profile::profile_href; 19 19 use crate::session::SessionIdentity; 20 20 use crate::viewer::{AssetViewer, ViewerFallbackImage, ViewerMeshSource, ViewerSourceFormat}; 21 21 ··· 334 334 } else { 335 335 "thing-detail-title" 336 336 }; 337 - let creator = actor_label(&thing.author).to_string(); 338 337 let summary = clean_text(thing.summary.as_ref().map(AsRef::as_ref)) 339 338 .or_else(|| clean_text(thing_record.summary.as_ref().map(AsRef::as_ref))) 340 339 .map(ToString::to_string); ··· 393 392 header { class: "thing-detail-header", 394 393 h1 { class: title_class, "{title}" } 395 394 div { class: "thing-detail-title-row", 396 - p { class: "thing-detail-creator", 397 - if let Some(href) = profile_href(&thing.author) { 398 - a { class: "thing-detail-creator-link", href: "{href}", "by {creator}" } 399 - } else { 400 - "by {creator}" 401 - } 402 - } 395 + AuthorByline { actor: thing.author.clone(), class: "thing-detail-creator".to_string() } 403 396 dl { class: "thing-detail-stats", aria_label: "Thing stats", 404 397 StatItem { label: "Models".to_string(), value: model_count } 405 398 StatItem { label: "Parts".to_string(), value: part_count } ··· 806 799 || url.starts_with("https://") 807 800 || url.starts_with('/') 808 801 || url.starts_with("data:image/") 809 - } 810 - 811 - fn actor_label(actor: &library::Actor) -> &str { 812 - clean_text(actor.display_name.as_ref().map(AsRef::as_ref)) 813 - .unwrap_or_else(|| actor.handle.as_ref()) 814 802 } 815 803 816 804 fn clean_text(value: Option<&str>) -> Option<&str> {