reworking styling

Orual 0fb1e75d 999e5691

+764 -158
+15 -8
crates/weaver-api/lexicons/sh_weaver_actor_defs.json
··· 13 "type": "string", 14 "format": "did" 15 }, 16 - "displayName": { 17 - "type": "string", 18 - "maxLength": 640, 19 - "maxGraphemes": 64 20 - }, 21 - "handle": { 22 - "type": "string", 23 - "format": "handle" 24 } 25 } 26 }, ··· 78 "type": "string", 79 "format": "uri" 80 }, 81 "createdAt": { 82 "type": "string", 83 "format": "datetime" ··· 137 "description": "Pronouns to use in user-generated content.", 138 "ref": "#pronounsList" 139 }, 140 "subscribedCount": { 141 "type": "integer" 142 }, 143 "subscriberCount": { 144 "type": "integer" 145 } 146 } 147 },
··· 13 "type": "string", 14 "format": "did" 15 }, 16 + "signature": { 17 + "type": "bytes", 18 + "description": "signed bytes of the corresponding notebook record in the author's repo" 19 } 20 } 21 }, ··· 73 "type": "string", 74 "format": "uri" 75 }, 76 + "bluesky": { 77 + "type": "boolean", 78 + "description": "Include link to this account on Bluesky." 79 + }, 80 "createdAt": { 81 "type": "string", 82 "format": "datetime" ··· 136 "description": "Pronouns to use in user-generated content.", 137 "ref": "#pronounsList" 138 }, 139 + "streamplace": { 140 + "type": "boolean", 141 + "description": "Include link to this account on stream.place." 142 + }, 143 "subscribedCount": { 144 "type": "integer" 145 }, 146 "subscriberCount": { 147 "type": "integer" 148 + }, 149 + "tangled": { 150 + "type": "boolean", 151 + "description": "Include link to this account on Tangled." 152 } 153 } 154 },
+149 -148
crates/weaver-api/src/sh_weaver/actor.rs
··· 23 pub struct Author<'a> { 24 #[serde(borrow)] 25 pub did: jacquard_common::types::string::Did<'a>, 26 #[serde(skip_serializing_if = "std::option::Option::is_none")] 27 - #[serde(borrow)] 28 - pub display_name: Option<jacquard_common::CowStr<'a>>, 29 - #[serde(skip_serializing_if = "std::option::Option::is_none")] 30 - #[serde(borrow)] 31 - pub handle: Option<jacquard_common::types::string::Handle<'a>>, 32 } 33 34 pub mod author_state { ··· 68 _phantom_state: ::core::marker::PhantomData<fn() -> S>, 69 __unsafe_private_named: ( 70 ::core::option::Option<jacquard_common::types::string::Did<'a>>, 71 - ::core::option::Option<jacquard_common::CowStr<'a>>, 72 - ::core::option::Option<jacquard_common::types::string::Handle<'a>>, 73 ), 74 _phantom: ::core::marker::PhantomData<&'a ()>, 75 } ··· 86 pub fn new() -> Self { 87 AuthorBuilder { 88 _phantom_state: ::core::marker::PhantomData, 89 - __unsafe_private_named: (None, None, None), 90 _phantom: ::core::marker::PhantomData, 91 } 92 } ··· 112 } 113 114 impl<'a, S: author_state::State> AuthorBuilder<'a, S> { 115 - /// Set the `displayName` field (optional) 116 - pub fn display_name( 117 - mut self, 118 - value: impl Into<Option<jacquard_common::CowStr<'a>>>, 119 - ) -> Self { 120 self.__unsafe_private_named.1 = value.into(); 121 self 122 } 123 - /// Set the `displayName` field to an Option value (optional) 124 - pub fn maybe_display_name( 125 - mut self, 126 - value: Option<jacquard_common::CowStr<'a>>, 127 - ) -> Self { 128 self.__unsafe_private_named.1 = value; 129 self 130 } 131 } 132 133 - impl<'a, S: author_state::State> AuthorBuilder<'a, S> { 134 - /// Set the `handle` field (optional) 135 - pub fn handle( 136 - mut self, 137 - value: impl Into<Option<jacquard_common::types::string::Handle<'a>>>, 138 - ) -> Self { 139 - self.__unsafe_private_named.2 = value.into(); 140 - self 141 - } 142 - /// Set the `handle` field to an Option value (optional) 143 - pub fn maybe_handle( 144 - mut self, 145 - value: Option<jacquard_common::types::string::Handle<'a>>, 146 - ) -> Self { 147 - self.__unsafe_private_named.2 = value; 148 - self 149 - } 150 - } 151 - 152 impl<'a, S> AuthorBuilder<'a, S> 153 where 154 S: author_state::State, ··· 158 pub fn build(self) -> Author<'a> { 159 Author { 160 did: self.__unsafe_private_named.0.unwrap(), 161 - display_name: self.__unsafe_private_named.1, 162 - handle: self.__unsafe_private_named.2, 163 extra_data: Default::default(), 164 } 165 } ··· 173 ) -> Author<'a> { 174 Author { 175 did: self.__unsafe_private_named.0.unwrap(), 176 - display_name: self.__unsafe_private_named.1, 177 - handle: self.__unsafe_private_named.2, 178 extra_data: Some(extra_data), 179 } 180 } ··· 224 ); 225 map.insert( 226 ::jacquard_common::smol_str::SmolStr::new_static( 227 - "displayName", 228 ), 229 - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 230 description: None, 231 - format: None, 232 - default: None, 233 min_length: None, 234 - max_length: Some(640usize), 235 - min_graphemes: None, 236 - max_graphemes: Some(64usize), 237 - r#enum: None, 238 - r#const: None, 239 - known_values: None, 240 - }), 241 - ); 242 - map.insert( 243 - ::jacquard_common::smol_str::SmolStr::new_static("handle"), 244 - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 245 - description: None, 246 - format: Some( 247 - ::jacquard_lexicon::lexicon::LexStringFormat::Handle, 248 - ), 249 - default: None, 250 - min_length: None, 251 - max_length: None, 252 - min_graphemes: None, 253 - max_graphemes: None, 254 - r#enum: None, 255 - r#const: None, 256 - known_values: None, 257 }), 258 ); 259 map ··· 374 r#enum: None, 375 r#const: None, 376 known_values: None, 377 }), 378 ); 379 map.insert( ··· 559 ); 560 map.insert( 561 ::jacquard_common::smol_str::SmolStr::new_static( 562 "subscribedCount", 563 ), 564 ::jacquard_lexicon::lexicon::LexObjectProperty::Integer(::jacquard_lexicon::lexicon::LexInteger { ··· 580 minimum: None, 581 maximum: None, 582 r#enum: None, 583 r#const: None, 584 }), 585 ); ··· 949 fn validate( 950 &self, 951 ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { 952 - if let Some(ref value) = self.display_name { 953 - #[allow(unused_comparisons)] 954 - if <str>::len(value.as_ref()) > 640usize { 955 - return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength { 956 - path: ::jacquard_lexicon::validation::ValidationPath::from_field( 957 - "display_name", 958 - ), 959 - max: 640usize, 960 - actual: <str>::len(value.as_ref()), 961 - }); 962 - } 963 - } 964 - if let Some(ref value) = self.display_name { 965 - { 966 - let count = ::unicode_segmentation::UnicodeSegmentation::graphemes( 967 - value.as_ref(), 968 - true, 969 - ) 970 - .count(); 971 - if count > 64usize { 972 - return Err(::jacquard_lexicon::validation::ConstraintError::MaxGraphemes { 973 - path: ::jacquard_lexicon::validation::ValidationPath::from_field( 974 - "display_name", 975 - ), 976 - max: 64usize, 977 - actual: count, 978 - }); 979 - } 980 - } 981 - } 982 Ok(()) 983 } 984 } ··· 1319 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1320 #[serde(borrow)] 1321 pub banner: Option<jacquard_common::types::string::Uri<'a>>, 1322 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1323 pub created_at: Option<jacquard_common::types::string::Datetime>, 1324 #[serde(skip_serializing_if = "std::option::Option::is_none")] ··· 1351 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1352 #[serde(borrow)] 1353 pub pronouns: Option<crate::sh_weaver::actor::PronounsList<'a>>, 1354 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1355 pub subscribed_count: Option<i64>, 1356 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1357 pub subscriber_count: Option<i64>, 1358 } 1359 1360 pub mod profile_view_state { ··· 1407 __unsafe_private_named: ( 1408 ::core::option::Option<jacquard_common::types::string::Uri<'a>>, 1409 ::core::option::Option<jacquard_common::types::string::Uri<'a>>, 1410 ::core::option::Option<jacquard_common::types::string::Datetime>, 1411 ::core::option::Option<jacquard_common::CowStr<'a>>, 1412 ::core::option::Option<jacquard_common::types::string::Did<'a>>, ··· 1418 ::core::option::Option<jacquard_common::CowStr<'a>>, 1419 ::core::option::Option<crate::sh_weaver::actor::PinnedList<'a>>, 1420 ::core::option::Option<crate::sh_weaver::actor::PronounsList<'a>>, 1421 ::core::option::Option<i64>, 1422 ::core::option::Option<i64>, 1423 ), 1424 _phantom: ::core::marker::PhantomData<&'a ()>, 1425 } ··· 1452 None, 1453 None, 1454 None, 1455 ), 1456 _phantom: ::core::marker::PhantomData, 1457 } ··· 1497 } 1498 1499 impl<'a, S: profile_view_state::State> ProfileViewBuilder<'a, S> { 1500 /// Set the `createdAt` field (optional) 1501 pub fn created_at( 1502 mut self, 1503 value: impl Into<Option<jacquard_common::types::string::Datetime>>, 1504 ) -> Self { 1505 - self.__unsafe_private_named.2 = value.into(); 1506 self 1507 } 1508 /// Set the `createdAt` field to an Option value (optional) ··· 1510 mut self, 1511 value: Option<jacquard_common::types::string::Datetime>, 1512 ) -> Self { 1513 - self.__unsafe_private_named.2 = value; 1514 self 1515 } 1516 } ··· 1521 mut self, 1522 value: impl Into<Option<jacquard_common::CowStr<'a>>>, 1523 ) -> Self { 1524 - self.__unsafe_private_named.3 = value.into(); 1525 self 1526 } 1527 /// Set the `description` field to an Option value (optional) ··· 1529 mut self, 1530 value: Option<jacquard_common::CowStr<'a>>, 1531 ) -> Self { 1532 - self.__unsafe_private_named.3 = value; 1533 self 1534 } 1535 } ··· 1544 mut self, 1545 value: impl Into<jacquard_common::types::string::Did<'a>>, 1546 ) -> ProfileViewBuilder<'a, profile_view_state::SetDid<S>> { 1547 - self.__unsafe_private_named.4 = ::core::option::Option::Some(value.into()); 1548 ProfileViewBuilder { 1549 _phantom_state: ::core::marker::PhantomData, 1550 __unsafe_private_named: self.__unsafe_private_named, ··· 1559 mut self, 1560 value: impl Into<Option<jacquard_common::CowStr<'a>>>, 1561 ) -> Self { 1562 - self.__unsafe_private_named.5 = value.into(); 1563 self 1564 } 1565 /// Set the `displayName` field to an Option value (optional) ··· 1567 mut self, 1568 value: Option<jacquard_common::CowStr<'a>>, 1569 ) -> Self { 1570 - self.__unsafe_private_named.5 = value; 1571 self 1572 } 1573 } ··· 1582 mut self, 1583 value: impl Into<jacquard_common::types::string::Handle<'a>>, 1584 ) -> ProfileViewBuilder<'a, profile_view_state::SetHandle<S>> { 1585 - self.__unsafe_private_named.6 = ::core::option::Option::Some(value.into()); 1586 ProfileViewBuilder { 1587 _phantom_state: ::core::marker::PhantomData, 1588 __unsafe_private_named: self.__unsafe_private_named, ··· 1597 mut self, 1598 value: impl Into<Option<jacquard_common::types::string::Datetime>>, 1599 ) -> Self { 1600 - self.__unsafe_private_named.7 = value.into(); 1601 self 1602 } 1603 /// Set the `indexedAt` field to an Option value (optional) ··· 1605 mut self, 1606 value: Option<jacquard_common::types::string::Datetime>, 1607 ) -> Self { 1608 - self.__unsafe_private_named.7 = value; 1609 self 1610 } 1611 } ··· 1616 mut self, 1617 value: impl Into<Option<Vec<crate::com_atproto::label::Label<'a>>>>, 1618 ) -> Self { 1619 - self.__unsafe_private_named.8 = value.into(); 1620 self 1621 } 1622 /// Set the `labels` field to an Option value (optional) ··· 1624 mut self, 1625 value: Option<Vec<crate::com_atproto::label::Label<'a>>>, 1626 ) -> Self { 1627 - self.__unsafe_private_named.8 = value; 1628 self 1629 } 1630 } ··· 1635 mut self, 1636 value: impl Into<Option<Vec<jacquard_common::types::string::Uri<'a>>>>, 1637 ) -> Self { 1638 - self.__unsafe_private_named.9 = value.into(); 1639 self 1640 } 1641 /// Set the `links` field to an Option value (optional) ··· 1643 mut self, 1644 value: Option<Vec<jacquard_common::types::string::Uri<'a>>>, 1645 ) -> Self { 1646 - self.__unsafe_private_named.9 = value; 1647 self 1648 } 1649 } ··· 1654 mut self, 1655 value: impl Into<Option<jacquard_common::CowStr<'a>>>, 1656 ) -> Self { 1657 - self.__unsafe_private_named.10 = value.into(); 1658 self 1659 } 1660 /// Set the `location` field to an Option value (optional) 1661 pub fn maybe_location(mut self, value: Option<jacquard_common::CowStr<'a>>) -> Self { 1662 - self.__unsafe_private_named.10 = value; 1663 self 1664 } 1665 } ··· 1670 mut self, 1671 value: impl Into<Option<crate::sh_weaver::actor::PinnedList<'a>>>, 1672 ) -> Self { 1673 - self.__unsafe_private_named.11 = value.into(); 1674 self 1675 } 1676 /// Set the `pinned` field to an Option value (optional) ··· 1678 mut self, 1679 value: Option<crate::sh_weaver::actor::PinnedList<'a>>, 1680 ) -> Self { 1681 - self.__unsafe_private_named.11 = value; 1682 self 1683 } 1684 } ··· 1689 mut self, 1690 value: impl Into<Option<crate::sh_weaver::actor::PronounsList<'a>>>, 1691 ) -> Self { 1692 - self.__unsafe_private_named.12 = value.into(); 1693 self 1694 } 1695 /// Set the `pronouns` field to an Option value (optional) ··· 1697 mut self, 1698 value: Option<crate::sh_weaver::actor::PronounsList<'a>>, 1699 ) -> Self { 1700 - self.__unsafe_private_named.12 = value; 1701 self 1702 } 1703 } ··· 1705 impl<'a, S: profile_view_state::State> ProfileViewBuilder<'a, S> { 1706 /// Set the `subscribedCount` field (optional) 1707 pub fn subscribed_count(mut self, value: impl Into<Option<i64>>) -> Self { 1708 - self.__unsafe_private_named.13 = value.into(); 1709 self 1710 } 1711 /// Set the `subscribedCount` field to an Option value (optional) 1712 pub fn maybe_subscribed_count(mut self, value: Option<i64>) -> Self { 1713 - self.__unsafe_private_named.13 = value; 1714 self 1715 } 1716 } ··· 1718 impl<'a, S: profile_view_state::State> ProfileViewBuilder<'a, S> { 1719 /// Set the `subscriberCount` field (optional) 1720 pub fn subscriber_count(mut self, value: impl Into<Option<i64>>) -> Self { 1721 - self.__unsafe_private_named.14 = value.into(); 1722 self 1723 } 1724 /// Set the `subscriberCount` field to an Option value (optional) 1725 pub fn maybe_subscriber_count(mut self, value: Option<i64>) -> Self { 1726 - self.__unsafe_private_named.14 = value; 1727 self 1728 } 1729 } ··· 1739 ProfileView { 1740 avatar: self.__unsafe_private_named.0, 1741 banner: self.__unsafe_private_named.1, 1742 - created_at: self.__unsafe_private_named.2, 1743 - description: self.__unsafe_private_named.3, 1744 - did: self.__unsafe_private_named.4.unwrap(), 1745 - display_name: self.__unsafe_private_named.5, 1746 - handle: self.__unsafe_private_named.6.unwrap(), 1747 - indexed_at: self.__unsafe_private_named.7, 1748 - labels: self.__unsafe_private_named.8, 1749 - links: self.__unsafe_private_named.9, 1750 - location: self.__unsafe_private_named.10, 1751 - pinned: self.__unsafe_private_named.11, 1752 - pronouns: self.__unsafe_private_named.12, 1753 - subscribed_count: self.__unsafe_private_named.13, 1754 - subscriber_count: self.__unsafe_private_named.14, 1755 extra_data: Default::default(), 1756 } 1757 } ··· 1766 ProfileView { 1767 avatar: self.__unsafe_private_named.0, 1768 banner: self.__unsafe_private_named.1, 1769 - created_at: self.__unsafe_private_named.2, 1770 - description: self.__unsafe_private_named.3, 1771 - did: self.__unsafe_private_named.4.unwrap(), 1772 - display_name: self.__unsafe_private_named.5, 1773 - handle: self.__unsafe_private_named.6.unwrap(), 1774 - indexed_at: self.__unsafe_private_named.7, 1775 - labels: self.__unsafe_private_named.8, 1776 - links: self.__unsafe_private_named.9, 1777 - location: self.__unsafe_private_named.10, 1778 - pinned: self.__unsafe_private_named.11, 1779 - pronouns: self.__unsafe_private_named.12, 1780 - subscribed_count: self.__unsafe_private_named.13, 1781 - subscriber_count: self.__unsafe_private_named.14, 1782 extra_data: Some(extra_data), 1783 } 1784 }
··· 23 pub struct Author<'a> { 24 #[serde(borrow)] 25 pub did: jacquard_common::types::string::Did<'a>, 26 + /// signed bytes of the corresponding notebook record in the author's repo 27 #[serde(skip_serializing_if = "std::option::Option::is_none")] 28 + pub signature: Option<bytes::Bytes>, 29 } 30 31 pub mod author_state { ··· 65 _phantom_state: ::core::marker::PhantomData<fn() -> S>, 66 __unsafe_private_named: ( 67 ::core::option::Option<jacquard_common::types::string::Did<'a>>, 68 + ::core::option::Option<bytes::Bytes>, 69 ), 70 _phantom: ::core::marker::PhantomData<&'a ()>, 71 } ··· 82 pub fn new() -> Self { 83 AuthorBuilder { 84 _phantom_state: ::core::marker::PhantomData, 85 + __unsafe_private_named: (None, None), 86 _phantom: ::core::marker::PhantomData, 87 } 88 } ··· 108 } 109 110 impl<'a, S: author_state::State> AuthorBuilder<'a, S> { 111 + /// Set the `signature` field (optional) 112 + pub fn signature(mut self, value: impl Into<Option<bytes::Bytes>>) -> Self { 113 self.__unsafe_private_named.1 = value.into(); 114 self 115 } 116 + /// Set the `signature` field to an Option value (optional) 117 + pub fn maybe_signature(mut self, value: Option<bytes::Bytes>) -> Self { 118 self.__unsafe_private_named.1 = value; 119 self 120 } 121 } 122 123 impl<'a, S> AuthorBuilder<'a, S> 124 where 125 S: author_state::State, ··· 129 pub fn build(self) -> Author<'a> { 130 Author { 131 did: self.__unsafe_private_named.0.unwrap(), 132 + signature: self.__unsafe_private_named.1, 133 extra_data: Default::default(), 134 } 135 } ··· 143 ) -> Author<'a> { 144 Author { 145 did: self.__unsafe_private_named.0.unwrap(), 146 + signature: self.__unsafe_private_named.1, 147 extra_data: Some(extra_data), 148 } 149 } ··· 193 ); 194 map.insert( 195 ::jacquard_common::smol_str::SmolStr::new_static( 196 + "signature", 197 ), 198 + ::jacquard_lexicon::lexicon::LexObjectProperty::Bytes(::jacquard_lexicon::lexicon::LexBytes { 199 description: None, 200 + max_length: None, 201 min_length: None, 202 }), 203 ); 204 map ··· 319 r#enum: None, 320 r#const: None, 321 known_values: None, 322 + }), 323 + ); 324 + map.insert( 325 + ::jacquard_common::smol_str::SmolStr::new_static("bluesky"), 326 + ::jacquard_lexicon::lexicon::LexObjectProperty::Boolean(::jacquard_lexicon::lexicon::LexBoolean { 327 + description: None, 328 + default: None, 329 + r#const: None, 330 }), 331 ); 332 map.insert( ··· 512 ); 513 map.insert( 514 ::jacquard_common::smol_str::SmolStr::new_static( 515 + "streamplace", 516 + ), 517 + ::jacquard_lexicon::lexicon::LexObjectProperty::Boolean(::jacquard_lexicon::lexicon::LexBoolean { 518 + description: None, 519 + default: None, 520 + r#const: None, 521 + }), 522 + ); 523 + map.insert( 524 + ::jacquard_common::smol_str::SmolStr::new_static( 525 "subscribedCount", 526 ), 527 ::jacquard_lexicon::lexicon::LexObjectProperty::Integer(::jacquard_lexicon::lexicon::LexInteger { ··· 543 minimum: None, 544 maximum: None, 545 r#enum: None, 546 + r#const: None, 547 + }), 548 + ); 549 + map.insert( 550 + ::jacquard_common::smol_str::SmolStr::new_static("tangled"), 551 + ::jacquard_lexicon::lexicon::LexObjectProperty::Boolean(::jacquard_lexicon::lexicon::LexBoolean { 552 + description: None, 553 + default: None, 554 r#const: None, 555 }), 556 ); ··· 920 fn validate( 921 &self, 922 ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { 923 Ok(()) 924 } 925 } ··· 1260 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1261 #[serde(borrow)] 1262 pub banner: Option<jacquard_common::types::string::Uri<'a>>, 1263 + /// Include link to this account on Bluesky. 1264 + #[serde(skip_serializing_if = "std::option::Option::is_none")] 1265 + pub bluesky: Option<bool>, 1266 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1267 pub created_at: Option<jacquard_common::types::string::Datetime>, 1268 #[serde(skip_serializing_if = "std::option::Option::is_none")] ··· 1295 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1296 #[serde(borrow)] 1297 pub pronouns: Option<crate::sh_weaver::actor::PronounsList<'a>>, 1298 + /// Include link to this account on stream.place. 1299 + #[serde(skip_serializing_if = "std::option::Option::is_none")] 1300 + pub streamplace: Option<bool>, 1301 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1302 pub subscribed_count: Option<i64>, 1303 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1304 pub subscriber_count: Option<i64>, 1305 + /// Include link to this account on Tangled. 1306 + #[serde(skip_serializing_if = "std::option::Option::is_none")] 1307 + pub tangled: Option<bool>, 1308 } 1309 1310 pub mod profile_view_state { ··· 1357 __unsafe_private_named: ( 1358 ::core::option::Option<jacquard_common::types::string::Uri<'a>>, 1359 ::core::option::Option<jacquard_common::types::string::Uri<'a>>, 1360 + ::core::option::Option<bool>, 1361 ::core::option::Option<jacquard_common::types::string::Datetime>, 1362 ::core::option::Option<jacquard_common::CowStr<'a>>, 1363 ::core::option::Option<jacquard_common::types::string::Did<'a>>, ··· 1369 ::core::option::Option<jacquard_common::CowStr<'a>>, 1370 ::core::option::Option<crate::sh_weaver::actor::PinnedList<'a>>, 1371 ::core::option::Option<crate::sh_weaver::actor::PronounsList<'a>>, 1372 + ::core::option::Option<bool>, 1373 ::core::option::Option<i64>, 1374 ::core::option::Option<i64>, 1375 + ::core::option::Option<bool>, 1376 ), 1377 _phantom: ::core::marker::PhantomData<&'a ()>, 1378 } ··· 1405 None, 1406 None, 1407 None, 1408 + None, 1409 + None, 1410 + None, 1411 ), 1412 _phantom: ::core::marker::PhantomData, 1413 } ··· 1453 } 1454 1455 impl<'a, S: profile_view_state::State> ProfileViewBuilder<'a, S> { 1456 + /// Set the `bluesky` field (optional) 1457 + pub fn bluesky(mut self, value: impl Into<Option<bool>>) -> Self { 1458 + self.__unsafe_private_named.2 = value.into(); 1459 + self 1460 + } 1461 + /// Set the `bluesky` field to an Option value (optional) 1462 + pub fn maybe_bluesky(mut self, value: Option<bool>) -> Self { 1463 + self.__unsafe_private_named.2 = value; 1464 + self 1465 + } 1466 + } 1467 + 1468 + impl<'a, S: profile_view_state::State> ProfileViewBuilder<'a, S> { 1469 /// Set the `createdAt` field (optional) 1470 pub fn created_at( 1471 mut self, 1472 value: impl Into<Option<jacquard_common::types::string::Datetime>>, 1473 ) -> Self { 1474 + self.__unsafe_private_named.3 = value.into(); 1475 self 1476 } 1477 /// Set the `createdAt` field to an Option value (optional) ··· 1479 mut self, 1480 value: Option<jacquard_common::types::string::Datetime>, 1481 ) -> Self { 1482 + self.__unsafe_private_named.3 = value; 1483 self 1484 } 1485 } ··· 1490 mut self, 1491 value: impl Into<Option<jacquard_common::CowStr<'a>>>, 1492 ) -> Self { 1493 + self.__unsafe_private_named.4 = value.into(); 1494 self 1495 } 1496 /// Set the `description` field to an Option value (optional) ··· 1498 mut self, 1499 value: Option<jacquard_common::CowStr<'a>>, 1500 ) -> Self { 1501 + self.__unsafe_private_named.4 = value; 1502 self 1503 } 1504 } ··· 1513 mut self, 1514 value: impl Into<jacquard_common::types::string::Did<'a>>, 1515 ) -> ProfileViewBuilder<'a, profile_view_state::SetDid<S>> { 1516 + self.__unsafe_private_named.5 = ::core::option::Option::Some(value.into()); 1517 ProfileViewBuilder { 1518 _phantom_state: ::core::marker::PhantomData, 1519 __unsafe_private_named: self.__unsafe_private_named, ··· 1528 mut self, 1529 value: impl Into<Option<jacquard_common::CowStr<'a>>>, 1530 ) -> Self { 1531 + self.__unsafe_private_named.6 = value.into(); 1532 self 1533 } 1534 /// Set the `displayName` field to an Option value (optional) ··· 1536 mut self, 1537 value: Option<jacquard_common::CowStr<'a>>, 1538 ) -> Self { 1539 + self.__unsafe_private_named.6 = value; 1540 self 1541 } 1542 } ··· 1551 mut self, 1552 value: impl Into<jacquard_common::types::string::Handle<'a>>, 1553 ) -> ProfileViewBuilder<'a, profile_view_state::SetHandle<S>> { 1554 + self.__unsafe_private_named.7 = ::core::option::Option::Some(value.into()); 1555 ProfileViewBuilder { 1556 _phantom_state: ::core::marker::PhantomData, 1557 __unsafe_private_named: self.__unsafe_private_named, ··· 1566 mut self, 1567 value: impl Into<Option<jacquard_common::types::string::Datetime>>, 1568 ) -> Self { 1569 + self.__unsafe_private_named.8 = value.into(); 1570 self 1571 } 1572 /// Set the `indexedAt` field to an Option value (optional) ··· 1574 mut self, 1575 value: Option<jacquard_common::types::string::Datetime>, 1576 ) -> Self { 1577 + self.__unsafe_private_named.8 = value; 1578 self 1579 } 1580 } ··· 1585 mut self, 1586 value: impl Into<Option<Vec<crate::com_atproto::label::Label<'a>>>>, 1587 ) -> Self { 1588 + self.__unsafe_private_named.9 = value.into(); 1589 self 1590 } 1591 /// Set the `labels` field to an Option value (optional) ··· 1593 mut self, 1594 value: Option<Vec<crate::com_atproto::label::Label<'a>>>, 1595 ) -> Self { 1596 + self.__unsafe_private_named.9 = value; 1597 self 1598 } 1599 } ··· 1604 mut self, 1605 value: impl Into<Option<Vec<jacquard_common::types::string::Uri<'a>>>>, 1606 ) -> Self { 1607 + self.__unsafe_private_named.10 = value.into(); 1608 self 1609 } 1610 /// Set the `links` field to an Option value (optional) ··· 1612 mut self, 1613 value: Option<Vec<jacquard_common::types::string::Uri<'a>>>, 1614 ) -> Self { 1615 + self.__unsafe_private_named.10 = value; 1616 self 1617 } 1618 } ··· 1623 mut self, 1624 value: impl Into<Option<jacquard_common::CowStr<'a>>>, 1625 ) -> Self { 1626 + self.__unsafe_private_named.11 = value.into(); 1627 self 1628 } 1629 /// Set the `location` field to an Option value (optional) 1630 pub fn maybe_location(mut self, value: Option<jacquard_common::CowStr<'a>>) -> Self { 1631 + self.__unsafe_private_named.11 = value; 1632 self 1633 } 1634 } ··· 1639 mut self, 1640 value: impl Into<Option<crate::sh_weaver::actor::PinnedList<'a>>>, 1641 ) -> Self { 1642 + self.__unsafe_private_named.12 = value.into(); 1643 self 1644 } 1645 /// Set the `pinned` field to an Option value (optional) ··· 1647 mut self, 1648 value: Option<crate::sh_weaver::actor::PinnedList<'a>>, 1649 ) -> Self { 1650 + self.__unsafe_private_named.12 = value; 1651 self 1652 } 1653 } ··· 1658 mut self, 1659 value: impl Into<Option<crate::sh_weaver::actor::PronounsList<'a>>>, 1660 ) -> Self { 1661 + self.__unsafe_private_named.13 = value.into(); 1662 self 1663 } 1664 /// Set the `pronouns` field to an Option value (optional) ··· 1666 mut self, 1667 value: Option<crate::sh_weaver::actor::PronounsList<'a>>, 1668 ) -> Self { 1669 + self.__unsafe_private_named.13 = value; 1670 + self 1671 + } 1672 + } 1673 + 1674 + impl<'a, S: profile_view_state::State> ProfileViewBuilder<'a, S> { 1675 + /// Set the `streamplace` field (optional) 1676 + pub fn streamplace(mut self, value: impl Into<Option<bool>>) -> Self { 1677 + self.__unsafe_private_named.14 = value.into(); 1678 + self 1679 + } 1680 + /// Set the `streamplace` field to an Option value (optional) 1681 + pub fn maybe_streamplace(mut self, value: Option<bool>) -> Self { 1682 + self.__unsafe_private_named.14 = value; 1683 self 1684 } 1685 } ··· 1687 impl<'a, S: profile_view_state::State> ProfileViewBuilder<'a, S> { 1688 /// Set the `subscribedCount` field (optional) 1689 pub fn subscribed_count(mut self, value: impl Into<Option<i64>>) -> Self { 1690 + self.__unsafe_private_named.15 = value.into(); 1691 self 1692 } 1693 /// Set the `subscribedCount` field to an Option value (optional) 1694 pub fn maybe_subscribed_count(mut self, value: Option<i64>) -> Self { 1695 + self.__unsafe_private_named.15 = value; 1696 self 1697 } 1698 } ··· 1700 impl<'a, S: profile_view_state::State> ProfileViewBuilder<'a, S> { 1701 /// Set the `subscriberCount` field (optional) 1702 pub fn subscriber_count(mut self, value: impl Into<Option<i64>>) -> Self { 1703 + self.__unsafe_private_named.16 = value.into(); 1704 self 1705 } 1706 /// Set the `subscriberCount` field to an Option value (optional) 1707 pub fn maybe_subscriber_count(mut self, value: Option<i64>) -> Self { 1708 + self.__unsafe_private_named.16 = value; 1709 + self 1710 + } 1711 + } 1712 + 1713 + impl<'a, S: profile_view_state::State> ProfileViewBuilder<'a, S> { 1714 + /// Set the `tangled` field (optional) 1715 + pub fn tangled(mut self, value: impl Into<Option<bool>>) -> Self { 1716 + self.__unsafe_private_named.17 = value.into(); 1717 + self 1718 + } 1719 + /// Set the `tangled` field to an Option value (optional) 1720 + pub fn maybe_tangled(mut self, value: Option<bool>) -> Self { 1721 + self.__unsafe_private_named.17 = value; 1722 self 1723 } 1724 } ··· 1734 ProfileView { 1735 avatar: self.__unsafe_private_named.0, 1736 banner: self.__unsafe_private_named.1, 1737 + bluesky: self.__unsafe_private_named.2, 1738 + created_at: self.__unsafe_private_named.3, 1739 + description: self.__unsafe_private_named.4, 1740 + did: self.__unsafe_private_named.5.unwrap(), 1741 + display_name: self.__unsafe_private_named.6, 1742 + handle: self.__unsafe_private_named.7.unwrap(), 1743 + indexed_at: self.__unsafe_private_named.8, 1744 + labels: self.__unsafe_private_named.9, 1745 + links: self.__unsafe_private_named.10, 1746 + location: self.__unsafe_private_named.11, 1747 + pinned: self.__unsafe_private_named.12, 1748 + pronouns: self.__unsafe_private_named.13, 1749 + streamplace: self.__unsafe_private_named.14, 1750 + subscribed_count: self.__unsafe_private_named.15, 1751 + subscriber_count: self.__unsafe_private_named.16, 1752 + tangled: self.__unsafe_private_named.17, 1753 extra_data: Default::default(), 1754 } 1755 } ··· 1764 ProfileView { 1765 avatar: self.__unsafe_private_named.0, 1766 banner: self.__unsafe_private_named.1, 1767 + bluesky: self.__unsafe_private_named.2, 1768 + created_at: self.__unsafe_private_named.3, 1769 + description: self.__unsafe_private_named.4, 1770 + did: self.__unsafe_private_named.5.unwrap(), 1771 + display_name: self.__unsafe_private_named.6, 1772 + handle: self.__unsafe_private_named.7.unwrap(), 1773 + indexed_at: self.__unsafe_private_named.8, 1774 + labels: self.__unsafe_private_named.9, 1775 + links: self.__unsafe_private_named.10, 1776 + location: self.__unsafe_private_named.11, 1777 + pinned: self.__unsafe_private_named.12, 1778 + pronouns: self.__unsafe_private_named.13, 1779 + streamplace: self.__unsafe_private_named.14, 1780 + subscribed_count: self.__unsafe_private_named.15, 1781 + subscriber_count: self.__unsafe_private_named.16, 1782 + tangled: self.__unsafe_private_named.17, 1783 extra_data: Some(extra_data), 1784 } 1785 }
+136
crates/weaver-app/assets/styling/profile.css
···
··· 1 + /* Profile display - sidebar on desktop, header on mobile */ 2 + 3 + .profile-display { 4 + background: var(--color-surface); 5 + border: 1px solid var(--color-border); 6 + border-radius: 4px; 7 + } 8 + 9 + .profile-banner { 10 + width: 100%; 11 + height: 120px; 12 + overflow: hidden; 13 + border-radius: 4px 4px 0 0; 14 + } 15 + 16 + .profile-banner img { 17 + width: 100%; 18 + height: 100%; 19 + object-fit: cover; 20 + } 21 + 22 + .profile-content { 23 + padding: 1.5rem; 24 + } 25 + 26 + .profile-identity { 27 + margin-bottom: 1.5rem; 28 + } 29 + 30 + .profile-identity .avatar { 31 + width: 120px; 32 + height: 120px; 33 + margin-bottom: 1rem; 34 + } 35 + 36 + .profile-name-section { 37 + margin-bottom: 0.75rem; 38 + } 39 + 40 + .profile-display-name { 41 + font-size: 1.5rem; 42 + font-weight: 600; 43 + color: var(--color-text); 44 + margin: 0 0 0.25rem 0; 45 + font-family: var(--font-heading); 46 + } 47 + 48 + .profile-pronouns { 49 + font-size: 1rem; 50 + font-weight: 400; 51 + color: var(--color-muted); 52 + } 53 + 54 + .profile-handle { 55 + font-size: 0.95rem; 56 + color: var(--color-subtle); 57 + margin-bottom: 0.5rem; 58 + } 59 + 60 + .profile-location { 61 + font-size: 0.9rem; 62 + color: var(--color-muted); 63 + } 64 + 65 + .profile-description { 66 + color: var(--color-text); 67 + line-height: 1.5; 68 + margin-top: 0.75rem; 69 + white-space: pre-wrap; 70 + } 71 + 72 + .profile-stats { 73 + display: grid; 74 + grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); 75 + gap: 1rem; 76 + padding: 1rem 0; 77 + margin: 1rem 0; 78 + border-top: 1px solid var(--color-border); 79 + border-bottom: 1px solid var(--color-border); 80 + } 81 + 82 + .profile-stat { 83 + display: flex; 84 + flex-direction: column; 85 + gap: 0.25rem; 86 + } 87 + 88 + .profile-stat-label { 89 + font-size: 0.85rem; 90 + color: var(--color-subtle); 91 + } 92 + 93 + .profile-stat-value { 94 + font-size: 1.25rem; 95 + font-weight: 600; 96 + color: var(--color-primary); 97 + } 98 + 99 + .profile-links { 100 + display: flex; 101 + flex-direction: column; 102 + gap: 0.5rem; 103 + } 104 + 105 + .profile-link { 106 + color: var(--color-link); 107 + text-decoration: none; 108 + font-size: 0.9rem; 109 + transition: color 0.15s ease; 110 + } 111 + 112 + .profile-link:hover { 113 + color: var(--color-primary); 114 + } 115 + 116 + .profile-link-platform { 117 + font-weight: 500; 118 + } 119 + 120 + .profile-loading { 121 + padding: 2rem; 122 + text-align: center; 123 + color: var(--color-muted); 124 + } 125 + 126 + /* Mobile layout */ 127 + @media (max-width: 768px) { 128 + .profile-identity .avatar { 129 + width: 80px; 130 + height: 80px; 131 + } 132 + 133 + .profile-display-name { 134 + font-size: 1.25rem; 135 + } 136 + }
+3
crates/weaver-app/src/components/mod.rs
··· 11 pub mod identity; 12 pub use identity::{NotebookCard, Repository, RepositoryIndex}; 13 pub mod avatar;
··· 11 pub mod identity; 12 pub use identity::{NotebookCard, Repository, RepositoryIndex}; 13 pub mod avatar; 14 + 15 + pub mod profile; 16 + pub use profile::ProfileDisplay;
+285
crates/weaver-app/src/components/profile.rs
···
··· 1 + #![allow(non_snake_case)] 2 + 3 + use crate::{ 4 + components::avatar::{Avatar, AvatarImage}, 5 + Route, 6 + }; 7 + use dioxus::prelude::*; 8 + use jacquard::types::ident::AtIdentifier; 9 + use weaver_api::sh_weaver::actor::ProfileDataViewInner; 10 + 11 + const PROFILE_CSS: Asset = asset!("/assets/styling/profile.css"); 12 + 13 + #[component] 14 + pub fn ProfileDisplay(ident: AtIdentifier<'static>) -> Element { 15 + // Fetch profile data 16 + let profile = crate::data::use_profile_data(ident.clone())?; 17 + 18 + match profile().as_ref() { 19 + Some(profile_view) => rsx! { 20 + document::Link { rel: "stylesheet", href: PROFILE_CSS } 21 + 22 + div { class: "profile-display", 23 + // Banner if present 24 + {match &profile_view.inner { 25 + ProfileDataViewInner::ProfileView(p) => { 26 + if let Some(ref banner) = p.banner { 27 + rsx! { 28 + div { class: "profile-banner", 29 + img { src: "{banner.as_ref()}", alt: "Profile banner" } 30 + } 31 + } 32 + } else { 33 + rsx! { } 34 + } 35 + } 36 + _ => rsx! { } 37 + }} 38 + 39 + div { class: "profile-content", 40 + // Avatar and identity 41 + ProfileIdentity { profile_view: profile_view.clone(), ident: ident.clone() } 42 + 43 + // Stats 44 + ProfileStats { ident: ident.clone() } 45 + 46 + // Links 47 + ProfileLinks { profile_view: profile_view.clone(), ident: ident.clone() } 48 + } 49 + } 50 + }, 51 + None => rsx! { 52 + div { class: "profile-display profile-loading", 53 + "Loading profile..." 54 + } 55 + }, 56 + } 57 + } 58 + 59 + #[component] 60 + fn ProfileIdentity( 61 + profile_view: weaver_api::sh_weaver::actor::ProfileDataView<'static>, 62 + ident: AtIdentifier<'static>, 63 + ) -> Element { 64 + match &profile_view.inner { 65 + ProfileDataViewInner::ProfileView(profile) => { 66 + let display_name = profile 67 + .display_name 68 + .as_ref() 69 + .map(|n| n.as_ref()) 70 + .unwrap_or("Unknown"); 71 + 72 + // Format pronouns 73 + let pronouns_text = if let Some(ref pronouns) = profile.pronouns { 74 + if !pronouns.is_empty() { 75 + Some( 76 + pronouns 77 + .iter() 78 + .map(|p| p.as_ref()) 79 + .collect::<Vec<_>>() 80 + .join(", "), 81 + ) 82 + } else { 83 + None 84 + } 85 + } else { 86 + None 87 + }; 88 + 89 + rsx! { 90 + div { class: "profile-identity", 91 + if let Some(ref avatar) = profile.avatar { 92 + Avatar { 93 + AvatarImage { src: avatar.as_ref() } 94 + } 95 + } 96 + 97 + div { class: "profile-name-section", 98 + h1 { class: "profile-display-name", 99 + "{display_name}" 100 + if let Some(ref pronouns) = pronouns_text { 101 + span { class: "profile-pronouns", " ({pronouns})" } 102 + } 103 + } 104 + div { class: "profile-handle", "@{ident}" } 105 + 106 + if let Some(ref location) = profile.location { 107 + div { class: "profile-location", "{location}" } 108 + } 109 + } 110 + 111 + if let Some(ref description) = profile.description { 112 + div { class: "profile-description", "{description}" } 113 + } 114 + } 115 + } 116 + } 117 + ProfileDataViewInner::ProfileViewDetailed(profile) => { 118 + let display_name = profile 119 + .display_name 120 + .as_ref() 121 + .map(|n| n.as_ref()) 122 + .unwrap_or("Unknown"); 123 + 124 + rsx! { 125 + div { class: "profile-identity", 126 + if let Some(ref avatar) = profile.avatar { 127 + Avatar { 128 + AvatarImage { src: avatar.as_ref() } 129 + } 130 + } 131 + 132 + div { class: "profile-name-section", 133 + h1 { class: "profile-display-name", "{display_name}" } 134 + div { class: "profile-handle", "@{ident}" } 135 + } 136 + 137 + if let Some(ref description) = profile.description { 138 + div { class: "profile-description", "{description}" } 139 + } 140 + } 141 + } 142 + } 143 + ProfileDataViewInner::TangledProfileView(profile) => { 144 + rsx! { 145 + div { class: "profile-identity", 146 + div { class: "profile-name-section", 147 + h1 { class: "profile-display-name", "@{profile.handle.as_ref()}" } 148 + div { class: "profile-handle", "{ident}" } 149 + 150 + if let Some(ref location) = profile.location { 151 + div { class: "profile-location", "{location}" } 152 + } 153 + } 154 + 155 + if let Some(ref description) = profile.description { 156 + div { class: "profile-description", "{description}" } 157 + } 158 + } 159 + } 160 + } 161 + _ => rsx! { 162 + div { class: "profile-identity", 163 + "Unknown profile type" 164 + } 165 + }, 166 + } 167 + } 168 + 169 + #[component] 170 + fn ProfileStats(ident: AtIdentifier<'static>) -> Element { 171 + // Fetch notebook count 172 + let notebooks = crate::data::use_notebooks_for_did(ident.clone())?; 173 + 174 + let notebook_count = notebooks().as_ref().map(|n| n.len()).unwrap_or(0); 175 + 176 + rsx! { 177 + div { class: "profile-stats", 178 + div { class: "profile-stat", 179 + span { class: "profile-stat-label", "Notebooks" } 180 + span { class: "profile-stat-value", "{notebook_count}" } 181 + } 182 + // TODO: Add entry count, subscriber counts when available 183 + } 184 + } 185 + } 186 + 187 + #[component] 188 + fn ProfileLinks( 189 + profile_view: weaver_api::sh_weaver::actor::ProfileDataView<'static>, 190 + ident: AtIdentifier<'static>, 191 + ) -> Element { 192 + match &profile_view.inner { 193 + ProfileDataViewInner::ProfileView(profile) => { 194 + rsx! { 195 + div { class: "profile-links", 196 + // Generic links 197 + if let Some(ref links) = profile.links { 198 + for link in links.iter() { 199 + a { 200 + href: "{link.as_ref()}", 201 + target: "_blank", 202 + rel: "noopener noreferrer", 203 + class: "profile-link", 204 + "{link.as_ref()}" 205 + } 206 + } 207 + } 208 + 209 + // Platform-specific links 210 + if profile.bluesky.unwrap_or(false) { 211 + a { 212 + href: "https://bsky.app/profile/{ident}", 213 + target: "_blank", 214 + rel: "noopener noreferrer", 215 + class: "profile-link profile-link-platform", 216 + "View on Bluesky" 217 + } 218 + } 219 + 220 + if profile.tangled.unwrap_or(false) { 221 + a { 222 + href: "https://tangled.dev/{ident}", 223 + target: "_blank", 224 + rel: "noopener noreferrer", 225 + class: "profile-link profile-link-platform", 226 + "View on Tangled" 227 + } 228 + } 229 + 230 + if profile.streamplace.unwrap_or(false) { 231 + a { 232 + href: "https://stream.place/{ident}", 233 + target: "_blank", 234 + rel: "noopener noreferrer", 235 + class: "profile-link profile-link-platform", 236 + "View on stream.place" 237 + } 238 + } 239 + } 240 + } 241 + } 242 + ProfileDataViewInner::ProfileViewDetailed(_profile) => { 243 + // Bluesky ProfileViewDetailed - doesn't have weaver platform flags 244 + rsx! { 245 + div { class: "profile-links", 246 + a { 247 + href: "https://bsky.app/profile/{ident}", 248 + target: "_blank", 249 + rel: "noopener noreferrer", 250 + class: "profile-link profile-link-platform", 251 + "View on Bluesky" 252 + } 253 + } 254 + } 255 + } 256 + ProfileDataViewInner::TangledProfileView(profile) => { 257 + rsx! { 258 + div { class: "profile-links", 259 + if let Some(ref links) = profile.links { 260 + for link in links.iter() { 261 + a { 262 + href: "{link.as_ref()}", 263 + target: "_blank", 264 + rel: "noopener noreferrer", 265 + class: "profile-link", 266 + "{link.as_ref()}" 267 + } 268 + } 269 + } 270 + 271 + if profile.bluesky { 272 + a { 273 + href: "https://bsky.app/profile/{ident}", 274 + target: "_blank", 275 + rel: "noopener noreferrer", 276 + class: "profile-link profile-link-platform", 277 + "View on Bluesky" 278 + } 279 + } 280 + } 281 + } 282 + } 283 + _ => rsx! {}, 284 + } 285 + }
+121
crates/weaver-app/src/data.rs
··· 236 html_buf 237 } 238 239 #[cfg(feature = "fullstack-server")] 240 #[put("/cache/{ident}/{cid}?name", cache: Extension<Arc<BlobCache>>)] 241 pub async fn cache_blob(ident: SmolStr, cid: SmolStr, name: Option<SmolStr>) -> Result<()> {
··· 236 html_buf 237 } 238 239 + /// Fetches profile data for a given identifier 240 + #[cfg(feature = "fullstack-server")] 241 + pub fn use_profile_data( 242 + ident: AtIdentifier<'static>, 243 + ) -> Result<Memo<Option<weaver_api::sh_weaver::actor::ProfileDataView<'static>>>, RenderError> { 244 + let fetcher = use_context::<crate::fetch::CachedFetcher>(); 245 + let ident = use_signal(|| ident); 246 + let res = use_server_future(move || { 247 + let fetcher = fetcher.clone(); 248 + async move { 249 + fetcher 250 + .fetch_profile(&ident()) 251 + .await 252 + .ok() 253 + .map(|arc| serde_json::to_value(&*arc).ok()) 254 + .flatten() 255 + } 256 + })?; 257 + Ok(use_memo(move || { 258 + if let Some(Some(value)) = &*res.read_unchecked() { 259 + jacquard::from_json_value::<weaver_api::sh_weaver::actor::ProfileDataView>(value.clone()).ok() 260 + } else { 261 + None 262 + } 263 + })) 264 + } 265 + 266 + /// Fetches profile data client-side only (no SSR) 267 + #[cfg(not(feature = "fullstack-server"))] 268 + pub fn use_profile_data( 269 + ident: AtIdentifier<'static>, 270 + ) -> Result<Memo<Option<weaver_api::sh_weaver::actor::ProfileDataView<'static>>>, RenderError> { 271 + let fetcher = use_context::<crate::fetch::CachedFetcher>(); 272 + let r = use_resource(use_reactive!(|ident| { 273 + let fetcher = fetcher.clone(); 274 + async move { fetcher.fetch_profile(&ident).await.ok().map(|arc| (*arc).clone()) } 275 + })); 276 + Ok(use_memo(move || { 277 + r.read_unchecked().as_ref().and_then(|v| v.clone()) 278 + })) 279 + } 280 + 281 + /// Fetches notebooks for a specific DID 282 + #[cfg(feature = "fullstack-server")] 283 + pub fn use_notebooks_for_did( 284 + ident: AtIdentifier<'static>, 285 + ) -> Result< 286 + Memo< 287 + Option< 288 + Vec<( 289 + weaver_api::sh_weaver::notebook::NotebookView<'static>, 290 + Vec<weaver_api::com_atproto::repo::strong_ref::StrongRef<'static>>, 291 + )>, 292 + >, 293 + >, 294 + RenderError, 295 + > { 296 + let fetcher = use_context::<crate::fetch::CachedFetcher>(); 297 + let ident = use_signal(|| ident); 298 + let res = use_server_future(move || { 299 + let fetcher = fetcher.clone(); 300 + async move { 301 + fetcher 302 + .fetch_notebooks_for_did(&ident()) 303 + .await 304 + .ok() 305 + .map(|notebooks| { 306 + notebooks 307 + .iter() 308 + .map(|arc| serde_json::to_value(arc.as_ref()).ok()) 309 + .collect::<Option<Vec<_>>>() 310 + }) 311 + .flatten() 312 + } 313 + })?; 314 + Ok(use_memo(move || { 315 + if let Some(Some(values)) = &*res.read_unchecked() { 316 + values 317 + .iter() 318 + .map(|v| jacquard::from_json_value::<( 319 + weaver_api::sh_weaver::notebook::NotebookView, 320 + Vec<weaver_api::com_atproto::repo::strong_ref::StrongRef>, 321 + )>(v.clone()).ok()) 322 + .collect::<Option<Vec<_>>>() 323 + } else { 324 + None 325 + } 326 + })) 327 + } 328 + 329 + /// Fetches notebooks client-side only (no SSR) 330 + #[cfg(not(feature = "fullstack-server"))] 331 + pub fn use_notebooks_for_did( 332 + ident: AtIdentifier<'static>, 333 + ) -> Result< 334 + Memo< 335 + Option< 336 + Vec<( 337 + weaver_api::sh_weaver::notebook::NotebookView<'static>, 338 + Vec<weaver_api::com_atproto::repo::strong_ref::StrongRef<'static>>, 339 + )>, 340 + >, 341 + >, 342 + RenderError, 343 + > { 344 + let fetcher = use_context::<crate::fetch::CachedFetcher>(); 345 + let r = use_resource(use_reactive!(|ident| { 346 + let fetcher = fetcher.clone(); 347 + async move { 348 + fetcher 349 + .fetch_notebooks_for_did(&ident) 350 + .await 351 + .ok() 352 + .map(|notebooks| notebooks.iter().map(|arc| (*arc).clone()).collect()) 353 + } 354 + })); 355 + Ok(use_memo(move || { 356 + r.read_unchecked().as_ref().and_then(|v| v.clone()) 357 + })) 358 + } 359 + 360 #[cfg(feature = "fullstack-server")] 361 #[put("/cache/{ident}/{cid}?name", cache: Extension<Arc<BlobCache>>)] 362 pub async fn cache_blob(ident: SmolStr, cid: SmolStr, name: Option<SmolStr>) -> Result<()> {
+39 -1
crates/weaver-app/src/fetch.rs
··· 6 use std::{sync::Arc, time::Duration}; 7 use weaver_api::{ 8 com_atproto::repo::strong_ref::StrongRef, 9 - sh_weaver::notebook::{entry::Entry, BookEntryView, NotebookView}, 10 }; 11 use weaver_common::WeaverExt; 12 ··· 30 (AtIdentifier<'static>, SmolStr), 31 Arc<(BookEntryView<'static>, Entry<'static>)>, 32 >, 33 } 34 35 impl CachedFetcher { ··· 38 client, 39 book_cache: cache_impl::new_cache(100, Duration::from_secs(1200)), 40 entry_cache: cache_impl::new_cache(100, Duration::from_secs(600)), 41 } 42 } 43 ··· 234 } else { 235 Ok(None) 236 } 237 } 238 }
··· 6 use std::{sync::Arc, time::Duration}; 7 use weaver_api::{ 8 com_atproto::repo::strong_ref::StrongRef, 9 + sh_weaver::{ 10 + actor::ProfileDataView, 11 + notebook::{entry::Entry, BookEntryView, NotebookView}, 12 + }, 13 }; 14 use weaver_common::WeaverExt; 15 ··· 33 (AtIdentifier<'static>, SmolStr), 34 Arc<(BookEntryView<'static>, Entry<'static>)>, 35 >, 36 + profile_cache: cache_impl::Cache<AtIdentifier<'static>, Arc<ProfileDataView<'static>>>, 37 } 38 39 impl CachedFetcher { ··· 42 client, 43 book_cache: cache_impl::new_cache(100, Duration::from_secs(1200)), 44 entry_cache: cache_impl::new_cache(100, Duration::from_secs(600)), 45 + profile_cache: cache_impl::new_cache(100, Duration::from_secs(1800)), 46 } 47 } 48 ··· 239 } else { 240 Ok(None) 241 } 242 + } 243 + 244 + pub async fn fetch_profile( 245 + &self, 246 + ident: &AtIdentifier<'_>, 247 + ) -> Result<Arc<ProfileDataView<'static>>> { 248 + use jacquard::IntoStatic; 249 + 250 + let ident_static = ident.clone().into_static(); 251 + 252 + if let Some(cached) = cache_impl::get(&self.profile_cache, &ident_static) { 253 + return Ok(cached); 254 + } 255 + 256 + let did = match ident { 257 + AtIdentifier::Did(d) => d.clone(), 258 + AtIdentifier::Handle(h) => self 259 + .client 260 + .resolve_handle(h) 261 + .await 262 + .map_err(|e| dioxus::CapturedError::from_display(e))?, 263 + }; 264 + 265 + let (_uri, profile_view) = self 266 + .client 267 + .hydrate_profile_view(&did) 268 + .await 269 + .map_err(|e| dioxus::CapturedError::from_display(e))?; 270 + 271 + let result = Arc::new(profile_view); 272 + cache_impl::insert(&self.profile_cache, ident_static, result.clone()); 273 + 274 + Ok(result) 275 } 276 }
+3
crates/weaver-common/src/agent.rs
··· 564 .maybe_description(weaver_record.value.description.clone()) 565 .maybe_avatar(avatar) 566 .maybe_banner(banner) 567 .maybe_location(weaver_record.value.location.clone()) 568 .maybe_links(weaver_record.value.links.clone()) 569 .maybe_pronouns(weaver_record.value.pronouns.clone())
··· 564 .maybe_description(weaver_record.value.description.clone()) 565 .maybe_avatar(avatar) 566 .maybe_banner(banner) 567 + .maybe_bluesky(weaver_record.value.bluesky) 568 + .maybe_tangled(weaver_record.value.tangled) 569 + .maybe_streamplace(weaver_record.value.streamplace) 570 .maybe_location(weaver_record.value.location.clone()) 571 .maybe_links(weaver_record.value.links.clone()) 572 .maybe_pronouns(weaver_record.value.pronouns.clone())
+13 -1
lexicons/actor/defs.json
··· 43 "type": "ref", 44 "ref": "#pinnedList" 45 }, 46 "labels": { 47 "type": "array", 48 "items": { "type": "ref", "ref": "com.atproto.label.defs#label" } ··· 112 "properties": { 113 "did": { "type": "string", "format": "did" }, 114 "signature": { 115 - "type": "bytes" 116 "description": "signed bytes of the corresponding notebook record in the author's repo" 117 } 118 }
··· 43 "type": "ref", 44 "ref": "#pinnedList" 45 }, 46 + "bluesky": { 47 + "type": "boolean", 48 + "description": "Include link to this account on Bluesky." 49 + }, 50 + "tangled": { 51 + "type": "boolean", 52 + "description": "Include link to this account on Tangled." 53 + }, 54 + "streamplace": { 55 + "type": "boolean", 56 + "description": "Include link to this account on stream.place." 57 + }, 58 "labels": { 59 "type": "array", 60 "items": { "type": "ref", "ref": "com.atproto.label.defs#label" } ··· 124 "properties": { 125 "did": { "type": "string", "format": "did" }, 126 "signature": { 127 + "type": "bytes", 128 "description": "signed bytes of the corresponding notebook record in the author's repo" 129 } 130 }