···44// Any manual changes will be overwritten on the next regeneration.
5566pub mod actor;
77+pub mod collab;
78pub mod edit;
89pub mod embed;
1010+pub mod graph;
911pub mod notebook;
1012pub mod publish;
+63
crates/weaver-api/src/sh_weaver/collab.rs
···11+// @generated by jacquard-lexicon. DO NOT EDIT.
22+//
33+// Lexicon: sh.weaver.collab.defs
44+//
55+// This file was automatically generated from Lexicon schemas.
66+// Any manual changes will be overwritten on the next regeneration.
77+88+pub mod accept;
99+pub mod invite;
1010+1111+/// Collaboration scoped to a chapter.
1212+#[derive(
1313+ serde::Serialize,
1414+ serde::Deserialize,
1515+ Debug,
1616+ Clone,
1717+ PartialEq,
1818+ Eq,
1919+ Hash,
2020+ jacquard_derive::IntoStatic
2121+)]
2222+pub struct Chapter;
2323+impl std::fmt::Display for Chapter {
2424+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2525+ write!(f, "chapter")
2626+ }
2727+}
2828+2929+/// Collaboration scoped to a single entry.
3030+#[derive(
3131+ serde::Serialize,
3232+ serde::Deserialize,
3333+ Debug,
3434+ Clone,
3535+ PartialEq,
3636+ Eq,
3737+ Hash,
3838+ jacquard_derive::IntoStatic
3939+)]
4040+pub struct Entry;
4141+impl std::fmt::Display for Entry {
4242+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4343+ write!(f, "entry")
4444+ }
4545+}
4646+4747+/// Collaboration scoped to an entire notebook.
4848+#[derive(
4949+ serde::Serialize,
5050+ serde::Deserialize,
5151+ Debug,
5252+ Clone,
5353+ PartialEq,
5454+ Eq,
5555+ Hash,
5656+ jacquard_derive::IntoStatic
5757+)]
5858+pub struct Notebook;
5959+impl std::fmt::Display for Notebook {
6060+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
6161+ write!(f, "notebook")
6262+ }
6363+}
+376
crates/weaver-api/src/sh_weaver/collab/accept.rs
···11+// @generated by jacquard-lexicon. DO NOT EDIT.
22+//
33+// Lexicon: sh.weaver.collab.accept
44+//
55+// This file was automatically generated from Lexicon schemas.
66+// Any manual changes will be overwritten on the next regeneration.
77+88+/// Acceptance of a collaboration invite. Completes the two-way agreement.
99+#[jacquard_derive::lexicon]
1010+#[derive(
1111+ serde::Serialize,
1212+ serde::Deserialize,
1313+ Debug,
1414+ Clone,
1515+ PartialEq,
1616+ Eq,
1717+ jacquard_derive::IntoStatic
1818+)]
1919+#[serde(rename_all = "camelCase")]
2020+pub struct Accept<'a> {
2121+ pub created_at: jacquard_common::types::string::Datetime,
2222+ /// Reference to the invite record being accepted.
2323+ #[serde(borrow)]
2424+ pub invite: crate::com_atproto::repo::strong_ref::StrongRef<'a>,
2525+ /// URI of the resource (denormalized for easier querying).
2626+ #[serde(borrow)]
2727+ pub resource: jacquard_common::types::string::AtUri<'a>,
2828+}
2929+3030+pub mod accept_state {
3131+3232+ pub use crate::builder_types::{Set, Unset, IsSet, IsUnset};
3333+ #[allow(unused)]
3434+ use ::core::marker::PhantomData;
3535+ mod sealed {
3636+ pub trait Sealed {}
3737+ }
3838+ /// State trait tracking which required fields have been set
3939+ pub trait State: sealed::Sealed {
4040+ type Invite;
4141+ type Resource;
4242+ type CreatedAt;
4343+ }
4444+ /// Empty state - all required fields are unset
4545+ pub struct Empty(());
4646+ impl sealed::Sealed for Empty {}
4747+ impl State for Empty {
4848+ type Invite = Unset;
4949+ type Resource = Unset;
5050+ type CreatedAt = Unset;
5151+ }
5252+ ///State transition - sets the `invite` field to Set
5353+ pub struct SetInvite<S: State = Empty>(PhantomData<fn() -> S>);
5454+ impl<S: State> sealed::Sealed for SetInvite<S> {}
5555+ impl<S: State> State for SetInvite<S> {
5656+ type Invite = Set<members::invite>;
5757+ type Resource = S::Resource;
5858+ type CreatedAt = S::CreatedAt;
5959+ }
6060+ ///State transition - sets the `resource` field to Set
6161+ pub struct SetResource<S: State = Empty>(PhantomData<fn() -> S>);
6262+ impl<S: State> sealed::Sealed for SetResource<S> {}
6363+ impl<S: State> State for SetResource<S> {
6464+ type Invite = S::Invite;
6565+ type Resource = Set<members::resource>;
6666+ type CreatedAt = S::CreatedAt;
6767+ }
6868+ ///State transition - sets the `created_at` field to Set
6969+ pub struct SetCreatedAt<S: State = Empty>(PhantomData<fn() -> S>);
7070+ impl<S: State> sealed::Sealed for SetCreatedAt<S> {}
7171+ impl<S: State> State for SetCreatedAt<S> {
7272+ type Invite = S::Invite;
7373+ type Resource = S::Resource;
7474+ type CreatedAt = Set<members::created_at>;
7575+ }
7676+ /// Marker types for field names
7777+ #[allow(non_camel_case_types)]
7878+ pub mod members {
7979+ ///Marker type for the `invite` field
8080+ pub struct invite(());
8181+ ///Marker type for the `resource` field
8282+ pub struct resource(());
8383+ ///Marker type for the `created_at` field
8484+ pub struct created_at(());
8585+ }
8686+}
8787+8888+/// Builder for constructing an instance of this type
8989+pub struct AcceptBuilder<'a, S: accept_state::State> {
9090+ _phantom_state: ::core::marker::PhantomData<fn() -> S>,
9191+ __unsafe_private_named: (
9292+ ::core::option::Option<jacquard_common::types::string::Datetime>,
9393+ ::core::option::Option<crate::com_atproto::repo::strong_ref::StrongRef<'a>>,
9494+ ::core::option::Option<jacquard_common::types::string::AtUri<'a>>,
9595+ ),
9696+ _phantom: ::core::marker::PhantomData<&'a ()>,
9797+}
9898+9999+impl<'a> Accept<'a> {
100100+ /// Create a new builder for this type
101101+ pub fn new() -> AcceptBuilder<'a, accept_state::Empty> {
102102+ AcceptBuilder::new()
103103+ }
104104+}
105105+106106+impl<'a> AcceptBuilder<'a, accept_state::Empty> {
107107+ /// Create a new builder with all fields unset
108108+ pub fn new() -> Self {
109109+ AcceptBuilder {
110110+ _phantom_state: ::core::marker::PhantomData,
111111+ __unsafe_private_named: (None, None, None),
112112+ _phantom: ::core::marker::PhantomData,
113113+ }
114114+ }
115115+}
116116+117117+impl<'a, S> AcceptBuilder<'a, S>
118118+where
119119+ S: accept_state::State,
120120+ S::CreatedAt: accept_state::IsUnset,
121121+{
122122+ /// Set the `createdAt` field (required)
123123+ pub fn created_at(
124124+ mut self,
125125+ value: impl Into<jacquard_common::types::string::Datetime>,
126126+ ) -> AcceptBuilder<'a, accept_state::SetCreatedAt<S>> {
127127+ self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into());
128128+ AcceptBuilder {
129129+ _phantom_state: ::core::marker::PhantomData,
130130+ __unsafe_private_named: self.__unsafe_private_named,
131131+ _phantom: ::core::marker::PhantomData,
132132+ }
133133+ }
134134+}
135135+136136+impl<'a, S> AcceptBuilder<'a, S>
137137+where
138138+ S: accept_state::State,
139139+ S::Invite: accept_state::IsUnset,
140140+{
141141+ /// Set the `invite` field (required)
142142+ pub fn invite(
143143+ mut self,
144144+ value: impl Into<crate::com_atproto::repo::strong_ref::StrongRef<'a>>,
145145+ ) -> AcceptBuilder<'a, accept_state::SetInvite<S>> {
146146+ self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into());
147147+ AcceptBuilder {
148148+ _phantom_state: ::core::marker::PhantomData,
149149+ __unsafe_private_named: self.__unsafe_private_named,
150150+ _phantom: ::core::marker::PhantomData,
151151+ }
152152+ }
153153+}
154154+155155+impl<'a, S> AcceptBuilder<'a, S>
156156+where
157157+ S: accept_state::State,
158158+ S::Resource: accept_state::IsUnset,
159159+{
160160+ /// Set the `resource` field (required)
161161+ pub fn resource(
162162+ mut self,
163163+ value: impl Into<jacquard_common::types::string::AtUri<'a>>,
164164+ ) -> AcceptBuilder<'a, accept_state::SetResource<S>> {
165165+ self.__unsafe_private_named.2 = ::core::option::Option::Some(value.into());
166166+ AcceptBuilder {
167167+ _phantom_state: ::core::marker::PhantomData,
168168+ __unsafe_private_named: self.__unsafe_private_named,
169169+ _phantom: ::core::marker::PhantomData,
170170+ }
171171+ }
172172+}
173173+174174+impl<'a, S> AcceptBuilder<'a, S>
175175+where
176176+ S: accept_state::State,
177177+ S::Invite: accept_state::IsSet,
178178+ S::Resource: accept_state::IsSet,
179179+ S::CreatedAt: accept_state::IsSet,
180180+{
181181+ /// Build the final struct
182182+ pub fn build(self) -> Accept<'a> {
183183+ Accept {
184184+ created_at: self.__unsafe_private_named.0.unwrap(),
185185+ invite: self.__unsafe_private_named.1.unwrap(),
186186+ resource: self.__unsafe_private_named.2.unwrap(),
187187+ extra_data: Default::default(),
188188+ }
189189+ }
190190+ /// Build the final struct with custom extra_data
191191+ pub fn build_with_data(
192192+ self,
193193+ extra_data: std::collections::BTreeMap<
194194+ jacquard_common::smol_str::SmolStr,
195195+ jacquard_common::types::value::Data<'a>,
196196+ >,
197197+ ) -> Accept<'a> {
198198+ Accept {
199199+ created_at: self.__unsafe_private_named.0.unwrap(),
200200+ invite: self.__unsafe_private_named.1.unwrap(),
201201+ resource: self.__unsafe_private_named.2.unwrap(),
202202+ extra_data: Some(extra_data),
203203+ }
204204+ }
205205+}
206206+207207+impl<'a> Accept<'a> {
208208+ pub fn uri(
209209+ uri: impl Into<jacquard_common::CowStr<'a>>,
210210+ ) -> Result<
211211+ jacquard_common::types::uri::RecordUri<'a, AcceptRecord>,
212212+ jacquard_common::types::uri::UriError,
213213+ > {
214214+ jacquard_common::types::uri::RecordUri::try_from_uri(
215215+ jacquard_common::types::string::AtUri::new_cow(uri.into())?,
216216+ )
217217+ }
218218+}
219219+220220+/// Typed wrapper for GetRecord response with this collection's record type.
221221+#[derive(
222222+ serde::Serialize,
223223+ serde::Deserialize,
224224+ Debug,
225225+ Clone,
226226+ PartialEq,
227227+ Eq,
228228+ jacquard_derive::IntoStatic
229229+)]
230230+#[serde(rename_all = "camelCase")]
231231+pub struct AcceptGetRecordOutput<'a> {
232232+ #[serde(skip_serializing_if = "std::option::Option::is_none")]
233233+ #[serde(borrow)]
234234+ pub cid: std::option::Option<jacquard_common::types::string::Cid<'a>>,
235235+ #[serde(borrow)]
236236+ pub uri: jacquard_common::types::string::AtUri<'a>,
237237+ #[serde(borrow)]
238238+ pub value: Accept<'a>,
239239+}
240240+241241+impl From<AcceptGetRecordOutput<'_>> for Accept<'_> {
242242+ fn from(output: AcceptGetRecordOutput<'_>) -> Self {
243243+ use jacquard_common::IntoStatic;
244244+ output.value.into_static()
245245+ }
246246+}
247247+248248+impl jacquard_common::types::collection::Collection for Accept<'_> {
249249+ const NSID: &'static str = "sh.weaver.collab.accept";
250250+ type Record = AcceptRecord;
251251+}
252252+253253+/// Marker type for deserializing records from this collection.
254254+#[derive(Debug, serde::Serialize, serde::Deserialize)]
255255+pub struct AcceptRecord;
256256+impl jacquard_common::xrpc::XrpcResp for AcceptRecord {
257257+ const NSID: &'static str = "sh.weaver.collab.accept";
258258+ const ENCODING: &'static str = "application/json";
259259+ type Output<'de> = AcceptGetRecordOutput<'de>;
260260+ type Err<'de> = jacquard_common::types::collection::RecordError<'de>;
261261+}
262262+263263+impl jacquard_common::types::collection::Collection for AcceptRecord {
264264+ const NSID: &'static str = "sh.weaver.collab.accept";
265265+ type Record = AcceptRecord;
266266+}
267267+268268+impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Accept<'a> {
269269+ fn nsid() -> &'static str {
270270+ "sh.weaver.collab.accept"
271271+ }
272272+ fn def_name() -> &'static str {
273273+ "main"
274274+ }
275275+ fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
276276+ lexicon_doc_sh_weaver_collab_accept()
277277+ }
278278+ fn validate(
279279+ &self,
280280+ ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> {
281281+ Ok(())
282282+ }
283283+}
284284+285285+fn lexicon_doc_sh_weaver_collab_accept() -> ::jacquard_lexicon::lexicon::LexiconDoc<
286286+ 'static,
287287+> {
288288+ ::jacquard_lexicon::lexicon::LexiconDoc {
289289+ lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1,
290290+ id: ::jacquard_common::CowStr::new_static("sh.weaver.collab.accept"),
291291+ revision: None,
292292+ description: None,
293293+ defs: {
294294+ let mut map = ::std::collections::BTreeMap::new();
295295+ map.insert(
296296+ ::jacquard_common::smol_str::SmolStr::new_static("main"),
297297+ ::jacquard_lexicon::lexicon::LexUserType::Record(::jacquard_lexicon::lexicon::LexRecord {
298298+ description: Some(
299299+ ::jacquard_common::CowStr::new_static(
300300+ "Acceptance of a collaboration invite. Completes the two-way agreement.",
301301+ ),
302302+ ),
303303+ key: Some(::jacquard_common::CowStr::new_static("tid")),
304304+ record: ::jacquard_lexicon::lexicon::LexRecordRecord::Object(::jacquard_lexicon::lexicon::LexObject {
305305+ description: None,
306306+ required: Some(
307307+ vec![
308308+ ::jacquard_common::smol_str::SmolStr::new_static("invite"),
309309+ ::jacquard_common::smol_str::SmolStr::new_static("resource"),
310310+ ::jacquard_common::smol_str::SmolStr::new_static("createdAt")
311311+ ],
312312+ ),
313313+ nullable: None,
314314+ properties: {
315315+ #[allow(unused_mut)]
316316+ let mut map = ::std::collections::BTreeMap::new();
317317+ map.insert(
318318+ ::jacquard_common::smol_str::SmolStr::new_static(
319319+ "createdAt",
320320+ ),
321321+ ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
322322+ description: None,
323323+ format: Some(
324324+ ::jacquard_lexicon::lexicon::LexStringFormat::Datetime,
325325+ ),
326326+ default: None,
327327+ min_length: None,
328328+ max_length: None,
329329+ min_graphemes: None,
330330+ max_graphemes: None,
331331+ r#enum: None,
332332+ r#const: None,
333333+ known_values: None,
334334+ }),
335335+ );
336336+ map.insert(
337337+ ::jacquard_common::smol_str::SmolStr::new_static("invite"),
338338+ ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef {
339339+ description: None,
340340+ r#ref: ::jacquard_common::CowStr::new_static(
341341+ "com.atproto.repo.strongRef",
342342+ ),
343343+ }),
344344+ );
345345+ map.insert(
346346+ ::jacquard_common::smol_str::SmolStr::new_static(
347347+ "resource",
348348+ ),
349349+ ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
350350+ description: Some(
351351+ ::jacquard_common::CowStr::new_static(
352352+ "URI of the resource (denormalized for easier querying).",
353353+ ),
354354+ ),
355355+ format: Some(
356356+ ::jacquard_lexicon::lexicon::LexStringFormat::AtUri,
357357+ ),
358358+ default: None,
359359+ min_length: None,
360360+ max_length: None,
361361+ min_graphemes: None,
362362+ max_graphemes: None,
363363+ r#enum: None,
364364+ r#const: None,
365365+ known_values: None,
366366+ }),
367367+ );
368368+ map
369369+ },
370370+ }),
371371+ }),
372372+ );
373373+ map
374374+ },
375375+ }
376376+}
+634
crates/weaver-api/src/sh_weaver/collab/invite.rs
···11+// @generated by jacquard-lexicon. DO NOT EDIT.
22+//
33+// Lexicon: sh.weaver.collab.invite
44+//
55+// This file was automatically generated from Lexicon schemas.
66+// Any manual changes will be overwritten on the next regeneration.
77+88+/// The scope/type of collaboration.
99+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1010+pub enum CollabScope<'a> {
1111+ ShWeaverCollabDefsNotebook,
1212+ ShWeaverCollabDefsEntry,
1313+ ShWeaverCollabDefsChapter,
1414+ Other(jacquard_common::CowStr<'a>),
1515+}
1616+1717+impl<'a> CollabScope<'a> {
1818+ pub fn as_str(&self) -> &str {
1919+ match self {
2020+ Self::ShWeaverCollabDefsNotebook => "sh.weaver.collab.defs#notebook",
2121+ Self::ShWeaverCollabDefsEntry => "sh.weaver.collab.defs#entry",
2222+ Self::ShWeaverCollabDefsChapter => "sh.weaver.collab.defs#chapter",
2323+ Self::Other(s) => s.as_ref(),
2424+ }
2525+ }
2626+}
2727+2828+impl<'a> From<&'a str> for CollabScope<'a> {
2929+ fn from(s: &'a str) -> Self {
3030+ match s {
3131+ "sh.weaver.collab.defs#notebook" => Self::ShWeaverCollabDefsNotebook,
3232+ "sh.weaver.collab.defs#entry" => Self::ShWeaverCollabDefsEntry,
3333+ "sh.weaver.collab.defs#chapter" => Self::ShWeaverCollabDefsChapter,
3434+ _ => Self::Other(jacquard_common::CowStr::from(s)),
3535+ }
3636+ }
3737+}
3838+3939+impl<'a> From<String> for CollabScope<'a> {
4040+ fn from(s: String) -> Self {
4141+ match s.as_str() {
4242+ "sh.weaver.collab.defs#notebook" => Self::ShWeaverCollabDefsNotebook,
4343+ "sh.weaver.collab.defs#entry" => Self::ShWeaverCollabDefsEntry,
4444+ "sh.weaver.collab.defs#chapter" => Self::ShWeaverCollabDefsChapter,
4545+ _ => Self::Other(jacquard_common::CowStr::from(s)),
4646+ }
4747+ }
4848+}
4949+5050+impl<'a> AsRef<str> for CollabScope<'a> {
5151+ fn as_ref(&self) -> &str {
5252+ self.as_str()
5353+ }
5454+}
5555+5656+impl<'a> serde::Serialize for CollabScope<'a> {
5757+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
5858+ where
5959+ S: serde::Serializer,
6060+ {
6161+ serializer.serialize_str(self.as_str())
6262+ }
6363+}
6464+6565+impl<'de, 'a> serde::Deserialize<'de> for CollabScope<'a>
6666+where
6767+ 'de: 'a,
6868+{
6969+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
7070+ where
7171+ D: serde::Deserializer<'de>,
7272+ {
7373+ let s = <&'de str>::deserialize(deserializer)?;
7474+ Ok(Self::from(s))
7575+ }
7676+}
7777+7878+impl jacquard_common::IntoStatic for CollabScope<'_> {
7979+ type Output = CollabScope<'static>;
8080+ fn into_static(self) -> Self::Output {
8181+ match self {
8282+ CollabScope::ShWeaverCollabDefsNotebook => {
8383+ CollabScope::ShWeaverCollabDefsNotebook
8484+ }
8585+ CollabScope::ShWeaverCollabDefsEntry => CollabScope::ShWeaverCollabDefsEntry,
8686+ CollabScope::ShWeaverCollabDefsChapter => {
8787+ CollabScope::ShWeaverCollabDefsChapter
8888+ }
8989+ CollabScope::Other(v) => CollabScope::Other(v.into_static()),
9090+ }
9191+ }
9292+}
9393+9494+/// Invitation to collaborate on a resource (notebook, entry, chapter, etc.). Creates half of a two-way agreement.
9595+#[jacquard_derive::lexicon]
9696+#[derive(
9797+ serde::Serialize,
9898+ serde::Deserialize,
9999+ Debug,
100100+ Clone,
101101+ PartialEq,
102102+ Eq,
103103+ jacquard_derive::IntoStatic
104104+)]
105105+#[serde(rename_all = "camelCase")]
106106+pub struct Invite<'a> {
107107+ pub created_at: jacquard_common::types::string::Datetime,
108108+ /// Optional expiration for the invite.
109109+ #[serde(skip_serializing_if = "std::option::Option::is_none")]
110110+ pub expires_at: std::option::Option<jacquard_common::types::string::Datetime>,
111111+ /// DID of the user being invited.
112112+ #[serde(borrow)]
113113+ pub invitee: jacquard_common::types::string::Did<'a>,
114114+ /// Optional message to the invitee.
115115+ #[serde(skip_serializing_if = "std::option::Option::is_none")]
116116+ #[serde(borrow)]
117117+ pub message: std::option::Option<jacquard_common::CowStr<'a>>,
118118+ /// The resource to collaborate on (notebook, entry, chapter, etc.).
119119+ #[serde(borrow)]
120120+ pub resource: crate::com_atproto::repo::strong_ref::StrongRef<'a>,
121121+ /// Optional explicit scope type. If omitted, inferred from resource lexicon.
122122+ #[serde(skip_serializing_if = "std::option::Option::is_none")]
123123+ #[serde(borrow)]
124124+ pub scope: std::option::Option<crate::sh_weaver::collab::invite::CollabScope<'a>>,
125125+}
126126+127127+pub mod invite_state {
128128+129129+ pub use crate::builder_types::{Set, Unset, IsSet, IsUnset};
130130+ #[allow(unused)]
131131+ use ::core::marker::PhantomData;
132132+ mod sealed {
133133+ pub trait Sealed {}
134134+ }
135135+ /// State trait tracking which required fields have been set
136136+ pub trait State: sealed::Sealed {
137137+ type Resource;
138138+ type Invitee;
139139+ type CreatedAt;
140140+ }
141141+ /// Empty state - all required fields are unset
142142+ pub struct Empty(());
143143+ impl sealed::Sealed for Empty {}
144144+ impl State for Empty {
145145+ type Resource = Unset;
146146+ type Invitee = Unset;
147147+ type CreatedAt = Unset;
148148+ }
149149+ ///State transition - sets the `resource` field to Set
150150+ pub struct SetResource<S: State = Empty>(PhantomData<fn() -> S>);
151151+ impl<S: State> sealed::Sealed for SetResource<S> {}
152152+ impl<S: State> State for SetResource<S> {
153153+ type Resource = Set<members::resource>;
154154+ type Invitee = S::Invitee;
155155+ type CreatedAt = S::CreatedAt;
156156+ }
157157+ ///State transition - sets the `invitee` field to Set
158158+ pub struct SetInvitee<S: State = Empty>(PhantomData<fn() -> S>);
159159+ impl<S: State> sealed::Sealed for SetInvitee<S> {}
160160+ impl<S: State> State for SetInvitee<S> {
161161+ type Resource = S::Resource;
162162+ type Invitee = Set<members::invitee>;
163163+ type CreatedAt = S::CreatedAt;
164164+ }
165165+ ///State transition - sets the `created_at` field to Set
166166+ pub struct SetCreatedAt<S: State = Empty>(PhantomData<fn() -> S>);
167167+ impl<S: State> sealed::Sealed for SetCreatedAt<S> {}
168168+ impl<S: State> State for SetCreatedAt<S> {
169169+ type Resource = S::Resource;
170170+ type Invitee = S::Invitee;
171171+ type CreatedAt = Set<members::created_at>;
172172+ }
173173+ /// Marker types for field names
174174+ #[allow(non_camel_case_types)]
175175+ pub mod members {
176176+ ///Marker type for the `resource` field
177177+ pub struct resource(());
178178+ ///Marker type for the `invitee` field
179179+ pub struct invitee(());
180180+ ///Marker type for the `created_at` field
181181+ pub struct created_at(());
182182+ }
183183+}
184184+185185+/// Builder for constructing an instance of this type
186186+pub struct InviteBuilder<'a, S: invite_state::State> {
187187+ _phantom_state: ::core::marker::PhantomData<fn() -> S>,
188188+ __unsafe_private_named: (
189189+ ::core::option::Option<jacquard_common::types::string::Datetime>,
190190+ ::core::option::Option<jacquard_common::types::string::Datetime>,
191191+ ::core::option::Option<jacquard_common::types::string::Did<'a>>,
192192+ ::core::option::Option<jacquard_common::CowStr<'a>>,
193193+ ::core::option::Option<crate::com_atproto::repo::strong_ref::StrongRef<'a>>,
194194+ ::core::option::Option<crate::sh_weaver::collab::invite::CollabScope<'a>>,
195195+ ),
196196+ _phantom: ::core::marker::PhantomData<&'a ()>,
197197+}
198198+199199+impl<'a> Invite<'a> {
200200+ /// Create a new builder for this type
201201+ pub fn new() -> InviteBuilder<'a, invite_state::Empty> {
202202+ InviteBuilder::new()
203203+ }
204204+}
205205+206206+impl<'a> InviteBuilder<'a, invite_state::Empty> {
207207+ /// Create a new builder with all fields unset
208208+ pub fn new() -> Self {
209209+ InviteBuilder {
210210+ _phantom_state: ::core::marker::PhantomData,
211211+ __unsafe_private_named: (None, None, None, None, None, None),
212212+ _phantom: ::core::marker::PhantomData,
213213+ }
214214+ }
215215+}
216216+217217+impl<'a, S> InviteBuilder<'a, S>
218218+where
219219+ S: invite_state::State,
220220+ S::CreatedAt: invite_state::IsUnset,
221221+{
222222+ /// Set the `createdAt` field (required)
223223+ pub fn created_at(
224224+ mut self,
225225+ value: impl Into<jacquard_common::types::string::Datetime>,
226226+ ) -> InviteBuilder<'a, invite_state::SetCreatedAt<S>> {
227227+ self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into());
228228+ InviteBuilder {
229229+ _phantom_state: ::core::marker::PhantomData,
230230+ __unsafe_private_named: self.__unsafe_private_named,
231231+ _phantom: ::core::marker::PhantomData,
232232+ }
233233+ }
234234+}
235235+236236+impl<'a, S: invite_state::State> InviteBuilder<'a, S> {
237237+ /// Set the `expiresAt` field (optional)
238238+ pub fn expires_at(
239239+ mut self,
240240+ value: impl Into<Option<jacquard_common::types::string::Datetime>>,
241241+ ) -> Self {
242242+ self.__unsafe_private_named.1 = value.into();
243243+ self
244244+ }
245245+ /// Set the `expiresAt` field to an Option value (optional)
246246+ pub fn maybe_expires_at(
247247+ mut self,
248248+ value: Option<jacquard_common::types::string::Datetime>,
249249+ ) -> Self {
250250+ self.__unsafe_private_named.1 = value;
251251+ self
252252+ }
253253+}
254254+255255+impl<'a, S> InviteBuilder<'a, S>
256256+where
257257+ S: invite_state::State,
258258+ S::Invitee: invite_state::IsUnset,
259259+{
260260+ /// Set the `invitee` field (required)
261261+ pub fn invitee(
262262+ mut self,
263263+ value: impl Into<jacquard_common::types::string::Did<'a>>,
264264+ ) -> InviteBuilder<'a, invite_state::SetInvitee<S>> {
265265+ self.__unsafe_private_named.2 = ::core::option::Option::Some(value.into());
266266+ InviteBuilder {
267267+ _phantom_state: ::core::marker::PhantomData,
268268+ __unsafe_private_named: self.__unsafe_private_named,
269269+ _phantom: ::core::marker::PhantomData,
270270+ }
271271+ }
272272+}
273273+274274+impl<'a, S: invite_state::State> InviteBuilder<'a, S> {
275275+ /// Set the `message` field (optional)
276276+ pub fn message(
277277+ mut self,
278278+ value: impl Into<Option<jacquard_common::CowStr<'a>>>,
279279+ ) -> Self {
280280+ self.__unsafe_private_named.3 = value.into();
281281+ self
282282+ }
283283+ /// Set the `message` field to an Option value (optional)
284284+ pub fn maybe_message(mut self, value: Option<jacquard_common::CowStr<'a>>) -> Self {
285285+ self.__unsafe_private_named.3 = value;
286286+ self
287287+ }
288288+}
289289+290290+impl<'a, S> InviteBuilder<'a, S>
291291+where
292292+ S: invite_state::State,
293293+ S::Resource: invite_state::IsUnset,
294294+{
295295+ /// Set the `resource` field (required)
296296+ pub fn resource(
297297+ mut self,
298298+ value: impl Into<crate::com_atproto::repo::strong_ref::StrongRef<'a>>,
299299+ ) -> InviteBuilder<'a, invite_state::SetResource<S>> {
300300+ self.__unsafe_private_named.4 = ::core::option::Option::Some(value.into());
301301+ InviteBuilder {
302302+ _phantom_state: ::core::marker::PhantomData,
303303+ __unsafe_private_named: self.__unsafe_private_named,
304304+ _phantom: ::core::marker::PhantomData,
305305+ }
306306+ }
307307+}
308308+309309+impl<'a, S: invite_state::State> InviteBuilder<'a, S> {
310310+ /// Set the `scope` field (optional)
311311+ pub fn scope(
312312+ mut self,
313313+ value: impl Into<Option<crate::sh_weaver::collab::invite::CollabScope<'a>>>,
314314+ ) -> Self {
315315+ self.__unsafe_private_named.5 = value.into();
316316+ self
317317+ }
318318+ /// Set the `scope` field to an Option value (optional)
319319+ pub fn maybe_scope(
320320+ mut self,
321321+ value: Option<crate::sh_weaver::collab::invite::CollabScope<'a>>,
322322+ ) -> Self {
323323+ self.__unsafe_private_named.5 = value;
324324+ self
325325+ }
326326+}
327327+328328+impl<'a, S> InviteBuilder<'a, S>
329329+where
330330+ S: invite_state::State,
331331+ S::Resource: invite_state::IsSet,
332332+ S::Invitee: invite_state::IsSet,
333333+ S::CreatedAt: invite_state::IsSet,
334334+{
335335+ /// Build the final struct
336336+ pub fn build(self) -> Invite<'a> {
337337+ Invite {
338338+ created_at: self.__unsafe_private_named.0.unwrap(),
339339+ expires_at: self.__unsafe_private_named.1,
340340+ invitee: self.__unsafe_private_named.2.unwrap(),
341341+ message: self.__unsafe_private_named.3,
342342+ resource: self.__unsafe_private_named.4.unwrap(),
343343+ scope: self.__unsafe_private_named.5,
344344+ extra_data: Default::default(),
345345+ }
346346+ }
347347+ /// Build the final struct with custom extra_data
348348+ pub fn build_with_data(
349349+ self,
350350+ extra_data: std::collections::BTreeMap<
351351+ jacquard_common::smol_str::SmolStr,
352352+ jacquard_common::types::value::Data<'a>,
353353+ >,
354354+ ) -> Invite<'a> {
355355+ Invite {
356356+ created_at: self.__unsafe_private_named.0.unwrap(),
357357+ expires_at: self.__unsafe_private_named.1,
358358+ invitee: self.__unsafe_private_named.2.unwrap(),
359359+ message: self.__unsafe_private_named.3,
360360+ resource: self.__unsafe_private_named.4.unwrap(),
361361+ scope: self.__unsafe_private_named.5,
362362+ extra_data: Some(extra_data),
363363+ }
364364+ }
365365+}
366366+367367+impl<'a> Invite<'a> {
368368+ pub fn uri(
369369+ uri: impl Into<jacquard_common::CowStr<'a>>,
370370+ ) -> Result<
371371+ jacquard_common::types::uri::RecordUri<'a, InviteRecord>,
372372+ jacquard_common::types::uri::UriError,
373373+ > {
374374+ jacquard_common::types::uri::RecordUri::try_from_uri(
375375+ jacquard_common::types::string::AtUri::new_cow(uri.into())?,
376376+ )
377377+ }
378378+}
379379+380380+/// Typed wrapper for GetRecord response with this collection's record type.
381381+#[derive(
382382+ serde::Serialize,
383383+ serde::Deserialize,
384384+ Debug,
385385+ Clone,
386386+ PartialEq,
387387+ Eq,
388388+ jacquard_derive::IntoStatic
389389+)]
390390+#[serde(rename_all = "camelCase")]
391391+pub struct InviteGetRecordOutput<'a> {
392392+ #[serde(skip_serializing_if = "std::option::Option::is_none")]
393393+ #[serde(borrow)]
394394+ pub cid: std::option::Option<jacquard_common::types::string::Cid<'a>>,
395395+ #[serde(borrow)]
396396+ pub uri: jacquard_common::types::string::AtUri<'a>,
397397+ #[serde(borrow)]
398398+ pub value: Invite<'a>,
399399+}
400400+401401+impl From<InviteGetRecordOutput<'_>> for Invite<'_> {
402402+ fn from(output: InviteGetRecordOutput<'_>) -> Self {
403403+ use jacquard_common::IntoStatic;
404404+ output.value.into_static()
405405+ }
406406+}
407407+408408+impl jacquard_common::types::collection::Collection for Invite<'_> {
409409+ const NSID: &'static str = "sh.weaver.collab.invite";
410410+ type Record = InviteRecord;
411411+}
412412+413413+/// Marker type for deserializing records from this collection.
414414+#[derive(Debug, serde::Serialize, serde::Deserialize)]
415415+pub struct InviteRecord;
416416+impl jacquard_common::xrpc::XrpcResp for InviteRecord {
417417+ const NSID: &'static str = "sh.weaver.collab.invite";
418418+ const ENCODING: &'static str = "application/json";
419419+ type Output<'de> = InviteGetRecordOutput<'de>;
420420+ type Err<'de> = jacquard_common::types::collection::RecordError<'de>;
421421+}
422422+423423+impl jacquard_common::types::collection::Collection for InviteRecord {
424424+ const NSID: &'static str = "sh.weaver.collab.invite";
425425+ type Record = InviteRecord;
426426+}
427427+428428+impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Invite<'a> {
429429+ fn nsid() -> &'static str {
430430+ "sh.weaver.collab.invite"
431431+ }
432432+ fn def_name() -> &'static str {
433433+ "main"
434434+ }
435435+ fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
436436+ lexicon_doc_sh_weaver_collab_invite()
437437+ }
438438+ fn validate(
439439+ &self,
440440+ ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> {
441441+ if let Some(ref value) = self.message {
442442+ #[allow(unused_comparisons)]
443443+ if <str>::len(value.as_ref()) > 3000usize {
444444+ return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength {
445445+ path: ::jacquard_lexicon::validation::ValidationPath::from_field(
446446+ "message",
447447+ ),
448448+ max: 3000usize,
449449+ actual: <str>::len(value.as_ref()),
450450+ });
451451+ }
452452+ }
453453+ if let Some(ref value) = self.message {
454454+ {
455455+ let count = ::unicode_segmentation::UnicodeSegmentation::graphemes(
456456+ value.as_ref(),
457457+ true,
458458+ )
459459+ .count();
460460+ if count > 300usize {
461461+ return Err(::jacquard_lexicon::validation::ConstraintError::MaxGraphemes {
462462+ path: ::jacquard_lexicon::validation::ValidationPath::from_field(
463463+ "message",
464464+ ),
465465+ max: 300usize,
466466+ actual: count,
467467+ });
468468+ }
469469+ }
470470+ }
471471+ Ok(())
472472+ }
473473+}
474474+475475+fn lexicon_doc_sh_weaver_collab_invite() -> ::jacquard_lexicon::lexicon::LexiconDoc<
476476+ 'static,
477477+> {
478478+ ::jacquard_lexicon::lexicon::LexiconDoc {
479479+ lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1,
480480+ id: ::jacquard_common::CowStr::new_static("sh.weaver.collab.invite"),
481481+ revision: None,
482482+ description: None,
483483+ defs: {
484484+ let mut map = ::std::collections::BTreeMap::new();
485485+ map.insert(
486486+ ::jacquard_common::smol_str::SmolStr::new_static("collabScope"),
487487+ ::jacquard_lexicon::lexicon::LexUserType::String(::jacquard_lexicon::lexicon::LexString {
488488+ description: Some(
489489+ ::jacquard_common::CowStr::new_static(
490490+ "The scope/type of collaboration.",
491491+ ),
492492+ ),
493493+ format: None,
494494+ default: None,
495495+ min_length: None,
496496+ max_length: None,
497497+ min_graphemes: None,
498498+ max_graphemes: None,
499499+ r#enum: None,
500500+ r#const: None,
501501+ known_values: None,
502502+ }),
503503+ );
504504+ map.insert(
505505+ ::jacquard_common::smol_str::SmolStr::new_static("main"),
506506+ ::jacquard_lexicon::lexicon::LexUserType::Record(::jacquard_lexicon::lexicon::LexRecord {
507507+ description: Some(
508508+ ::jacquard_common::CowStr::new_static(
509509+ "Invitation to collaborate on a resource (notebook, entry, chapter, etc.). Creates half of a two-way agreement.",
510510+ ),
511511+ ),
512512+ key: Some(::jacquard_common::CowStr::new_static("tid")),
513513+ record: ::jacquard_lexicon::lexicon::LexRecordRecord::Object(::jacquard_lexicon::lexicon::LexObject {
514514+ description: None,
515515+ required: Some(
516516+ vec![
517517+ ::jacquard_common::smol_str::SmolStr::new_static("resource"),
518518+ ::jacquard_common::smol_str::SmolStr::new_static("invitee"),
519519+ ::jacquard_common::smol_str::SmolStr::new_static("createdAt")
520520+ ],
521521+ ),
522522+ nullable: None,
523523+ properties: {
524524+ #[allow(unused_mut)]
525525+ let mut map = ::std::collections::BTreeMap::new();
526526+ map.insert(
527527+ ::jacquard_common::smol_str::SmolStr::new_static(
528528+ "createdAt",
529529+ ),
530530+ ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
531531+ description: None,
532532+ format: Some(
533533+ ::jacquard_lexicon::lexicon::LexStringFormat::Datetime,
534534+ ),
535535+ default: None,
536536+ min_length: None,
537537+ max_length: None,
538538+ min_graphemes: None,
539539+ max_graphemes: None,
540540+ r#enum: None,
541541+ r#const: None,
542542+ known_values: None,
543543+ }),
544544+ );
545545+ map.insert(
546546+ ::jacquard_common::smol_str::SmolStr::new_static(
547547+ "expiresAt",
548548+ ),
549549+ ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
550550+ description: Some(
551551+ ::jacquard_common::CowStr::new_static(
552552+ "Optional expiration for the invite.",
553553+ ),
554554+ ),
555555+ format: Some(
556556+ ::jacquard_lexicon::lexicon::LexStringFormat::Datetime,
557557+ ),
558558+ default: None,
559559+ min_length: None,
560560+ max_length: None,
561561+ min_graphemes: None,
562562+ max_graphemes: None,
563563+ r#enum: None,
564564+ r#const: None,
565565+ known_values: None,
566566+ }),
567567+ );
568568+ map.insert(
569569+ ::jacquard_common::smol_str::SmolStr::new_static("invitee"),
570570+ ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
571571+ description: Some(
572572+ ::jacquard_common::CowStr::new_static(
573573+ "DID of the user being invited.",
574574+ ),
575575+ ),
576576+ format: Some(
577577+ ::jacquard_lexicon::lexicon::LexStringFormat::Did,
578578+ ),
579579+ default: None,
580580+ min_length: None,
581581+ max_length: None,
582582+ min_graphemes: None,
583583+ max_graphemes: None,
584584+ r#enum: None,
585585+ r#const: None,
586586+ known_values: None,
587587+ }),
588588+ );
589589+ map.insert(
590590+ ::jacquard_common::smol_str::SmolStr::new_static("message"),
591591+ ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
592592+ description: Some(
593593+ ::jacquard_common::CowStr::new_static(
594594+ "Optional message to the invitee.",
595595+ ),
596596+ ),
597597+ format: None,
598598+ default: None,
599599+ min_length: None,
600600+ max_length: Some(3000usize),
601601+ min_graphemes: None,
602602+ max_graphemes: Some(300usize),
603603+ r#enum: None,
604604+ r#const: None,
605605+ known_values: None,
606606+ }),
607607+ );
608608+ map.insert(
609609+ ::jacquard_common::smol_str::SmolStr::new_static(
610610+ "resource",
611611+ ),
612612+ ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef {
613613+ description: None,
614614+ r#ref: ::jacquard_common::CowStr::new_static(
615615+ "com.atproto.repo.strongRef",
616616+ ),
617617+ }),
618618+ );
619619+ map.insert(
620620+ ::jacquard_common::smol_str::SmolStr::new_static("scope"),
621621+ ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef {
622622+ description: None,
623623+ r#ref: ::jacquard_common::CowStr::new_static("#collabScope"),
624624+ }),
625625+ );
626626+ map
627627+ },
628628+ }),
629629+ }),
630630+ );
631631+ map
632632+ },
633633+ }
634634+}
···11+// @generated by jacquard-lexicon. DO NOT EDIT.
22+//
33+// Lexicon: sh.weaver.graph.subscribeAccept
44+//
55+// This file was automatically generated from Lexicon schemas.
66+// Any manual changes will be overwritten on the next regeneration.
77+88+/// Acceptance of a subscription request.
99+#[jacquard_derive::lexicon]
1010+#[derive(
1111+ serde::Serialize,
1212+ serde::Deserialize,
1313+ Debug,
1414+ Clone,
1515+ PartialEq,
1616+ Eq,
1717+ jacquard_derive::IntoStatic
1818+)]
1919+#[serde(rename_all = "camelCase")]
2020+pub struct SubscribeAccept<'a> {
2121+ pub created_at: jacquard_common::types::string::Datetime,
2222+ /// Reference to the subscribe record being accepted.
2323+ #[serde(borrow)]
2424+ pub subscribe: crate::com_atproto::repo::strong_ref::StrongRef<'a>,
2525+}
2626+2727+pub mod subscribe_accept_state {
2828+2929+ pub use crate::builder_types::{Set, Unset, IsSet, IsUnset};
3030+ #[allow(unused)]
3131+ use ::core::marker::PhantomData;
3232+ mod sealed {
3333+ pub trait Sealed {}
3434+ }
3535+ /// State trait tracking which required fields have been set
3636+ pub trait State: sealed::Sealed {
3737+ type Subscribe;
3838+ type CreatedAt;
3939+ }
4040+ /// Empty state - all required fields are unset
4141+ pub struct Empty(());
4242+ impl sealed::Sealed for Empty {}
4343+ impl State for Empty {
4444+ type Subscribe = Unset;
4545+ type CreatedAt = Unset;
4646+ }
4747+ ///State transition - sets the `subscribe` field to Set
4848+ pub struct SetSubscribe<S: State = Empty>(PhantomData<fn() -> S>);
4949+ impl<S: State> sealed::Sealed for SetSubscribe<S> {}
5050+ impl<S: State> State for SetSubscribe<S> {
5151+ type Subscribe = Set<members::subscribe>;
5252+ type CreatedAt = S::CreatedAt;
5353+ }
5454+ ///State transition - sets the `created_at` field to Set
5555+ pub struct SetCreatedAt<S: State = Empty>(PhantomData<fn() -> S>);
5656+ impl<S: State> sealed::Sealed for SetCreatedAt<S> {}
5757+ impl<S: State> State for SetCreatedAt<S> {
5858+ type Subscribe = S::Subscribe;
5959+ type CreatedAt = Set<members::created_at>;
6060+ }
6161+ /// Marker types for field names
6262+ #[allow(non_camel_case_types)]
6363+ pub mod members {
6464+ ///Marker type for the `subscribe` field
6565+ pub struct subscribe(());
6666+ ///Marker type for the `created_at` field
6767+ pub struct created_at(());
6868+ }
6969+}
7070+7171+/// Builder for constructing an instance of this type
7272+pub struct SubscribeAcceptBuilder<'a, S: subscribe_accept_state::State> {
7373+ _phantom_state: ::core::marker::PhantomData<fn() -> S>,
7474+ __unsafe_private_named: (
7575+ ::core::option::Option<jacquard_common::types::string::Datetime>,
7676+ ::core::option::Option<crate::com_atproto::repo::strong_ref::StrongRef<'a>>,
7777+ ),
7878+ _phantom: ::core::marker::PhantomData<&'a ()>,
7979+}
8080+8181+impl<'a> SubscribeAccept<'a> {
8282+ /// Create a new builder for this type
8383+ pub fn new() -> SubscribeAcceptBuilder<'a, subscribe_accept_state::Empty> {
8484+ SubscribeAcceptBuilder::new()
8585+ }
8686+}
8787+8888+impl<'a> SubscribeAcceptBuilder<'a, subscribe_accept_state::Empty> {
8989+ /// Create a new builder with all fields unset
9090+ pub fn new() -> Self {
9191+ SubscribeAcceptBuilder {
9292+ _phantom_state: ::core::marker::PhantomData,
9393+ __unsafe_private_named: (None, None),
9494+ _phantom: ::core::marker::PhantomData,
9595+ }
9696+ }
9797+}
9898+9999+impl<'a, S> SubscribeAcceptBuilder<'a, S>
100100+where
101101+ S: subscribe_accept_state::State,
102102+ S::CreatedAt: subscribe_accept_state::IsUnset,
103103+{
104104+ /// Set the `createdAt` field (required)
105105+ pub fn created_at(
106106+ mut self,
107107+ value: impl Into<jacquard_common::types::string::Datetime>,
108108+ ) -> SubscribeAcceptBuilder<'a, subscribe_accept_state::SetCreatedAt<S>> {
109109+ self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into());
110110+ SubscribeAcceptBuilder {
111111+ _phantom_state: ::core::marker::PhantomData,
112112+ __unsafe_private_named: self.__unsafe_private_named,
113113+ _phantom: ::core::marker::PhantomData,
114114+ }
115115+ }
116116+}
117117+118118+impl<'a, S> SubscribeAcceptBuilder<'a, S>
119119+where
120120+ S: subscribe_accept_state::State,
121121+ S::Subscribe: subscribe_accept_state::IsUnset,
122122+{
123123+ /// Set the `subscribe` field (required)
124124+ pub fn subscribe(
125125+ mut self,
126126+ value: impl Into<crate::com_atproto::repo::strong_ref::StrongRef<'a>>,
127127+ ) -> SubscribeAcceptBuilder<'a, subscribe_accept_state::SetSubscribe<S>> {
128128+ self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into());
129129+ SubscribeAcceptBuilder {
130130+ _phantom_state: ::core::marker::PhantomData,
131131+ __unsafe_private_named: self.__unsafe_private_named,
132132+ _phantom: ::core::marker::PhantomData,
133133+ }
134134+ }
135135+}
136136+137137+impl<'a, S> SubscribeAcceptBuilder<'a, S>
138138+where
139139+ S: subscribe_accept_state::State,
140140+ S::Subscribe: subscribe_accept_state::IsSet,
141141+ S::CreatedAt: subscribe_accept_state::IsSet,
142142+{
143143+ /// Build the final struct
144144+ pub fn build(self) -> SubscribeAccept<'a> {
145145+ SubscribeAccept {
146146+ created_at: self.__unsafe_private_named.0.unwrap(),
147147+ subscribe: self.__unsafe_private_named.1.unwrap(),
148148+ extra_data: Default::default(),
149149+ }
150150+ }
151151+ /// Build the final struct with custom extra_data
152152+ pub fn build_with_data(
153153+ self,
154154+ extra_data: std::collections::BTreeMap<
155155+ jacquard_common::smol_str::SmolStr,
156156+ jacquard_common::types::value::Data<'a>,
157157+ >,
158158+ ) -> SubscribeAccept<'a> {
159159+ SubscribeAccept {
160160+ created_at: self.__unsafe_private_named.0.unwrap(),
161161+ subscribe: self.__unsafe_private_named.1.unwrap(),
162162+ extra_data: Some(extra_data),
163163+ }
164164+ }
165165+}
166166+167167+impl<'a> SubscribeAccept<'a> {
168168+ pub fn uri(
169169+ uri: impl Into<jacquard_common::CowStr<'a>>,
170170+ ) -> Result<
171171+ jacquard_common::types::uri::RecordUri<'a, SubscribeAcceptRecord>,
172172+ jacquard_common::types::uri::UriError,
173173+ > {
174174+ jacquard_common::types::uri::RecordUri::try_from_uri(
175175+ jacquard_common::types::string::AtUri::new_cow(uri.into())?,
176176+ )
177177+ }
178178+}
179179+180180+/// Typed wrapper for GetRecord response with this collection's record type.
181181+#[derive(
182182+ serde::Serialize,
183183+ serde::Deserialize,
184184+ Debug,
185185+ Clone,
186186+ PartialEq,
187187+ Eq,
188188+ jacquard_derive::IntoStatic
189189+)]
190190+#[serde(rename_all = "camelCase")]
191191+pub struct SubscribeAcceptGetRecordOutput<'a> {
192192+ #[serde(skip_serializing_if = "std::option::Option::is_none")]
193193+ #[serde(borrow)]
194194+ pub cid: std::option::Option<jacquard_common::types::string::Cid<'a>>,
195195+ #[serde(borrow)]
196196+ pub uri: jacquard_common::types::string::AtUri<'a>,
197197+ #[serde(borrow)]
198198+ pub value: SubscribeAccept<'a>,
199199+}
200200+201201+impl From<SubscribeAcceptGetRecordOutput<'_>> for SubscribeAccept<'_> {
202202+ fn from(output: SubscribeAcceptGetRecordOutput<'_>) -> Self {
203203+ use jacquard_common::IntoStatic;
204204+ output.value.into_static()
205205+ }
206206+}
207207+208208+impl jacquard_common::types::collection::Collection for SubscribeAccept<'_> {
209209+ const NSID: &'static str = "sh.weaver.graph.subscribeAccept";
210210+ type Record = SubscribeAcceptRecord;
211211+}
212212+213213+/// Marker type for deserializing records from this collection.
214214+#[derive(Debug, serde::Serialize, serde::Deserialize)]
215215+pub struct SubscribeAcceptRecord;
216216+impl jacquard_common::xrpc::XrpcResp for SubscribeAcceptRecord {
217217+ const NSID: &'static str = "sh.weaver.graph.subscribeAccept";
218218+ const ENCODING: &'static str = "application/json";
219219+ type Output<'de> = SubscribeAcceptGetRecordOutput<'de>;
220220+ type Err<'de> = jacquard_common::types::collection::RecordError<'de>;
221221+}
222222+223223+impl jacquard_common::types::collection::Collection for SubscribeAcceptRecord {
224224+ const NSID: &'static str = "sh.weaver.graph.subscribeAccept";
225225+ type Record = SubscribeAcceptRecord;
226226+}
227227+228228+impl<'a> ::jacquard_lexicon::schema::LexiconSchema for SubscribeAccept<'a> {
229229+ fn nsid() -> &'static str {
230230+ "sh.weaver.graph.subscribeAccept"
231231+ }
232232+ fn def_name() -> &'static str {
233233+ "main"
234234+ }
235235+ fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
236236+ lexicon_doc_sh_weaver_graph_subscribeAccept()
237237+ }
238238+ fn validate(
239239+ &self,
240240+ ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> {
241241+ Ok(())
242242+ }
243243+}
244244+245245+fn lexicon_doc_sh_weaver_graph_subscribeAccept() -> ::jacquard_lexicon::lexicon::LexiconDoc<
246246+ 'static,
247247+> {
248248+ ::jacquard_lexicon::lexicon::LexiconDoc {
249249+ lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1,
250250+ id: ::jacquard_common::CowStr::new_static("sh.weaver.graph.subscribeAccept"),
251251+ revision: None,
252252+ description: None,
253253+ defs: {
254254+ let mut map = ::std::collections::BTreeMap::new();
255255+ map.insert(
256256+ ::jacquard_common::smol_str::SmolStr::new_static("main"),
257257+ ::jacquard_lexicon::lexicon::LexUserType::Record(::jacquard_lexicon::lexicon::LexRecord {
258258+ description: Some(
259259+ ::jacquard_common::CowStr::new_static(
260260+ "Acceptance of a subscription request.",
261261+ ),
262262+ ),
263263+ key: Some(::jacquard_common::CowStr::new_static("tid")),
264264+ record: ::jacquard_lexicon::lexicon::LexRecordRecord::Object(::jacquard_lexicon::lexicon::LexObject {
265265+ description: None,
266266+ required: Some(
267267+ vec![
268268+ ::jacquard_common::smol_str::SmolStr::new_static("subscribe"),
269269+ ::jacquard_common::smol_str::SmolStr::new_static("createdAt")
270270+ ],
271271+ ),
272272+ nullable: None,
273273+ properties: {
274274+ #[allow(unused_mut)]
275275+ let mut map = ::std::collections::BTreeMap::new();
276276+ map.insert(
277277+ ::jacquard_common::smol_str::SmolStr::new_static(
278278+ "createdAt",
279279+ ),
280280+ ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString {
281281+ description: None,
282282+ format: Some(
283283+ ::jacquard_lexicon::lexicon::LexStringFormat::Datetime,
284284+ ),
285285+ default: None,
286286+ min_length: None,
287287+ max_length: None,
288288+ min_graphemes: None,
289289+ max_graphemes: None,
290290+ r#enum: None,
291291+ r#const: None,
292292+ known_values: None,
293293+ }),
294294+ );
295295+ map.insert(
296296+ ::jacquard_common::smol_str::SmolStr::new_static(
297297+ "subscribe",
298298+ ),
299299+ ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef {
300300+ description: None,
301301+ r#ref: ::jacquard_common::CowStr::new_static(
302302+ "com.atproto.repo.strongRef",
303303+ ),
304304+ }),
305305+ );
306306+ map
307307+ },
308308+ }),
309309+ }),
310310+ );
311311+ map
312312+ },
313313+ }
314314+}
+4
crates/weaver-api/src/sh_weaver/notebook.rs
···1010pub mod chapter;
1111pub mod colour_scheme;
1212pub mod entry;
1313+pub mod get_entry;
1414+pub mod get_entry_by_title;
1515+pub mod get_notebook;
1616+pub mod get_notebook_by_title;
1317pub mod page;
1418pub mod theme;
1519
···44use jacquard::IntoStatic;
55use jacquard::cowstr::ToCowStr;
66use jacquard::identity::resolver::IdentityResolver;
77+use jacquard::smol_str::SmolStr;
78use jacquard::types::blob::BlobRef;
89use jacquard::types::ident::AtIdentifier;
910use weaver_api::sh_weaver::embed::images::Image;
···1415use crate::fetch::Fetcher;
15161617use super::actions::{
1717- execute_action, handle_keydown_with_bindings, EditorAction, Key, KeyCombo, KeybindingConfig,
1818- KeydownResult, Range,
1818+ EditorAction, Key, KeyCombo, KeybindingConfig, KeydownResult, Range, execute_action,
1919+ handle_keydown_with_bindings,
1920};
2020-use super::beforeinput::{handle_beforeinput, BeforeInputContext, BeforeInputResult, InputType};
2121+use super::beforeinput::{BeforeInputContext, BeforeInputResult, InputType, handle_beforeinput};
2122#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
2223use super::beforeinput::{get_data_from_event, get_target_range_from_event};
2324use super::document::{CompositionState, EditorDocument, LoadedDocState};
···5758/// # Props
5859/// - `initial_content`: Optional initial markdown content (for new entries)
5960/// - `entry_uri`: Optional AT-URI of an existing entry to edit
6161+/// - `target_notebook`: Optional notebook title to add the entry to when publishing
6062#[component]
6161-pub fn MarkdownEditor(initial_content: Option<String>, entry_uri: Option<String>) -> Element {
6363+pub fn MarkdownEditor(
6464+ initial_content: Option<String>,
6565+ entry_uri: Option<String>,
6666+ target_notebook: Option<SmolStr>,
6767+) -> Element {
6268 let fetcher = use_context::<Fetcher>();
63696470 // Determine draft key - use entry URI if editing existing, otherwise generate TID
6571 let draft_key = use_hook(|| {
6672 entry_uri.clone().unwrap_or_else(|| {
6767- format!("new:{}", jacquard::types::tid::Ticker::new().next(None).as_str())
7373+ format!(
7474+ "new:{}",
7575+ jacquard::types::tid::Ticker::new().next(None).as_str()
7676+ )
6877 })
6978 });
70797180 // Parse entry URI once
7281 let parsed_uri = entry_uri.as_ref().and_then(|s| {
7373- jacquard::types::string::AtUri::new(s).ok().map(|u| u.into_static())
8282+ jacquard::types::string::AtUri::new(s)
8383+ .ok()
8484+ .map(|u| u.into_static())
7485 });
75867687 // Clone draft_key for render (resource closure moves it)
···112123 entry_authority
113124 );
114125 return LoadResult::Failed(
115115- "You can only edit your own entries".to_string()
126126+ "You can only edit your own entries".to_string(),
116127 );
117128 }
118129 }
···215226/// - PDS sync with auto-save
216227/// - Keyboard shortcuts (Ctrl+B for bold, Ctrl+I for italic)
217228#[component]
218218-fn MarkdownEditorInner(
219219- draft_key: String,
220220- loaded_state: LoadedDocState,
221221-) -> Element {
229229+fn MarkdownEditorInner(draft_key: String, loaded_state: LoadedDocState) -> Element {
222230 // Context for authenticated API calls
223231 let fetcher = use_context::<Fetcher>();
224232 let auth_state = use_context::<Signal<AuthState>>();
···352360353361 // Track last saved frontiers to detect changes (peek-only, no subscriptions)
354362 let mut last_saved_frontiers: Signal<Option<loro::Frontiers>> = use_signal(|| None);
363363+364364+ // Store interval handle so it's dropped when component unmounts (prevents panic)
365365+ #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
366366+ let mut interval_holder: Signal<Option<gloo_timers::callback::Interval>> = use_signal(|| None);
355367356368 // Auto-save with periodic check (no reactive dependency to avoid loops)
357369 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
···383395 last_saved_frontiers.set(Some(current_frontiers));
384396 }
385397 });
386386- interval.forget();
398398+ // Store in signal instead of forget - interval drops when component unmounts
399399+ interval_holder.set(Some(interval));
387400 });
388401389402 // Set up beforeinput listener for all text input handling.
···466479 if let Some(window) = web_sys::window() {
467480 if let Some(doc) = window.document() {
468481 if let Some(elem) = doc.get_element_by_id(editor_id) {
469469- if let Some(html_elem) = elem.dyn_ref::<web_sys::HtmlElement>() {
482482+ if let Some(html_elem) =
483483+ elem.dyn_ref::<web_sys::HtmlElement>()
484484+ {
470485 let _ = html_elem.blur();
471486 let _ = html_elem.focus();
472487 }
···817832 if plat.android && evt.key() == Key::Enter {
818833 tracing::debug!("Android: handling Enter in keypress");
819834 evt.prevent_default();
820820-835835+821836 // Get current range
822837 let range = if let Some(sel) = *doc.selection.read() {
823838 Range::new(sel.anchor.min(sel.head), sel.anchor.max(sel.head))
824839 } else {
825840 Range::caret(doc.cursor.read().offset)
826841 };
827827-842842+828843 let action = EditorAction::InsertParagraph { range };
829844 execute_action(&mut doc, &action);
830845 }
+144-86
crates/weaver-app/src/components/entry.rs
···11#![allow(non_snake_case)]
2233+use crate::Route;
34#[cfg(feature = "server")]
45use crate::blobcache::BlobCache;
56use crate::{
77+ components::EntryActions,
68 components::avatar::{Avatar, AvatarImage},
79 data::use_handle,
810};
99-1010-use crate::Route;
1111use dioxus::prelude::*;
1212+use jacquard::IntoStatic;
1313+use jacquard::types::aturi::AtUri;
12141315const ENTRY_CSS: Asset = asset!("/assets/styling/entry.css");
1416···6668 }));
67696870 // Handle blob caching when entry data is available
6969- match &*entry.read() {
7171+ match &*entry.read_unchecked() {
7072 Some((book_entry_view, entry_record)) => {
7173 if let Some(embeds) = &entry_record.embeds {
7274 if let Some(_images) = &embeds.images {
7375 // Register blob mappings with service worker (client-side only)
7474- #[cfg(all(
7575- target_family = "wasm",
7676- target_os = "unknown",
7777- not(feature = "fullstack-server")
7878- ))]
7979- {
8080- let fetcher = fetcher.clone();
8181- let images = images.clone();
8282- spawn(async move {
8383- let _ = crate::service_worker::register_entry_blobs(
8484- &ident(),
8585- book_title().as_str(),
8686- &_images,
8787- &fetcher,
8888- )
8989- .await;
9090- });
9191- }
7676+ // #[cfg(all(
7777+ // target_family = "wasm",
7878+ // target_os = "unknown",
7979+ // not(feature = "fullstack-server")
8080+ // ))]
8181+ // {
8282+ // let fetcher = fetcher.clone();
8383+ // let images = _images.clone().into_static();
8484+ // spawn(async move {
8585+ // let images = images.clone();
8686+ // let fetcher = fetcher.clone();
8787+ // let _ = crate::service_worker::register_entry_blobs(
8888+ // &ident(),
8989+ // book_title().as_str(),
9090+ // &_images,
9191+ // &fetcher,
9292+ // )
9393+ // .await;
9494+ // });
9595+ // }
9296 }
9397 }
9498 rsx! { EntryPageView {
···143147 // Metadata header
144148 EntryMetadata {
145149 entry_view: entry_view.clone(),
146146- created_at: entry_record().created_at.clone()
150150+ created_at: entry_record().created_at.clone(),
151151+ entry_uri: entry_view.uri.clone().into_static(),
152152+ book_title: Some(book_title()),
153153+ ident: ident()
147154 }
148155149156 // Rendered markdown
···176183 ident: AtIdentifier<'static>,
177184) -> Element {
178185 use crate::Route;
186186+ use crate::auth::AuthState;
179187 use jacquard::from_data;
180188 use weaver_api::sh_weaver::notebook::entry::Entry;
181189190190+ let mut hidden = use_signal(|| false);
191191+192192+ // If removed from notebook, hide this card
193193+ if hidden() {
194194+ return rsx! {};
195195+ }
196196+197197+ let auth_state = use_context::<Signal<AuthState>>();
198198+182199 let entry_view = &entry.entry;
183200 let title = entry_view
184201 .title
···192209 .format("%B %d, %Y")
193210 .to_string();
194211212212+ // Check ownership
213213+ let is_owner = {
214214+ let current_did = auth_state.read().did.clone();
215215+ match (¤t_did, &ident) {
216216+ (Some(did), AtIdentifier::Did(ident_did)) => *did == *ident_did,
217217+ _ => false,
218218+ }
219219+ };
220220+221221+ let entry_uri = entry_view.uri.clone().into_static();
222222+195223 // Only show author if notebook has multiple authors
196224 let show_author = author_count > 1;
197225 let first_author = if show_author {
···210238211239 rsx! {
212240 div { class: "entry-card",
213213- Link {
214214- to: Route::EntryPage {
215215- ident: ident,
216216- book_title: book_title.clone(),
217217- title: title.to_string().into()
218218- },
219219- class: "entry-card-link",
220220-221221-222222-223223- div { class: "entry-card-meta",
224224- div { class: "entry-card-header",
225225-241241+ div { class: "entry-card-meta",
242242+ div { class: "entry-card-header",
243243+ Link {
244244+ to: Route::EntryPage {
245245+ ident: ident.clone(),
246246+ book_title: book_title.clone(),
247247+ title: title.to_string().into()
248248+ },
249249+ class: "entry-card-title-link",
226250 h3 { class: "entry-card-title", "{title}" }
227227- div { class: "entry-card-date",
228228- time { datetime: "{entry_view.indexed_at.as_str()}", "{formatted_date}" }
251251+ }
252252+ div { class: "entry-card-date",
253253+ time { datetime: "{entry_view.indexed_at.as_str()}", "{formatted_date}" }
254254+ }
255255+ if is_owner {
256256+ EntryActions {
257257+ entry_uri,
258258+ entry_title: title.to_string(),
259259+ in_notebook: true,
260260+ notebook_title: Some(book_title.clone()),
261261+ on_removed: Some(EventHandler::new(move |_| hidden.set(true)))
229262 }
230263 }
231231- if let Some(author) = first_author {
232232- div { class: "entry-card-author",
233233- {
234234- use weaver_api::sh_weaver::actor::ProfileDataViewInner;
264264+ }
265265+ if let Some(author) = first_author {
266266+ div { class: "entry-card-author",
267267+ {
268268+ use weaver_api::sh_weaver::actor::ProfileDataViewInner;
235269236236- match &author.record.inner {
237237- ProfileDataViewInner::ProfileView(profile) => {
238238- let display_name = profile.display_name.as_ref().map(|n| n.as_ref()).unwrap_or("Unknown");
239239- let handle = profile.handle.clone();
240240- rsx! {
241241- if let Some(ref avatar_url) = profile.avatar {
242242- Avatar {
243243- AvatarImage { src: avatar_url.as_ref() }
244244- }
270270+ match &author.record.inner {
271271+ ProfileDataViewInner::ProfileView(profile) => {
272272+ let display_name = profile.display_name.as_ref().map(|n| n.as_ref()).unwrap_or("Unknown");
273273+ let handle = profile.handle.clone();
274274+ rsx! {
275275+ if let Some(ref avatar_url) = profile.avatar {
276276+ Avatar {
277277+ AvatarImage { src: avatar_url.as_ref() }
245278 }
246246- span { class: "author-name", "{display_name}" }
247247- span { class: "meta-label", "@{handle}" }
248279 }
280280+ span { class: "author-name", "{display_name}" }
281281+ span { class: "meta-label", "@{handle}" }
249282 }
250250- ProfileDataViewInner::ProfileViewDetailed(profile) => {
251251- let display_name = profile.display_name.as_ref().map(|n| n.as_ref()).unwrap_or("Unknown");
252252- let handle = profile.handle.clone();
253253- rsx! {
254254- if let Some(ref avatar_url) = profile.avatar {
255255- Avatar {
256256- AvatarImage { src: avatar_url.as_ref() }
257257- }
283283+ }
284284+ ProfileDataViewInner::ProfileViewDetailed(profile) => {
285285+ let display_name = profile.display_name.as_ref().map(|n| n.as_ref()).unwrap_or("Unknown");
286286+ let handle = profile.handle.clone();
287287+ rsx! {
288288+ if let Some(ref avatar_url) = profile.avatar {
289289+ Avatar {
290290+ AvatarImage { src: avatar_url.as_ref() }
258291 }
259259- span { class: "author-name", "{display_name}" }
260260- span { class: "meta-label", "@{handle}" }
261292 }
293293+ span { class: "author-name", "{display_name}" }
294294+ span { class: "meta-label", "@{handle}" }
262295 }
263263- ProfileDataViewInner::TangledProfileView(profile) => {
264264- rsx! {
265265- span { class: "author-name", "@{profile.handle.as_ref()}" }
266266- }
296296+ }
297297+ ProfileDataViewInner::TangledProfileView(profile) => {
298298+ rsx! {
299299+ span { class: "author-name", "@{profile.handle.as_ref()}" }
267300 }
268268- _ => {
269269- rsx! {
270270- span { class: "author-name", "Unknown" }
271271- }
301301+ }
302302+ _ => {
303303+ rsx! {
304304+ span { class: "author-name", "Unknown" }
272305 }
273306 }
274307 }
275308 }
276309 }
277277-278278-279310 }
311311+ }
280312281281-282282-283283- if let Some(ref html) = preview_html {
284284- div { class: "entry-card-preview", dangerous_inner_html: "{html}" }
285285- }
286286- if let Some(ref tags) = entry_view.tags {
287287- if !tags.is_empty() {
288288- div { class: "entry-card-tags",
289289- for tag in tags.iter() {
290290- span { class: "entry-card-tag", "{tag}" }
291291- }
313313+ if let Some(ref html) = preview_html {
314314+ div { class: "entry-card-preview", dangerous_inner_html: "{html}" }
315315+ }
316316+ if let Some(ref tags) = entry_view.tags {
317317+ if !tags.is_empty() {
318318+ div { class: "entry-card-tags",
319319+ for tag in tags.iter() {
320320+ span { class: "entry-card-tag", "{tag}" }
292321 }
293322 }
294323 }
···299328300329/// Metadata header showing title, authors, date, tags
301330#[component]
302302-fn EntryMetadata(entry_view: EntryView<'static>, created_at: Datetime) -> Element {
331331+fn EntryMetadata(
332332+ entry_view: EntryView<'static>,
333333+ created_at: Datetime,
334334+ entry_uri: AtUri<'static>,
335335+ book_title: Option<SmolStr>,
336336+ ident: AtIdentifier<'static>,
337337+) -> Element {
338338+ let navigator = use_navigator();
339339+303340 let title = entry_view
304341 .title
305342 .as_ref()
306343 .map(|t| t.as_ref())
307344 .unwrap_or("Untitled");
308345309309- //let indexed_at_chrono = entry_view.indexed_at.as_ref();
346346+ let entry_title = title.to_string();
347347+348348+ // Navigate back to notebook when entry is removed
349349+ let nav_book_title = book_title.clone();
350350+ let nav_ident = ident.clone();
351351+ let on_removed = move |_| {
352352+ if let Some(ref title) = nav_book_title {
353353+ navigator.push(Route::NotebookIndex {
354354+ ident: nav_ident.clone(),
355355+ book_title: title.clone(),
356356+ });
357357+ }
358358+ };
310359311360 rsx! {
312361 header { class: "entry-metadata",
313313- h1 { class: "entry-title", "{title}" }
362362+ div { class: "entry-header-row",
363363+ h1 { class: "entry-title", "{title}" }
364364+ EntryActions {
365365+ entry_uri: entry_uri.clone(),
366366+ entry_title,
367367+ in_notebook: book_title.is_some(),
368368+ notebook_title: book_title.clone(),
369369+ on_removed: Some(EventHandler::new(on_removed))
370370+ }
371371+ }
314372315373 div { class: "entry-meta-info",
316374 // Authors
+371
crates/weaver-app/src/components/entry_actions.rs
···11+//! Action buttons for entries (edit, delete, remove from notebook).
22+33+use crate::Route;
44+use crate::auth::AuthState;
55+use crate::components::button::{Button, ButtonVariant};
66+use crate::components::dialog::{DialogContent, DialogDescription, DialogRoot, DialogTitle};
77+use crate::fetch::Fetcher;
88+use dioxus::prelude::*;
99+use jacquard::smol_str::SmolStr;
1010+use jacquard::types::aturi::AtUri;
1111+use jacquard::types::ident::AtIdentifier;
1212+use jacquard::IntoStatic;
1313+use weaver_api::com_atproto::repo::delete_record::DeleteRecord;
1414+use weaver_api::com_atproto::repo::put_record::PutRecord;
1515+1616+const ENTRY_ACTIONS_CSS: Asset = asset!("/assets/styling/entry-actions.css");
1717+1818+#[derive(Props, Clone, PartialEq)]
1919+pub struct EntryActionsProps {
2020+ /// The AT-URI of the entry
2121+ pub entry_uri: AtUri<'static>,
2222+ /// The entry title (for display in confirmation)
2323+ pub entry_title: String,
2424+ /// Whether this entry is in a notebook (enables "remove from notebook")
2525+ #[props(default = false)]
2626+ pub in_notebook: bool,
2727+ /// Notebook title (if in_notebook is true, used for edit route)
2828+ #[props(default)]
2929+ pub notebook_title: Option<SmolStr>,
3030+ /// Callback when entry is removed from notebook (for optimistic UI update)
3131+ #[props(default)]
3232+ pub on_removed: Option<EventHandler<()>>,
3333+}
3434+3535+/// Action buttons for an entry: edit, delete, optionally remove from notebook.
3636+#[component]
3737+pub fn EntryActions(props: EntryActionsProps) -> Element {
3838+ let auth_state = use_context::<Signal<AuthState>>();
3939+ let fetcher = use_context::<Fetcher>();
4040+ let navigator = use_navigator();
4141+4242+ let mut show_delete_confirm = use_signal(|| false);
4343+ let mut show_remove_confirm = use_signal(|| false);
4444+ let mut show_dropdown = use_signal(|| false);
4545+ let mut deleting = use_signal(|| false);
4646+ let mut removing = use_signal(|| false);
4747+ let mut error = use_signal(|| None::<String>);
4848+4949+ // Check ownership - compare auth DID with entry's authority
5050+ let current_did = auth_state.read().did.clone();
5151+ let entry_authority = props.entry_uri.authority();
5252+ let is_owner = match (¤t_did, entry_authority) {
5353+ (Some(current), AtIdentifier::Did(entry_did)) => *current == *entry_did,
5454+ _ => false,
5555+ };
5656+5757+ if !is_owner {
5858+ return rsx! {};
5959+ }
6060+6161+ // Extract rkey from URI for edit route
6262+ let rkey = match props.entry_uri.rkey() {
6363+ Some(r) => r.0.to_string(),
6464+ None => return rsx! {}, // Can't edit without rkey
6565+ };
6666+6767+ // Build edit route based on whether entry is in a notebook
6868+ let ident = props.entry_uri.authority().clone();
6969+ let edit_route = if props.in_notebook {
7070+ if let Some(ref notebook) = props.notebook_title {
7171+ Route::NotebookEntryEdit {
7272+ ident: ident.into_static(),
7373+ book_title: notebook.clone(),
7474+ rkey: rkey.clone().into(),
7575+ }
7676+ } else {
7777+ Route::StandaloneEntryEdit {
7878+ ident: ident.into_static(),
7979+ rkey: rkey.clone().into(),
8080+ }
8181+ }
8282+ } else {
8383+ Route::StandaloneEntryEdit {
8484+ ident: ident.into_static(),
8585+ rkey: rkey.clone().into(),
8686+ }
8787+ };
8888+8989+ let entry_uri_for_delete = props.entry_uri.clone();
9090+ let entry_title = props.entry_title.clone();
9191+9292+ let delete_fetcher = fetcher.clone();
9393+ let handle_delete = move |_| {
9494+ let fetcher = delete_fetcher.clone();
9595+ let uri = entry_uri_for_delete.clone();
9696+ let navigator = navigator.clone();
9797+9898+ spawn(async move {
9999+ use jacquard::prelude::*;
100100+101101+ deleting.set(true);
102102+ error.set(None);
103103+104104+ let client = fetcher.get_client();
105105+ let collection = uri.collection();
106106+ let rkey = uri.rkey();
107107+108108+ if let (Some(collection), Some(rkey)) = (collection, rkey) {
109109+ let did = match fetcher.current_did().await {
110110+ Some(d) => d,
111111+ None => {
112112+ error.set(Some("Not authenticated".to_string()));
113113+ deleting.set(false);
114114+ return;
115115+ }
116116+ };
117117+118118+ let request = DeleteRecord::new()
119119+ .repo(AtIdentifier::Did(did))
120120+ .collection(collection.clone())
121121+ .rkey(rkey.clone())
122122+ .build();
123123+124124+ match client.send(request).await {
125125+ Ok(_) => {
126126+ show_delete_confirm.set(false);
127127+ // Navigate back to home after delete
128128+ navigator.push(Route::Home {});
129129+ }
130130+ Err(e) => {
131131+ error.set(Some(format!("Delete failed: {:?}", e)));
132132+ }
133133+ }
134134+ } else {
135135+ error.set(Some("Invalid entry URI".to_string()));
136136+ }
137137+ deleting.set(false);
138138+ });
139139+ };
140140+141141+ // Handler for removing entry from notebook (keeps entry, just removes from notebook's list)
142142+ let entry_uri_for_remove = props.entry_uri.clone();
143143+ let notebook_title_for_remove = props.notebook_title.clone();
144144+ let on_removed = props.on_removed.clone();
145145+ let handle_remove_from_notebook = move |_| {
146146+ let fetcher = fetcher.clone();
147147+ let entry_uri = entry_uri_for_remove.clone();
148148+ let notebook_title = notebook_title_for_remove.clone();
149149+ let on_removed = on_removed.clone();
150150+151151+ spawn(async move {
152152+ use jacquard::{from_data, to_data, prelude::*, types::string::Nsid};
153153+ use weaver_api::sh_weaver::notebook::book::Book;
154154+155155+ let client = fetcher.get_client();
156156+157157+ removing.set(true);
158158+ error.set(None);
159159+160160+ let notebook_title = match notebook_title {
161161+ Some(t) => t,
162162+ None => {
163163+ error.set(Some("No notebook specified".to_string()));
164164+ removing.set(false);
165165+ return;
166166+ }
167167+ };
168168+169169+ let did = match fetcher.current_did().await {
170170+ Some(d) => d,
171171+ None => {
172172+ error.set(Some("Not authenticated".to_string()));
173173+ removing.set(false);
174174+ return;
175175+ }
176176+ };
177177+178178+ // Get the notebook by title
179179+ let ident = AtIdentifier::Did(did.clone());
180180+ let notebook_result = fetcher.get_notebook(ident.clone(), notebook_title.clone()).await;
181181+182182+ let (notebook_view, _) = match notebook_result {
183183+ Ok(Some(data)) => data.as_ref().clone(),
184184+ Ok(None) => {
185185+ error.set(Some("Notebook not found".to_string()));
186186+ removing.set(false);
187187+ return;
188188+ }
189189+ Err(e) => {
190190+ error.set(Some(format!("Failed to get notebook: {:?}", e)));
191191+ removing.set(false);
192192+ return;
193193+ }
194194+ };
195195+196196+ // Parse the book record to get the entry_list
197197+ let mut book: Book = match from_data(¬ebook_view.record) {
198198+ Ok(b) => b,
199199+ Err(e) => {
200200+ error.set(Some(format!("Failed to parse notebook: {:?}", e)));
201201+ removing.set(false);
202202+ return;
203203+ }
204204+ };
205205+206206+ // Filter out the entry
207207+ let entry_uri_str = entry_uri.as_str();
208208+ let original_len = book.entry_list.len();
209209+ book.entry_list.retain(|ref_| ref_.uri.as_str() != entry_uri_str);
210210+211211+ if book.entry_list.len() == original_len {
212212+ error.set(Some("Entry not found in notebook".to_string()));
213213+ removing.set(false);
214214+ return;
215215+ }
216216+217217+ // Get the notebook's rkey from its URI
218218+ let notebook_rkey = match notebook_view.uri.rkey() {
219219+ Some(r) => r,
220220+ None => {
221221+ error.set(Some("Invalid notebook URI".to_string()));
222222+ removing.set(false);
223223+ return;
224224+ }
225225+ };
226226+227227+ // Convert book to Data for the request
228228+ let book_data = match to_data(&book) {
229229+ Ok(d) => d,
230230+ Err(e) => {
231231+ error.set(Some(format!("Failed to serialize notebook: {:?}", e)));
232232+ removing.set(false);
233233+ return;
234234+ }
235235+ };
236236+237237+ // Update the notebook record
238238+ let request = PutRecord::new()
239239+ .repo(AtIdentifier::Did(did))
240240+ .collection(Nsid::new_static("sh.weaver.notebook.book").unwrap())
241241+ .rkey(notebook_rkey.clone())
242242+ .record(book_data)
243243+ .build();
244244+245245+ match client.send(request).await {
246246+ Ok(_) => {
247247+ show_remove_confirm.set(false);
248248+ // Notify parent to remove from local state
249249+ if let Some(handler) = &on_removed {
250250+ handler.call(());
251251+ }
252252+ }
253253+ Err(e) => {
254254+ error.set(Some(format!("Failed to update notebook: {:?}", e)));
255255+ }
256256+ }
257257+ removing.set(false);
258258+ });
259259+ };
260260+261261+ rsx! {
262262+ document::Link { rel: "stylesheet", href: ENTRY_ACTIONS_CSS }
263263+264264+ div { class: "entry-actions",
265265+ // Edit button (always visible for owner)
266266+ Link {
267267+ to: edit_route,
268268+ class: "entry-action-link",
269269+ Button {
270270+ variant: ButtonVariant::Ghost,
271271+ "Edit"
272272+ }
273273+ }
274274+275275+ // Dropdown for destructive actions
276276+ div { class: "entry-actions-dropdown",
277277+ Button {
278278+ variant: ButtonVariant::Ghost,
279279+ onclick: move |_| show_dropdown.toggle(),
280280+ "⋮"
281281+ }
282282+283283+ if show_dropdown() {
284284+ div { class: "dropdown-menu",
285285+ if props.in_notebook {
286286+ button {
287287+ class: "dropdown-item",
288288+ onclick: move |_| {
289289+ show_dropdown.set(false);
290290+ show_remove_confirm.set(true);
291291+ },
292292+ "Remove from notebook"
293293+ }
294294+ }
295295+ button {
296296+ class: "dropdown-item dropdown-item-danger",
297297+ onclick: move |_| {
298298+ show_dropdown.set(false);
299299+ show_delete_confirm.set(true);
300300+ },
301301+ "Delete"
302302+ }
303303+ }
304304+ }
305305+ }
306306+307307+ // Delete confirmation dialog
308308+ DialogRoot {
309309+ open: show_delete_confirm(),
310310+ on_open_change: move |open: bool| show_delete_confirm.set(open),
311311+ DialogContent {
312312+ DialogTitle { "Delete Entry?" }
313313+ DialogDescription {
314314+ "Delete \"{entry_title}\"? This removes the published entry. You can restore from drafts if needed."
315315+ }
316316+ if let Some(ref err) = error() {
317317+ div { class: "dialog-error", "{err}" }
318318+ }
319319+ div { class: "dialog-actions",
320320+ Button {
321321+ variant: ButtonVariant::Destructive,
322322+ onclick: handle_delete,
323323+ disabled: deleting(),
324324+ if deleting() { "Deleting..." } else { "Delete" }
325325+ }
326326+ Button {
327327+ variant: ButtonVariant::Ghost,
328328+ onclick: move |_| show_delete_confirm.set(false),
329329+ "Cancel"
330330+ }
331331+ }
332332+ }
333333+ }
334334+335335+ // Remove from notebook confirmation dialog
336336+ if props.in_notebook {
337337+ {
338338+ let entry_title_for_remove = entry_title.clone();
339339+ rsx! {
340340+ DialogRoot {
341341+ open: show_remove_confirm(),
342342+ on_open_change: move |open: bool| show_remove_confirm.set(open),
343343+ DialogContent {
344344+ DialogTitle { "Remove from Notebook?" }
345345+ DialogDescription {
346346+ "Remove \"{entry_title_for_remove}\" from this notebook? The entry will still exist but won't be part of this notebook."
347347+ }
348348+ if let Some(ref err) = error() {
349349+ div { class: "dialog-error", "{err}" }
350350+ }
351351+ div { class: "dialog-actions",
352352+ Button {
353353+ variant: ButtonVariant::Primary,
354354+ onclick: handle_remove_from_notebook,
355355+ disabled: removing(),
356356+ if removing() { "Removing..." } else { "Remove" }
357357+ }
358358+ Button {
359359+ variant: ButtonVariant::Ghost,
360360+ onclick: move |_| show_remove_confirm.set(false),
361361+ "Cancel"
362362+ }
363363+ }
364364+ }
365365+ }
366366+ }
367367+ }
368368+ }
369369+ }
370370+ }
371371+}
+119-40
crates/weaver-app/src/components/identity.rs
···11+use crate::auth::AuthState;
22+use crate::components::{ProfileActions, ProfileActionsMenubar};
13use crate::{Route, data, fetch};
24use dioxus::prelude::*;
35use jacquard::{smol_str::SmolStr, types::ident::AtIdentifier};
···47494850 // Main content area
4951 main { class: "repository-main",
5252+ // Mobile menubar (hidden on desktop)
5353+ ProfileActionsMenubar { ident }
5454+5055 div { class: "notebooks-list",
5156 match &*notebooks.read() {
5257 Some(notebook_list) => rsx! {
···7277 }
7378 }
7479 }
8080+8181+ // Actions sidebar (desktop only)
8282+ ProfileActions { ident }
7583 }
7684 }
7785}
···8492 use jacquard::IntoStatic;
85938694 let fetcher = use_context::<fetch::Fetcher>();
9595+ let auth_state = use_context::<Signal<AuthState>>();
87968897 let title = notebook
8998 .title
···91100 .map(|t| t.as_ref())
92101 .unwrap_or("Untitled Notebook");
93102103103+ // Check ownership for "Add Entry" link
104104+ let notebook_ident = notebook.uri.authority().clone().into_static();
105105+ let is_owner = {
106106+ let current_did = auth_state.read().did.clone();
107107+ match (¤t_did, ¬ebook_ident) {
108108+ (Some(did), AtIdentifier::Did(nb_did)) => *did == *nb_did,
109109+ _ => false,
110110+ }
111111+ };
112112+94113 // Format date
95114 let formatted_date = notebook.indexed_at.as_ref().format("%B %d, %Y").to_string();
96115···126145 class: "notebook-card-header-link",
127146128147 div { class: "notebook-card-header",
129129- h2 { class: "notebook-card-title", "{title}" }
148148+ div { class: "notebook-card-header-top",
149149+ h2 { class: "notebook-card-title", "{title}" }
150150+ if is_owner {
151151+ Link {
152152+ to: Route::NewDraft { ident: notebook_ident.clone(), notebook: Some(book_title.clone()) },
153153+ class: "notebook-add-entry",
154154+ "+ Add"
155155+ }
156156+ }
157157+ }
130158131159 div { class: "notebook-card-date",
132160 time { datetime: "{notebook.indexed_at.as_str()}", "{formatted_date}" }
···199227 let created_at = from_data::<Entry>(&entry_view.entry.record).ok()
200228 .map(|entry| entry.created_at.as_ref().format("%B %d, %Y").to_string());
201229202202- rsx! {
203203- Link {
204204- to: Route::EntryPage {
205205- ident: ident.clone(),
206206- book_title: book_title.clone(),
207207- title: entry_title.to_string().into()
208208- },
209209- class: "notebook-entry-preview-link",
230230+ let entry_uri = entry_view.entry.uri.clone().into_static();
210231211211- div { class: "notebook-entry-preview",
212212- div { class: "entry-preview-header",
232232+ rsx! {
233233+ div { class: "notebook-entry-preview",
234234+ div { class: "entry-preview-header",
235235+ Link {
236236+ to: Route::EntryPage {
237237+ ident: ident.clone(),
238238+ book_title: book_title.clone(),
239239+ title: entry_title.to_string().into()
240240+ },
241241+ class: "entry-preview-title-link",
213242 div { class: "entry-preview-title", "{entry_title}" }
214214- if let Some(ref date) = created_at {
215215- div { class: "entry-preview-date", "{date}" }
243243+ }
244244+ if let Some(ref date) = created_at {
245245+ div { class: "entry-preview-date", "{date}" }
246246+ }
247247+ if is_owner {
248248+ crate::components::EntryActions {
249249+ entry_uri,
250250+ entry_title: entry_title.to_string(),
251251+ in_notebook: true,
252252+ notebook_title: Some(book_title.clone())
216253 }
217254 }
218218- if let Some(ref html) = preview_html {
255255+ }
256256+ if let Some(ref html) = preview_html {
257257+ Link {
258258+ to: Route::EntryPage {
259259+ ident: ident.clone(),
260260+ book_title: book_title.clone(),
261261+ title: entry_title.to_string().into()
262262+ },
263263+ class: "entry-preview-content-link",
219264 div { class: "entry-preview-content", dangerous_inner_html: "{html}" }
220265 }
221266 }
···243288 let created_at = from_data::<Entry>(&first_entry.entry.record).ok()
244289 .map(|entry| entry.created_at.as_ref().format("%B %d, %Y").to_string());
245290291291+ let entry_uri = first_entry.entry.uri.clone().into_static();
292292+246293 rsx! {
247247- Link {
248248- to: Route::EntryPage {
249249- ident: ident.clone(),
250250- book_title: book_title.clone(),
251251- title: entry_title.to_string().into()
252252- },
253253- class: "notebook-entry-preview-link",
254254-255255- div { class: "notebook-entry-preview notebook-entry-preview-first",
256256- div { class: "entry-preview-header",
294294+ div { class: "notebook-entry-preview notebook-entry-preview-first",
295295+ div { class: "entry-preview-header",
296296+ Link {
297297+ to: Route::EntryPage {
298298+ ident: ident.clone(),
299299+ book_title: book_title.clone(),
300300+ title: entry_title.to_string().into()
301301+ },
302302+ class: "entry-preview-title-link",
257303 div { class: "entry-preview-title", "{entry_title}" }
258258- if let Some(ref date) = created_at {
259259- div { class: "entry-preview-date", "{date}" }
304304+ }
305305+ if let Some(ref date) = created_at {
306306+ div { class: "entry-preview-date", "{date}" }
307307+ }
308308+ if is_owner {
309309+ crate::components::EntryActions {
310310+ entry_uri,
311311+ entry_title: entry_title.to_string(),
312312+ in_notebook: true,
313313+ notebook_title: Some(book_title.clone())
260314 }
261315 }
262262- if let Some(ref html) = preview_html {
316316+ }
317317+ if let Some(ref html) = preview_html {
318318+ Link {
319319+ to: Route::EntryPage {
320320+ ident: ident.clone(),
321321+ book_title: book_title.clone(),
322322+ title: entry_title.to_string().into()
323323+ },
324324+ class: "entry-preview-content-link",
263325 div { class: "entry-preview-content", dangerous_inner_html: "{html}" }
264326 }
265327 }
···296358 let created_at = from_data::<Entry>(&last_entry.entry.record).ok()
297359 .map(|entry| entry.created_at.as_ref().format("%B %d, %Y").to_string());
298360361361+ let entry_uri = last_entry.entry.uri.clone().into_static();
362362+299363 rsx! {
300300- Link {
301301- to: Route::EntryPage {
302302- ident: ident.clone(),
303303- book_title: book_title.clone(),
304304- title: entry_title.to_string().into()
305305- },
306306- class: "notebook-entry-preview-link",
307307-308308- div { class: "notebook-entry-preview notebook-entry-preview-last",
309309- div { class: "entry-preview-header",
364364+ div { class: "notebook-entry-preview notebook-entry-preview-last",
365365+ div { class: "entry-preview-header",
366366+ Link {
367367+ to: Route::EntryPage {
368368+ ident: ident.clone(),
369369+ book_title: book_title.clone(),
370370+ title: entry_title.to_string().into()
371371+ },
372372+ class: "entry-preview-title-link",
310373 div { class: "entry-preview-title", "{entry_title}" }
311311- if let Some(ref date) = created_at {
312312- div { class: "entry-preview-date", "{date}" }
374374+ }
375375+ if let Some(ref date) = created_at {
376376+ div { class: "entry-preview-date", "{date}" }
377377+ }
378378+ if is_owner {
379379+ crate::components::EntryActions {
380380+ entry_uri,
381381+ entry_title: entry_title.to_string(),
382382+ in_notebook: true,
383383+ notebook_title: Some(book_title.clone())
313384 }
314385 }
315315- if let Some(ref html) = preview_html {
386386+ }
387387+ if let Some(ref html) = preview_html {
388388+ Link {
389389+ to: Route::EntryPage {
390390+ ident: ident.clone(),
391391+ book_title: book_title.clone(),
392392+ title: entry_title.to_string().into()
393393+ },
394394+ class: "entry-preview-content-link",
316395 div { class: "entry-preview-content", dangerous_inner_html: "{html}" }
317396 }
318397 }
+5
crates/weaver-app/src/components/mod.rs
···128128pub mod button;
129129pub mod dialog;
130130pub mod editor;
131131+pub mod entry_actions;
131132pub mod input;
133133+pub mod profile_actions;
134134+135135+pub use entry_actions::EntryActions;
136136+pub use profile_actions::{ProfileActions, ProfileActionsMenubar};
···11+//! Actions sidebar/menubar for profile page.
22+33+use crate::Route;
44+use crate::auth::AuthState;
55+use crate::components::button::{Button, ButtonVariant};
66+use dioxus::prelude::*;
77+use jacquard::types::ident::AtIdentifier;
88+99+const PROFILE_ACTIONS_CSS: Asset = asset!("/assets/styling/profile-actions.css");
1010+1111+/// Actions available on the profile page for the owner.
1212+#[component]
1313+pub fn ProfileActions(ident: ReadSignal<AtIdentifier<'static>>) -> Element {
1414+ let auth_state = use_context::<Signal<AuthState>>();
1515+1616+ // Check if viewing own profile
1717+ let is_owner = {
1818+ let current_did = auth_state.read().did.clone();
1919+ match (¤t_did, ident()) {
2020+ (Some(did), AtIdentifier::Did(ref ident_did)) => *did == *ident_did,
2121+ _ => false,
2222+ }
2323+ };
2424+2525+ if !is_owner {
2626+ return rsx! {};
2727+ }
2828+2929+ rsx! {
3030+ document::Link { rel: "stylesheet", href: PROFILE_ACTIONS_CSS }
3131+3232+ aside { class: "profile-actions",
3333+ div { class: "profile-actions-container",
3434+ div { class: "profile-actions-list",
3535+ Link {
3636+ to: Route::NewDraft { ident: ident(), notebook: None },
3737+ class: "profile-action-link",
3838+ Button {
3939+ variant: ButtonVariant::Outline,
4040+ "New Entry"
4141+ }
4242+ }
4343+4444+ // TODO: New Notebook button (disabled for now)
4545+ Button {
4646+ variant: ButtonVariant::Outline,
4747+ disabled: true,
4848+ "New Notebook"
4949+ }
5050+5151+ Link {
5252+ to: Route::DraftsList { ident: ident() },
5353+ class: "profile-action-link",
5454+ Button {
5555+ variant: ButtonVariant::Ghost,
5656+ "Drafts"
5757+ }
5858+ }
5959+ }
6060+ }
6161+ }
6262+ }
6363+}
6464+6565+/// Mobile-friendly menubar version of profile actions.
6666+#[component]
6767+pub fn ProfileActionsMenubar(ident: ReadSignal<AtIdentifier<'static>>) -> Element {
6868+ let auth_state = use_context::<Signal<AuthState>>();
6969+7070+ let is_owner = {
7171+ let current_did = auth_state.read().did.clone();
7272+ match (¤t_did, ident()) {
7373+ (Some(did), AtIdentifier::Did(ref ident_did)) => *did == *ident_did,
7474+ _ => false,
7575+ }
7676+ };
7777+7878+ if !is_owner {
7979+ return rsx! {};
8080+ }
8181+8282+ rsx! {
8383+ div { class: "profile-actions-menubar",
8484+ Link {
8585+ to: Route::NewDraft { ident: ident(), notebook: None },
8686+ Button {
8787+ variant: ButtonVariant::Primary,
8888+ "New Entry"
8989+ }
9090+ }
9191+9292+ Link {
9393+ to: Route::DraftsList { ident: ident() },
9494+ Button {
9595+ variant: ButtonVariant::Ghost,
9696+ "Drafts"
9797+ }
9898+ }
9999+ }
100100+ }
101101+}
+84-2
crates/weaver-app/src/data.rs
···518518 None
519519 }
520520 });
521521- (res, memo);
521521+ (res, memo)
522522}
523523524524/// Fetches notebooks for a specific DID
···644644/// Fetches notebooks from UFOS client-side only (no SSR)
645645#[cfg(not(feature = "fullstack-server"))]
646646pub fn use_notebooks_from_ufos() -> (
647647- Resource<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>>,
647647+ Resource<Option<Vec<serde_json::Value>>>,
648648 Memo<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>>,
649649) {
650650 let fetcher = use_context::<crate::fetch::Fetcher>();
···808808 });
809809 let memo = use_memo(move || r.read().as_ref().and_then(|v| v.clone()));
810810 (r, memo)
811811+}
812812+813813+// ============================================================================
814814+// Ownership Checking
815815+// ============================================================================
816816+817817+/// Check if the current authenticated user owns a resource identified by an AtIdentifier.
818818+///
819819+/// Returns a memo that is:
820820+/// - `Some(true)` if the user is authenticated and their DID matches the resource owner
821821+/// - `Some(false)` if the user is authenticated but doesn't match, or resource is a handle
822822+/// - `None` if the user is not authenticated
823823+///
824824+/// For handles, this does a synchronous check that returns `false` since we can't resolve
825825+/// handles synchronously. Use `use_is_owner_async` for handle resolution.
826826+pub fn use_is_owner(resource_owner: ReadSignal<AtIdentifier<'static>>) -> Memo<Option<bool>> {
827827+ let auth_state = use_context::<Signal<AuthState>>();
828828+829829+ use_memo(move || {
830830+ let current_did = auth_state.read().did.clone()?;
831831+ let owner = resource_owner();
832832+833833+ match owner {
834834+ AtIdentifier::Did(did) => Some(did == current_did),
835835+ AtIdentifier::Handle(_) => Some(false), // Can't resolve synchronously
836836+ }
837837+ })
838838+}
839839+840840+/// Check ownership with async handle resolution.
841841+///
842842+/// Returns a resource that resolves to:
843843+/// - `Some(true)` if the user owns the resource
844844+/// - `Some(false)` if the user doesn't own the resource
845845+/// - `None` if the user is not authenticated
846846+#[cfg(feature = "fullstack-server")]
847847+pub fn use_is_owner_async(
848848+ resource_owner: ReadSignal<AtIdentifier<'static>>,
849849+) -> Resource<Option<bool>> {
850850+ let auth_state = use_context::<Signal<AuthState>>();
851851+ let fetcher = use_context::<crate::fetch::Fetcher>();
852852+853853+ use_resource(move || {
854854+ let fetcher = fetcher.clone();
855855+ let owner = resource_owner();
856856+ async move {
857857+ let current_did = auth_state.read().did.clone()?;
858858+859859+ match owner {
860860+ AtIdentifier::Did(did) => Some(did == current_did),
861861+ AtIdentifier::Handle(handle) => match fetcher.resolve_handle(&handle).await {
862862+ Ok(resolved_did) => Some(resolved_did == current_did),
863863+ Err(_) => Some(false),
864864+ },
865865+ }
866866+ }
867867+ })
868868+}
869869+870870+/// Check ownership with async handle resolution (client-only mode).
871871+#[cfg(not(feature = "fullstack-server"))]
872872+pub fn use_is_owner_async(
873873+ resource_owner: ReadSignal<AtIdentifier<'static>>,
874874+) -> Resource<Option<bool>> {
875875+ let auth_state = use_context::<Signal<AuthState>>();
876876+ let fetcher = use_context::<crate::fetch::Fetcher>();
877877+878878+ use_resource(move || {
879879+ let fetcher = fetcher.clone();
880880+ let owner = resource_owner();
881881+ async move {
882882+ let current_did = auth_state.read().did.clone()?;
883883+884884+ match owner {
885885+ AtIdentifier::Did(did) => Some(did == current_did),
886886+ AtIdentifier::Handle(handle) => match fetcher.resolve_handle(&handle).await {
887887+ Ok(resolved_did) => Some(resolved_did == current_did),
888888+ Err(_) => Some(false),
889889+ },
890890+ }
891891+ }
892892+ })
811893}
812894813895#[cfg(feature = "fullstack-server")]
···3535 "type": "string",
3636 "format": "datetime",
3737 "description": "Client-declared timestamp when this was originally created."
3838+ },
3939+ "updatedAt": {
4040+ "type": "string",
4141+ "format": "datetime",
4242+ "description": "Client-declared timestamp of last modification. Used for canonicality tiebreaking in multi-author scenarios."
3843 }
3944 }
4045 }
+5
lexicons/notebook/entry.json
···2525 "format": "datetime",
2626 "description": "Client-declared timestamp when this was originally created."
2727 },
2828+ "updatedAt": {
2929+ "type": "string",
3030+ "format": "datetime",
3131+ "description": "Client-declared timestamp of last modification. Used for canonicality tiebreaking in multi-author scenarios."
3232+ },
2833 "embeds": {
2934 "type": "object",
3035 "description": "The set of images and records, if any, embedded in the notebook entry.",
+35
lexicons/notebook/getEntry.json
···11+{
22+ "lexicon": 1,
33+ "id": "sh.weaver.notebook.getEntry",
44+ "defs": {
55+ "main": {
66+ "type": "query",
77+ "description": "Get an entry view by notebook URI and index, including prev/next navigation.",
88+ "parameters": {
99+ "type": "params",
1010+ "required": ["notebook"],
1111+ "properties": {
1212+ "notebook": {
1313+ "type": "string",
1414+ "format": "at-uri",
1515+ "description": "AT-URI of the notebook containing the entry."
1616+ },
1717+ "index": {
1818+ "type": "integer",
1919+ "minimum": 0,
2020+ "default": 0,
2121+ "description": "Zero-based index of the entry in the notebook's entry list."
2222+ }
2323+ }
2424+ },
2525+ "output": {
2626+ "encoding": "application/json",
2727+ "schema": {
2828+ "type": "ref",
2929+ "ref": "sh.weaver.notebook.defs#bookEntryView"
3030+ }
3131+ },
3232+ "errors": [{ "name": "NotebookNotFound" }, { "name": "EntryNotFound" }]
3333+ }
3434+ }
3535+}
+47
lexicons/notebook/getEntryByTitle.json
···11+{
22+ "lexicon": 1,
33+ "id": "sh.weaver.notebook.getEntryByTitle",
44+ "defs": {
55+ "main": {
66+ "type": "query",
77+ "description": "Get an entry view by notebook URI and title. Matches on either the entry's path or title field.",
88+ "parameters": {
99+ "type": "params",
1010+ "required": ["notebook", "title"],
1111+ "properties": {
1212+ "notebook": {
1313+ "type": "string",
1414+ "format": "at-uri",
1515+ "description": "AT-URI of the notebook containing the entry."
1616+ },
1717+ "title": {
1818+ "type": "string",
1919+ "maxLength": 300,
2020+ "description": "Title or path of the entry to fetch."
2121+ }
2222+ }
2323+ },
2424+ "output": {
2525+ "encoding": "application/json",
2626+ "schema": {
2727+ "type": "object",
2828+ "required": ["entry", "record"],
2929+ "properties": {
3030+ "entry": {
3131+ "type": "ref",
3232+ "ref": "sh.weaver.notebook.defs#bookEntryView"
3333+ },
3434+ "record": {
3535+ "type": "unknown",
3636+ "description": "The raw entry record data."
3737+ }
3838+ }
3939+ }
4040+ },
4141+ "errors": [
4242+ { "name": "NotebookNotFound" },
4343+ { "name": "EntryNotFound" }
4444+ ]
4545+ }
4646+ }
4747+}