···22};
23#[allow(unused_imports)]
24use std::sync::Arc;
25-use weaver_api::sh_weaver::notebook::{entry, BookEntryView};
2627#[component]
28-pub fn Entry(ident: AtIdentifier<'static>, book_title: SmolStr, title: SmolStr) -> Element {
29- let ident_clone = ident.clone();
30- let book_title_clone = book_title.clone();
31-032 // Use feature-gated hook for SSR support
33- let entry = crate::data::use_entry_data(ident.clone(), book_title.clone(), title.clone())?;
3435 // Handle blob caching when entry data is available
36 use_effect(move || {
···44 not(feature = "fullstack-server")
45 ))]
46 {
47- let ident = ident.clone();
48- let book_title = book_title.clone();
49 let images = images.clone();
50 spawn(async move {
51 let fetcher = use_context::<fetch::CachedFetcher>();
52 let _ = crate::service_worker::register_entry_blobs(
53- &ident,
54- book_title.as_str(),
55 &images,
56 &fetcher,
57 )
···60 }
61 #[cfg(feature = "fullstack-server")]
62 {
63- let ident = ident.clone();
64 let images = images.clone();
65 spawn(async move {
66 for image in &images.images {
···87 rsx! { EntryPage {
88 book_entry_view: book_entry_view.clone(),
89 entry_record: entry_record.clone(),
90- ident: use_handle(ident_clone)?(),
91- book_title: book_title_clone
92 } }
93 }
94 _ => rsx! { p { "Loading..." } },
···167 author_count: usize,
168) -> Element {
169 use crate::Route;
170- use jacquard::{from_data, IntoStatic};
171 use weaver_api::sh_weaver::notebook::entry::Entry;
172173 let entry_view = &entry.entry;
···22};
23#[allow(unused_imports)]
24use std::sync::Arc;
25+use weaver_api::sh_weaver::notebook::{BookEntryView, entry};
2627#[component]
28+pub fn Entry(
29+ ident: ReadSignal<AtIdentifier<'static>>,
30+ book_title: ReadSignal<SmolStr>,
31+ title: ReadSignal<SmolStr>,
32+) -> Element {
33 // Use feature-gated hook for SSR support
34+ let entry = crate::data::use_entry_data(ident(), book_title(), title())?;
3536 // Handle blob caching when entry data is available
37 use_effect(move || {
···45 not(feature = "fullstack-server")
46 ))]
47 {
0048 let images = images.clone();
49 spawn(async move {
50 let fetcher = use_context::<fetch::CachedFetcher>();
51 let _ = crate::service_worker::register_entry_blobs(
52+ &ident(),
53+ book_title().as_str(),
54 &images,
55 &fetcher,
56 )
···59 }
60 #[cfg(feature = "fullstack-server")]
61 {
62+ let ident = ident();
63 let images = images.clone();
64 spawn(async move {
65 for image in &images.images {
···86 rsx! { EntryPage {
87 book_entry_view: book_entry_view.clone(),
88 entry_record: entry_record.clone(),
89+ ident: use_handle(ident())?(),
90+ book_title: book_title()
91 } }
92 }
93 _ => rsx! { p { "Loading..." } },
···166 author_count: usize,
167) -> Element {
168 use crate::Route;
169+ use jacquard::{IntoStatic, from_data};
170 use weaver_api::sh_weaver::notebook::entry::Entry;
171172 let entry_view = &entry.entry;
+8-8
crates/weaver-app/src/components/identity.rs
···1-use crate::{fetch, Route};
2use dioxus::prelude::*;
3use jacquard::{smol_str::SmolStr, types::ident::AtIdentifier};
4use weaver_api::com_atproto::repo::strong_ref::StrongRef;
···7const NOTEBOOK_CARD_CSS: Asset = asset!("/assets/styling/notebook-card.css");
89#[component]
10-pub fn Repository(ident: AtIdentifier<'static>) -> Element {
11 rsx! {
12 // We can create elements inside the rsx macro with the element name followed by a block of attributes and children.
13 div {
···17}
1819#[component]
20-pub fn RepositoryIndex(ident: AtIdentifier<'static>) -> Element {
21 use crate::components::ProfileDisplay;
2223 let fetcher = use_context::<fetch::CachedFetcher>();
2425 // Fetch notebooks for this specific DID
26- let notebooks = use_resource(use_reactive!(|ident| {
27 let fetcher = fetcher.clone();
28- async move { fetcher.fetch_notebooks_for_did(&ident).await }
29- }));
3031 rsx! {
32 document::Stylesheet { href: NOTEBOOK_CARD_CSS }
···34 div { class: "repository-layout",
35 // Profile sidebar (desktop) / header (mobile)
36 aside { class: "repository-sidebar",
37- ProfileDisplay { ident: ident.clone() }
38 }
3940 // Main content area
···178 if entry_list.len() <= 5 {
179 // Show all entries if 5 or fewer
180 rsx! {
181- for (i, entry_view) in entry_list.iter().enumerate() {
182 {
183 let entry_title = entry_view.entry.title.as_ref()
184 .map(|t| t.as_ref())
···1+use crate::{Route, fetch};
2use dioxus::prelude::*;
3use jacquard::{smol_str::SmolStr, types::ident::AtIdentifier};
4use weaver_api::com_atproto::repo::strong_ref::StrongRef;
···7const NOTEBOOK_CARD_CSS: Asset = asset!("/assets/styling/notebook-card.css");
89#[component]
10+pub fn Repository(ident: ReadSignal<AtIdentifier<'static>>) -> Element {
11 rsx! {
12 // We can create elements inside the rsx macro with the element name followed by a block of attributes and children.
13 div {
···17}
1819#[component]
20+pub fn RepositoryIndex(ident: ReadSignal<AtIdentifier<'static>>) -> Element {
21 use crate::components::ProfileDisplay;
2223 let fetcher = use_context::<fetch::CachedFetcher>();
2425 // Fetch notebooks for this specific DID
26+ let notebooks = use_resource(move || {
27 let fetcher = fetcher.clone();
28+ async move { fetcher.fetch_notebooks_for_did(&ident()).await }
29+ });
3031 rsx! {
32 document::Stylesheet { href: NOTEBOOK_CARD_CSS }
···34 div { class: "repository-layout",
35 // Profile sidebar (desktop) / header (mobile)
36 aside { class: "repository-sidebar",
37+ ProfileDisplay { ident: ident() }
38 }
3940 // Main content area
···178 if entry_list.len() <= 5 {
179 // Show all entries if 5 or fewer
180 rsx! {
181+ for entry_view in entry_list.iter() {
182 {
183 let entry_title = entry_view.entry.title.as_ref()
184 .map(|t| t.as_ref())
···6pub use css::NotebookCss;
78mod entry;
09pub use entry::{Entry, EntryCard, EntryMarkdown};
1011pub mod identity;
012pub use identity::{NotebookCard, Repository, RepositoryIndex};
13pub mod avatar;
14···1718pub mod notebook_cover;
19pub use notebook_cover::NotebookCover;
002021use dioxus::prelude::*;
22···117 .with_hash_suffix(false)
118 .into_asset_options()
119);
000
···6pub use css::NotebookCss;
78mod entry;
9+#[allow(unused_imports)]
10pub use entry::{Entry, EntryCard, EntryMarkdown};
1112pub mod identity;
13+#[allow(unused_imports)]
14pub use identity::{NotebookCard, Repository, RepositoryIndex};
15pub mod avatar;
16···1920pub mod notebook_cover;
21pub use notebook_cover::NotebookCover;
22+23+pub mod login;
2425use dioxus::prelude::*;
26···121 .with_hash_suffix(false)
122 .into_asset_options()
123);
124+pub mod input;
125+pub mod dialog;
126+pub mod button;
···01use crate::cache_impl;
02use dioxus::Result;
00000000003use jacquard::prelude::*;
4-use jacquard::{client::BasicClient, smol_str::SmolStr, types::ident::AtIdentifier};
00000005use serde::{Deserialize, Serialize};
06use std::{sync::Arc, time::Duration};
07use weaver_api::{
8 com_atproto::repo::strong_ref::StrongRef,
9 sh_weaver::{
···22 time_us: u64,
23}
24000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000025#[derive(Clone)]
26pub struct CachedFetcher {
27- pub client: Arc<BasicClient>,
28 book_cache: cache_impl::Cache<
29 (AtIdentifier<'static>, SmolStr),
30 Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>,
···36 profile_cache: cache_impl::Cache<AtIdentifier<'static>, Arc<ProfileDataView<'static>>>,
37}
3800000039impl CachedFetcher {
40- pub fn new(client: Arc<BasicClient>) -> Self {
41 Self {
42- client,
43 book_cache: cache_impl::new_cache(100, Duration::from_secs(1200)),
44 entry_cache: cache_impl::new_cache(100, Duration::from_secs(600)),
45 profile_cache: cache_impl::new_cache(100, Duration::from_secs(1800)),
46 }
47 }
48000000000000000000000000000049 pub async fn get_notebook(
50 &self,
51 ident: AtIdentifier<'static>,
···54 if let Some(entry) = cache_impl::get(&self.book_cache, &(ident.clone(), title.clone())) {
55 Ok(Some(entry))
56 } else {
57- if let Some((notebook, entries)) =
58- self.client
59- .notebook_by_title(&ident, &title)
60- .await
61- .map_err(|e| dioxus::CapturedError::from_display(e))?
62 {
63 let stored = Arc::new((notebook, entries));
64 cache_impl::insert(&self.book_cache, (ident, title), stored.clone());
···82 {
83 Ok(Some(entry))
84 } else {
85- if let Some(entry) = self
86- .client
87 .entry_by_title(notebook, entries.as_ref(), &entry_title)
88 .await
89 .map_err(|e| dioxus::CapturedError::from_display(e))?
···116 .map_err(|e| dioxus::CapturedError::from_display(e))?;
117118 let mut notebooks = Vec::new();
0119120 for ufos_record in records {
121 // Construct URI
···127 .map_err(|e| dioxus::CapturedError::from_display(format!("Invalid URI: {}", e)))?;
128129 // Fetch the full notebook view (which hydrates authors)
130- match self.client.view_notebook(&uri).await {
131 Ok((notebook, entries)) => {
132 let ident = uri.authority().clone().into_static();
133 let title = notebook
···161 com_atproto::repo::list_records::ListRecords, sh_weaver::notebook::book::Book,
162 };
16300164 // Resolve DID and PDS
165 let (repo_did, pds_url) = match ident {
166 AtIdentifier::Did(did) => {
167- let pds = self
168- .client
169 .pds_for_did(did)
170 .await
171 .map_err(|e| dioxus::CapturedError::from_display(e))?;
172 (did.clone(), pds)
173 }
174- AtIdentifier::Handle(handle) => self
175- .client
176 .pds_for_handle(handle)
177 .await
178 .map_err(|e| dioxus::CapturedError::from_display(e))?,
179 };
180181 // Fetch all notebook records for this repo
182- let resp = self
183- .client
184 .xrpc(pds_url)
185 .send(
186 &ListRecords::new()
···197 if let Ok(list) = resp.parse() {
198 for record in list.records {
199 // View the notebook (which hydrates authors)
200- match self.client.view_notebook(&record.uri).await {
201 Ok((notebook, entries)) => {
202 let ident = record.uri.authority().clone().into_static();
203 let title = notebook
···227 if let Some(result) = self.get_notebook(ident.clone(), book_title).await? {
228 let (notebook, entries) = result.as_ref();
229 let mut book_entries = Vec::new();
0230231 for index in 0..entries.len() {
232- match self.client.view_entry(notebook, entries, index).await {
233 Ok(book_entry) => book_entries.push(book_entry),
234 Err(_) => continue, // Skip entries that fail to load
235 }
···253 return Ok(cached);
254 }
25500256 let did = match ident {
257 AtIdentifier::Did(d) => d.clone(),
258- AtIdentifier::Handle(h) => self
259- .client
260 .resolve_handle(h)
261 .await
262 .map_err(|e| dioxus::CapturedError::from_display(e))?,
263 };
264265- let (_uri, profile_view) = self
266- .client
267 .hydrate_profile_view(&did)
268 .await
269 .map_err(|e| dioxus::CapturedError::from_display(e))?;
···274 Ok(result)
275 }
276}
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
···1+use crate::auth::AuthStore;
2use crate::cache_impl;
3+use dioxus::prelude::*;
4use dioxus::Result;
5+use jacquard::client::Agent;
6+use jacquard::client::AgentKind;
7+use jacquard::error::ClientError;
8+use jacquard::error::XrpcResult;
9+use jacquard::identity::resolver::DidDocResponse;
10+use jacquard::identity::resolver::IdentityError;
11+use jacquard::identity::resolver::ResolverOptions;
12+use jacquard::identity::JacquardResolver;
13+use jacquard::oauth::client::OAuthClient;
14+use jacquard::oauth::client::OAuthSession;
15use jacquard::prelude::*;
16+use jacquard::types::string::Did;
17+use jacquard::types::string::Handle;
18+use jacquard::xrpc::XrpcResponse;
19+use jacquard::xrpc::*;
20+use jacquard::AuthorizationToken;
21+use jacquard::CowStr;
22+use jacquard::IntoStatic;
23+use jacquard::{smol_str::SmolStr, types::ident::AtIdentifier};
24use serde::{Deserialize, Serialize};
25+use std::future::Future;
26use std::{sync::Arc, time::Duration};
27+use tokio::sync::RwLock;
28use weaver_api::{
29 com_atproto::repo::strong_ref::StrongRef,
30 sh_weaver::{
···43 time_us: u64,
44}
4546+pub struct Client {
47+ pub oauth_client: Arc<OAuthClient<JacquardResolver, AuthStore>>,
48+ pub session: RwLock<Option<Arc<Agent<OAuthSession<JacquardResolver, AuthStore>>>>>,
49+}
50+51+impl Client {
52+ pub fn new(oauth_client: OAuthClient<JacquardResolver, AuthStore>) -> Self {
53+ Self {
54+ oauth_client: Arc::new(oauth_client),
55+ session: RwLock::new(None),
56+ }
57+ }
58+}
59+60+impl HttpClient for Client {
61+ type Error = reqwest::Error;
62+63+ #[cfg(not(target_arch = "wasm32"))]
64+ fn send_http(
65+ &self,
66+ request: http::Request<Vec<u8>>,
67+ ) -> impl Future<Output = core::result::Result<http::Response<Vec<u8>>, Self::Error>> + Send
68+ {
69+ self.oauth_client.client.send_http(request)
70+ }
71+72+ #[doc = " Send an HTTP request and return the response."]
73+ #[cfg(target_arch = "wasm32")]
74+ fn send_http(
75+ &self,
76+ request: http::Request<Vec<u8>>,
77+ ) -> impl Future<Output = core::result::Result<http::Response<Vec<u8>>, Self::Error>> {
78+ self.oauth_client.client.send_http(request)
79+ }
80+}
81+82+impl XrpcClient for Client {
83+ #[doc = " Get the base URI for the client."]
84+ fn base_uri(&self) -> impl Future<Output = jacquard::url::Url> + Send {
85+ async {
86+ let guard = self.session.read().await;
87+ if let Some(session) = guard.clone() {
88+ session.base_uri().await
89+ } else {
90+ self.oauth_client.base_uri().await
91+ }
92+ }
93+ }
94+95+ #[doc = " Send an XRPC request and parse the response"]
96+ #[cfg(not(target_arch = "wasm32"))]
97+ fn send<R>(&self, request: R) -> impl Future<Output = XrpcResult<XrpcResponse<R>>> + Send
98+ where
99+ R: XrpcRequest + Send + Sync,
100+ <R as XrpcRequest>::Response: Send + Sync,
101+ Self: Sync,
102+ {
103+ async {
104+ let guard = self.session.read().await;
105+ if let Some(session) = guard.clone() {
106+ session.send(request).await
107+ } else {
108+ self.oauth_client.send(request).await
109+ }
110+ }
111+ }
112+113+ #[doc = " Send an XRPC request and parse the response"]
114+ #[cfg(not(target_arch = "wasm32"))]
115+ fn send_with_opts<R>(
116+ &self,
117+ request: R,
118+ opts: CallOptions<'_>,
119+ ) -> impl Future<Output = XrpcResult<XrpcResponse<R>>> + Send
120+ where
121+ R: XrpcRequest + Send + Sync,
122+ <R as XrpcRequest>::Response: Send + Sync,
123+ Self: Sync,
124+ {
125+ async {
126+ let guard = self.session.read().await;
127+ if let Some(session) = guard.clone() {
128+ session.send_with_opts(request, opts).await
129+ } else {
130+ self.oauth_client.send_with_opts(request, opts).await
131+ }
132+ }
133+ }
134+135+ #[doc = " Send an XRPC request and parse the response"]
136+ #[cfg(target_arch = "wasm32")]
137+ fn send<R>(&self, request: R) -> impl Future<Output = XrpcResult<XrpcResponse<R>>>
138+ where
139+ R: XrpcRequest + Send + Sync,
140+ <R as XrpcRequest>::Response: Send + Sync,
141+ {
142+ async {
143+ let guard = self.session.read().await;
144+ if let Some(session) = guard.clone() {
145+ session.send(request).await
146+ } else {
147+ self.oauth_client.send(request).await
148+ }
149+ }
150+ }
151+152+ #[doc = " Send an XRPC request and parse the response"]
153+ #[cfg(target_arch = "wasm32")]
154+ fn send_with_opts<R>(
155+ &self,
156+ request: R,
157+ opts: CallOptions<'_>,
158+ ) -> impl Future<Output = XrpcResult<XrpcResponse<R>>>
159+ where
160+ R: XrpcRequest + Send + Sync,
161+ <R as XrpcRequest>::Response: Send + Sync,
162+ {
163+ async {
164+ let guard = self.session.read().await;
165+ if let Some(session) = guard.clone() {
166+ session.send_with_opts(request, opts).await
167+ } else {
168+ self.oauth_client.send_with_opts(request, opts).await
169+ }
170+ }
171+ }
172+173+ #[doc = " Set the base URI for the client."]
174+ fn set_base_uri(&self, url: jacquard::url::Url) -> impl Future<Output = ()> + Send {
175+ async {
176+ let guard = self.session.read().await;
177+ if let Some(session) = guard.clone() {
178+ session.set_base_uri(url).await
179+ } else {
180+ self.oauth_client.set_base_uri(url).await
181+ }
182+ }
183+ }
184+185+ #[doc = " Get the call options for the client."]
186+ fn opts(&self) -> impl Future<Output = CallOptions<'_>> + Send {
187+ async {
188+ let guard = self.session.read().await;
189+ if let Some(session) = guard.clone() {
190+ session.opts().await.into_static()
191+ } else {
192+ self.oauth_client.opts().await
193+ }
194+ }
195+ }
196+197+ #[doc = " Set the call options for the client."]
198+ fn set_opts(&self, opts: CallOptions) -> impl Future<Output = ()> + Send {
199+ async {
200+ let guard = self.session.read().await;
201+ if let Some(session) = guard.clone() {
202+ session.set_opts(opts).await
203+ } else {
204+ self.oauth_client.set_opts(opts).await
205+ }
206+ }
207+ }
208+}
209+210+impl IdentityResolver for Client {
211+ #[doc = " Access options for validation decisions in default methods"]
212+ fn options(&self) -> &ResolverOptions {
213+ self.oauth_client.client.options()
214+ }
215+216+ #[doc = " Resolve handle"]
217+ #[cfg(not(target_arch = "wasm32"))]
218+ fn resolve_handle(
219+ &self,
220+ handle: &Handle<'_>,
221+ ) -> impl Future<Output = core::result::Result<Did<'static>, IdentityError>> + Send
222+ where
223+ Self: Sync,
224+ {
225+ self.oauth_client.client.resolve_handle(handle)
226+ }
227+228+ #[doc = " Resolve DID document"]
229+ #[cfg(not(target_arch = "wasm32"))]
230+ fn resolve_did_doc(
231+ &self,
232+ did: &Did<'_>,
233+ ) -> impl Future<Output = core::result::Result<DidDocResponse, IdentityError>> + Send
234+ where
235+ Self: Sync,
236+ {
237+ self.oauth_client.client.resolve_did_doc(did)
238+ }
239+240+ #[doc = " Resolve handle"]
241+ #[cfg(target_arch = "wasm32")]
242+ fn resolve_handle(
243+ &self,
244+ handle: &Handle<'_>,
245+ ) -> impl Future<Output = core::result::Result<Did<'static>, IdentityError>> {
246+ self.oauth_client.client.resolve_handle(handle)
247+ }
248+249+ #[doc = " Resolve DID document"]
250+ #[cfg(target_arch = "wasm32")]
251+ fn resolve_did_doc(
252+ &self,
253+ did: &Did<'_>,
254+ ) -> impl Future<Output = core::result::Result<DidDocResponse, IdentityError>> {
255+ self.oauth_client.client.resolve_did_doc(did)
256+ }
257+}
258+259+impl AgentSession for Client {
260+ #[doc = " Identify the kind of session."]
261+ fn session_kind(&self) -> AgentKind {
262+ self.oauth_client.session_kind()
263+ }
264+265+ #[doc = " Return current DID and an optional session id (always Some for OAuth)."]
266+ async fn session_info(&self) -> Option<(Did<'static>, Option<CowStr<'static>>)> {
267+ let guard = self.session.read().await;
268+ if let Some(session) = guard.clone() {
269+ session.info().await
270+ } else {
271+ None
272+ }
273+ }
274+275+ #[doc = " Current base endpoint."]
276+ async fn endpoint(&self) -> jacquard::url::Url {
277+ let guard = self.session.read().await;
278+ if let Some(session) = guard.clone() {
279+ session.endpoint().await
280+ } else {
281+ self.oauth_client.endpoint().await
282+ }
283+ }
284+285+ #[doc = " Override per-session call options."]
286+ async fn set_options<'a>(&'a self, opts: CallOptions<'a>) {
287+ let guard = self.session.read().await;
288+ if let Some(session) = guard.clone() {
289+ session.set_options(opts).await
290+ } else {
291+ self.oauth_client.set_options(opts).await
292+ }
293+ }
294+295+ #[doc = " Refresh the session and return a fresh AuthorizationToken."]
296+ async fn refresh(&self) -> XrpcResult<AuthorizationToken<'static>> {
297+ let guard = self.session.read().await;
298+ if let Some(session) = guard.clone() {
299+ session.refresh().await
300+ } else {
301+ Err(ClientError::auth(
302+ jacquard::error::AuthError::NotAuthenticated,
303+ ))
304+ }
305+ }
306+}
307+308#[derive(Clone)]
309pub struct CachedFetcher {
310+ pub client: Arc<Client>,
311 book_cache: cache_impl::Cache<
312 (AtIdentifier<'static>, SmolStr),
313 Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>,
···319 profile_cache: cache_impl::Cache<AtIdentifier<'static>, Arc<ProfileDataView<'static>>>,
320}
321322+/// SAFETY: This isn't thread-safe on WASM, but we aren't multithreaded on WASM
323+unsafe impl Sync for CachedFetcher {}
324+325+/// SAFETY: This isn't thread-safe on WASM, but we aren't multithreaded on WASM
326+unsafe impl Send for CachedFetcher {}
327+328impl CachedFetcher {
329+ pub fn new(client: OAuthClient<JacquardResolver, AuthStore>) -> Self {
330 Self {
331+ client: Arc::new(Client::new(client)),
332 book_cache: cache_impl::new_cache(100, Duration::from_secs(1200)),
333 entry_cache: cache_impl::new_cache(100, Duration::from_secs(600)),
334 profile_cache: cache_impl::new_cache(100, Duration::from_secs(1800)),
335 }
336 }
337338+ pub async fn upgrade_to_authenticated(
339+ &self,
340+ session: OAuthSession<JacquardResolver, crate::auth::AuthStore>,
341+ ) {
342+ let mut session_slot = self.client.session.write().await;
343+ *session_slot = Some(Arc::new(Agent::new(session)));
344+ }
345+346+ pub async fn downgrade_to_unauthenticated(&self) {
347+ let mut session_slot = self.client.session.write().await;
348+ if let Some(session) = session_slot.take() {
349+ session.inner().logout().await;
350+ }
351+ }
352+353+ pub async fn current_did(&self) -> Option<Did<'static>> {
354+ let session_slot = self.client.session.read().await;
355+ if let Some(session) = session_slot.as_ref() {
356+ session.info().await.map(|(d, _)| d)
357+ } else {
358+ None
359+ }
360+ }
361+362+ pub fn get_client(&self) -> Arc<Client> {
363+ self.client.clone()
364+ }
365+366 pub async fn get_notebook(
367 &self,
368 ident: AtIdentifier<'static>,
···371 if let Some(entry) = cache_impl::get(&self.book_cache, &(ident.clone(), title.clone())) {
372 Ok(Some(entry))
373 } else {
374+ let client = self.get_client();
375+ if let Some((notebook, entries)) = client
376+ .notebook_by_title(&ident, &title)
377+ .await
378+ .map_err(|e| dioxus::CapturedError::from_display(e))?
379 {
380 let stored = Arc::new((notebook, entries));
381 cache_impl::insert(&self.book_cache, (ident, title), stored.clone());
···399 {
400 Ok(Some(entry))
401 } else {
402+ let client = self.get_client();
403+ if let Some(entry) = client
404 .entry_by_title(notebook, entries.as_ref(), &entry_title)
405 .await
406 .map_err(|e| dioxus::CapturedError::from_display(e))?
···433 .map_err(|e| dioxus::CapturedError::from_display(e))?;
434435 let mut notebooks = Vec::new();
436+ let client = self.get_client();
437438 for ufos_record in records {
439 // Construct URI
···445 .map_err(|e| dioxus::CapturedError::from_display(format!("Invalid URI: {}", e)))?;
446447 // Fetch the full notebook view (which hydrates authors)
448+ match client.view_notebook(&uri).await {
449 Ok((notebook, entries)) => {
450 let ident = uri.authority().clone().into_static();
451 let title = notebook
···479 com_atproto::repo::list_records::ListRecords, sh_weaver::notebook::book::Book,
480 };
481482+ let client = self.get_client();
483+484 // Resolve DID and PDS
485 let (repo_did, pds_url) = match ident {
486 AtIdentifier::Did(did) => {
487+ let pds = client
0488 .pds_for_did(did)
489 .await
490 .map_err(|e| dioxus::CapturedError::from_display(e))?;
491 (did.clone(), pds)
492 }
493+ AtIdentifier::Handle(handle) => client
0494 .pds_for_handle(handle)
495 .await
496 .map_err(|e| dioxus::CapturedError::from_display(e))?,
497 };
498499 // Fetch all notebook records for this repo
500+ let resp = client
0501 .xrpc(pds_url)
502 .send(
503 &ListRecords::new()
···514 if let Ok(list) = resp.parse() {
515 for record in list.records {
516 // View the notebook (which hydrates authors)
517+ match client.view_notebook(&record.uri).await {
518 Ok((notebook, entries)) => {
519 let ident = record.uri.authority().clone().into_static();
520 let title = notebook
···544 if let Some(result) = self.get_notebook(ident.clone(), book_title).await? {
545 let (notebook, entries) = result.as_ref();
546 let mut book_entries = Vec::new();
547+ let client = self.get_client();
548549 for index in 0..entries.len() {
550+ match client.view_entry(notebook, entries, index).await {
551 Ok(book_entry) => book_entries.push(book_entry),
552 Err(_) => continue, // Skip entries that fail to load
553 }
···571 return Ok(cached);
572 }
573574+ let client = self.get_client();
575+576 let did = match ident {
577 AtIdentifier::Did(d) => d.clone(),
578+ AtIdentifier::Handle(h) => client
0579 .resolve_handle(h)
580 .await
581 .map_err(|e| dioxus::CapturedError::from_display(e))?,
582 };
583584+ let (_uri, profile_view) = client
0585 .hydrate_profile_view(&did)
586 .await
587 .map_err(|e| dioxus::CapturedError::from_display(e))?;
···592 Ok(result)
593 }
594}
595+596+impl HttpClient for CachedFetcher {
597+ #[doc = " Error type returned by the HTTP client"]
598+ type Error = reqwest::Error;
599+600+ #[doc = " Send an HTTP request and return the response."]
601+ #[cfg(not(target_arch = "wasm32"))]
602+ fn send_http(
603+ &self,
604+ request: http::Request<Vec<u8>>,
605+ ) -> impl Future<Output = core::result::Result<http::Response<Vec<u8>>, Self::Error>> + Send
606+ {
607+ async {
608+ let client = self.get_client();
609+ client.send_http(request).await
610+ }
611+ }
612+613+ #[doc = " Send an HTTP request and return the response."]
614+ #[cfg(target_arch = "wasm32")]
615+ fn send_http(
616+ &self,
617+ request: http::Request<Vec<u8>>,
618+ ) -> impl Future<Output = core::result::Result<http::Response<Vec<u8>>, Self::Error>> {
619+ async {
620+ let client = self.get_client();
621+ client.send_http(request).await
622+ }
623+ }
624+}
625+626+impl XrpcClient for CachedFetcher {
627+ #[doc = " Get the base URI for the client."]
628+ fn base_uri(&self) -> impl Future<Output = jacquard::url::Url> + Send {
629+ self.client.base_uri()
630+ }
631+632+ #[doc = " Send an XRPC request and parse the response"]
633+ #[cfg(not(target_arch = "wasm32"))]
634+ fn send<R>(&self, request: R) -> impl Future<Output = XrpcResult<XrpcResponse<R>>> + Send
635+ where
636+ R: XrpcRequest + Send + Sync,
637+ <R as XrpcRequest>::Response: Send + Sync,
638+ Self: Sync,
639+ {
640+ self.client.send(request)
641+ }
642+643+ #[doc = " Send an XRPC request and parse the response"]
644+ #[cfg(not(target_arch = "wasm32"))]
645+ fn send_with_opts<R>(
646+ &self,
647+ request: R,
648+ opts: CallOptions<'_>,
649+ ) -> impl Future<Output = XrpcResult<XrpcResponse<R>>> + Send
650+ where
651+ R: XrpcRequest + Send + Sync,
652+ <R as XrpcRequest>::Response: Send + Sync,
653+ Self: Sync,
654+ {
655+ self.client.send_with_opts(request, opts)
656+ }
657+658+ #[doc = " Send an XRPC request and parse the response"]
659+ #[cfg(target_arch = "wasm32")]
660+ fn send<R>(&self, request: R) -> impl Future<Output = XrpcResult<XrpcResponse<R>>>
661+ where
662+ R: XrpcRequest + Send + Sync,
663+ <R as XrpcRequest>::Response: Send + Sync,
664+ {
665+ self.client.send(request)
666+ }
667+668+ #[doc = " Send an XRPC request and parse the response"]
669+ #[cfg(target_arch = "wasm32")]
670+ fn send_with_opts<R>(
671+ &self,
672+ request: R,
673+ opts: CallOptions<'_>,
674+ ) -> impl Future<Output = XrpcResult<XrpcResponse<R>>>
675+ where
676+ R: XrpcRequest + Send + Sync,
677+ <R as XrpcRequest>::Response: Send + Sync,
678+ {
679+ self.client.send_with_opts(request, opts)
680+ }
681+682+ #[doc = " Set the base URI for the client."]
683+ fn set_base_uri(&self, url: jacquard::url::Url) -> impl Future<Output = ()> + Send {
684+ self.client.set_base_uri(url)
685+ }
686+687+ #[doc = " Get the call options for the client."]
688+ fn opts(&self) -> impl Future<Output = CallOptions<'_>> + Send {
689+ self.client.opts()
690+ }
691+692+ #[doc = " Set the call options for the client."]
693+ fn set_opts(&self, opts: CallOptions) -> impl Future<Output = ()> + Send {
694+ self.client.set_opts(opts)
695+ }
696+}
697+698+impl IdentityResolver for CachedFetcher {
699+ #[doc = " Access options for validation decisions in default methods"]
700+ fn options(&self) -> &ResolverOptions {
701+ self.client.options()
702+ }
703+704+ #[doc = " Resolve handle"]
705+ #[cfg(not(target_arch = "wasm32"))]
706+ fn resolve_handle(
707+ &self,
708+ handle: &Handle<'_>,
709+ ) -> impl Future<Output = core::result::Result<Did<'static>, IdentityError>> + Send
710+ where
711+ Self: Sync,
712+ {
713+ self.client.resolve_handle(handle)
714+ }
715+716+ #[doc = " Resolve DID document"]
717+ #[cfg(not(target_arch = "wasm32"))]
718+ fn resolve_did_doc(
719+ &self,
720+ did: &Did<'_>,
721+ ) -> impl Future<Output = core::result::Result<DidDocResponse, IdentityError>> + Send
722+ where
723+ Self: Sync,
724+ {
725+ self.client.resolve_did_doc(did)
726+ }
727+728+ #[doc = " Resolve handle"]
729+ #[cfg(target_arch = "wasm32")]
730+ fn resolve_handle(
731+ &self,
732+ handle: &Handle<'_>,
733+ ) -> impl Future<Output = core::result::Result<Did<'static>, IdentityError>> {
734+ self.client.resolve_handle(handle)
735+ }
736+737+ #[doc = " Resolve DID document"]
738+ #[cfg(target_arch = "wasm32")]
739+ fn resolve_did_doc(
740+ &self,
741+ did: &Did<'_>,
742+ ) -> impl Future<Output = core::result::Result<DidDocResponse, IdentityError>> {
743+ self.client.resolve_did_doc(did)
744+ }
745+}
746+747+impl AgentSession for CachedFetcher {
748+ #[doc = " Identify the kind of session."]
749+ fn session_kind(&self) -> AgentKind {
750+ self.client.session_kind()
751+ }
752+753+ #[doc = " Return current DID and an optional session id (always Some for OAuth)."]
754+ async fn session_info(&self) -> Option<(Did<'static>, Option<CowStr<'static>>)> {
755+ self.client.session_info().await
756+ }
757+758+ async fn endpoint(&self) -> jacquard::url::Url {
759+ self.client.endpoint().await
760+ }
761+762+ async fn set_options<'a>(&'a self, opts: CallOptions<'a>) {
763+ self.client.set_options(opts).await
764+ }
765+766+ async fn refresh(&self) -> XrpcResult<AuthorizationToken<'static>> {
767+ self.client.refresh().await
768+ }
769+}
+48-21
crates/weaver-app/src/main.rs
···2// need dioxus
3use components::{Entry, Repository, RepositoryIndex};
4#[allow(unused)]
5-use dioxus::{prelude::*, CapturedError};
6007#[cfg(all(feature = "fullstack-server", feature = "server"))]
8use dioxus::fullstack::response::Extension;
9-#[cfg(feature = "fullstack-server")]
10-use dioxus::fullstack::FullstackContext;
11#[allow(unused)]
12use jacquard::{
13- client::BasicClient,
14 smol_str::SmolStr,
15 types::{cid::Cid, string::AtIdentifier},
16};
17-18use std::sync::Arc;
019#[allow(unused)]
20-use views::{Home, Navbar, Notebook, NotebookIndex, NotebookPage, RecordView};
0000021022#[cfg(feature = "server")]
23mod blobcache;
24mod cache_impl;
25/// Define a components module that contains all shared components for our app.
26mod components;
027mod data;
028mod fetch;
29mod service_worker;
30/// Define a views module that contains the UI for all Layouts and Routes for our app.
···48 #[layout(ErrorLayout)]
49 #[route("/record#:uri")]
50 RecordView { uri: SmolStr },
0051 #[nest("/:ident")]
52 #[layout(Repository)]
53 #[route("/")]
···79 .into_response()
80}
8100082fn main() {
083 // Set up better panic messages for wasm
84 #[cfg(target_arch = "wasm32")]
85 console_error_panic_hook::set_once();
···88 #[cfg(feature = "server")]
89 dioxus::serve(|| async move {
90 use crate::blobcache::BlobCache;
91- use crate::fetch::CachedFetcher;
92 use axum::{
93 extract::{Extension, Request},
94 middleware,
···105 .merge(dioxus::server::router(App))
106 };
107108- let client = Arc::new(BasicClient::unauthenticated());
109-110 #[cfg(feature = "fullstack-server")]
111 let router = {
112- let fetcher = Arc::new(CachedFetcher::new(client.clone()));
113- let blob_cache = Arc::new(BlobCache::new(client.clone()));
000000114 dioxus::server::router(App).layer(middleware::from_fn({
0115 let fetcher = fetcher.clone();
116- let blob_cache = blob_cache.clone();
117 move |mut req: Request, next: Next| {
0118 let fetcher = fetcher.clone();
119- let blob_cache = blob_cache.clone();
120 async move {
121- // Attach extensions for dioxus server functions
122 req.extensions_mut().insert(fetcher);
123- req.extensions_mut().insert(blob_cache);
124125 // And then return the response with `next.run()
126 Ok::<_, Infallible>(next.run(req).await)
···137 dioxus::launch(App);
138}
139140-/// App is the main component of our app. Components are the building blocks of dioxus apps. Each component is a function
141-/// that takes some props and returns an Element. In this case, App takes no props because it is the root of our app.
142-///
143-/// Components should be annotated with `#[component]` to support props, better error messages, and autocomplete
144#[component]
145fn App() -> Element {
146- // The `rsx!` macro lets us define HTML inside of rust. It expands to an Element with all of our HTML inside.
147- use_context_provider(|| fetch::CachedFetcher::new(Arc::new(BasicClient::unauthenticated())));
0000000000000148149 // Register service worker on startup (only on web)
150 #[cfg(all(
···2// need dioxus
3use components::{Entry, Repository, RepositoryIndex};
4#[allow(unused)]
5+use dioxus::{CapturedError, prelude::*};
67+#[cfg(feature = "fullstack-server")]
8+use dioxus::fullstack::FullstackContext;
9#[cfg(all(feature = "fullstack-server", feature = "server"))]
10use dioxus::fullstack::response::Extension;
11+use dioxus_logger::tracing::Level;
12+use jacquard::oauth::{client::OAuthClient, session::ClientData};
13#[allow(unused)]
14use jacquard::{
015 smol_str::SmolStr,
16 types::{cid::Cid, string::AtIdentifier},
17};
18+#[cfg(feature = "server")]
19use std::sync::Arc;
20+use std::sync::{LazyLock, Mutex};
21#[allow(unused)]
22+use views::{Callback, Home, Navbar, Notebook, NotebookIndex, NotebookPage, RecordView};
23+24+use crate::{
25+ auth::{AuthState, AuthStore},
26+ config::{Config, OAuthConfig},
27+};
2829+mod auth;
30#[cfg(feature = "server")]
31mod blobcache;
32mod cache_impl;
33/// Define a components module that contains all shared components for our app.
34mod components;
35+mod config;
36mod data;
37+mod env;
38mod fetch;
39mod service_worker;
40/// Define a views module that contains the UI for all Layouts and Routes for our app.
···58 #[layout(ErrorLayout)]
59 #[route("/record#:uri")]
60 RecordView { uri: SmolStr },
61+ #[route("/callback?:state&:iss&:code")]
62+ Callback { state: SmolStr, iss: SmolStr, code: SmolStr },
63 #[nest("/:ident")]
64 #[layout(Repository)]
65 #[route("/")]
···91 .into_response()
92}
9394+pub static CONFIG: LazyLock<Config> = LazyLock::new(|| Config {
95+ oauth: OAuthConfig::from_env().as_metadata(),
96+});
97fn main() {
98+ dioxus_logger::init(Level::DEBUG).expect("logger failed to init");
99 // Set up better panic messages for wasm
100 #[cfg(target_arch = "wasm32")]
101 console_error_panic_hook::set_once();
···104 #[cfg(feature = "server")]
105 dioxus::serve(|| async move {
106 use crate::blobcache::BlobCache;
0107 use axum::{
108 extract::{Extension, Request},
109 middleware,
···120 .merge(dioxus::server::router(App))
121 };
12200123 #[cfg(feature = "fullstack-server")]
124 let router = {
125+ use jacquard::client::UnauthenticatedSession;
126+ let fetcher = Arc::new(fetch::CachedFetcher::new(OAuthClient::new(
127+ AuthStore::new(),
128+ ClientData::new_public(CONFIG.oauth.clone()),
129+ )));
130+ let blob_cache = Arc::new(BlobCache::new(Arc::new(
131+ UnauthenticatedSession::new_public(),
132+ )));
133 dioxus::server::router(App).layer(middleware::from_fn({
134+ let blob_cache = blob_cache.clone();
135 let fetcher = fetcher.clone();
0136 move |mut req: Request, next: Next| {
137+ let blob_cache = blob_cache.clone();
138 let fetcher = fetcher.clone();
0139 async move {
140+ req.extensions_mut().insert(blob_cache);
141 req.extensions_mut().insert(fetcher);
0142143 // And then return the response with `next.run()
144 Ok::<_, Infallible>(next.run(req).await)
···155 dioxus::launch(App);
156}
1570000158#[component]
159fn App() -> Element {
160+ use_context_provider(|| {
161+ fetch::CachedFetcher::new(OAuthClient::new(
162+ AuthStore::new(),
163+ ClientData::new_public(CONFIG.oauth.clone()),
164+ ))
165+ });
166+ use_context_provider(|| Signal::new(AuthState::default()));
167+168+ use_effect(move || {
169+ spawn(async move {
170+ if let Err(e) = auth::restore_session().await {
171+ dioxus_logger::tracing::warn!("Session restoration failed: {}", e);
172+ }
173+ });
174+ });
175176 // Register service worker on startup (only on web)
177 #[cfg(all(