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