clean up

Orual b95932ff 51c54057

+190 -179
+14 -14
crates/weaver-app/src/blobcache.rs
··· 8 IntoStatic, 9 bytes::Bytes, 10 prelude::*, 11 - smol_str::SmolStr, 12 types::{cid::Cid, collection::Collection, ident::AtIdentifier, nsid::Nsid, string::Rkey}, 13 xrpc::XrpcExt, 14 }; ··· 136 .build(), 137 ) 138 .await 139 - .map_err(|e| CapturedError::from_display(format!("Failed to fetch entry: {}", e)))?; 140 141 let record = resp 142 .into_output() 143 - .map_err(|e| CapturedError::from_display(format!("Failed to parse entry: {}", e)))?; 144 145 // Parse the entry 146 let entry: Entry = jacquard::from_data(&record.value).map_err(|e| { 147 - CapturedError::from_display(format!("Failed to deserialize entry: {}", e)) 148 })?; 149 150 // Find the image by name ··· 159 }) 160 .map(|img| img.image.blob().cid().clone().into_static()) 161 .ok_or_else(|| { 162 - CapturedError::from_display(format!("Image '{}' not found in entry", name)) 163 })?; 164 165 // Check cache first ··· 199 ) 200 .await 201 .map_err(|e| { 202 - CapturedError::from_display(format!("Failed to fetch PublishedBlob: {}", e)) 203 })?; 204 205 let record = resp.into_output().map_err(|e| { 206 - CapturedError::from_display(format!("Failed to parse PublishedBlob: {}", e)) 207 })?; 208 209 // Parse the PublishedBlob 210 let published: PublishedBlob = jacquard::from_data(&record.value).map_err(|e| { 211 - CapturedError::from_display(format!("Failed to deserialize PublishedBlob: {}", e)) 212 })?; 213 214 // Get CID from the upload blob ref ··· 237 image_name: &str, 238 ) -> Result<Bytes> { 239 // Try scoped cache key first: {notebook_key}_{image_name} 240 - let cache_key: SmolStr = format!("{}_{}", notebook_key, image_name).into(); 241 if let Some(bytes) = self.get_named(&cache_key) { 242 return Ok(bytes); 243 } ··· 248 .get_notebook_by_key(notebook_key) 249 .await? 250 .ok_or_else(|| { 251 - CapturedError::from_display(format!("Notebook '{}' not found", notebook_key)) 252 })?; 253 254 let (view, entry_refs) = notebook.as_ref(); 255 256 // Get the DID from the notebook URI for blob fetching 257 let notebook_did = jacquard::types::aturi::AtUri::new(view.uri.as_ref()) 258 - .map_err(|e| CapturedError::from_display(format!("Invalid notebook URI: {}", e)))? 259 .authority() 260 .clone() 261 .into_static(); ··· 278 for entry_ref in entry_refs { 279 // Parse the entry URI to get rkey 280 let entry_uri = jacquard::types::aturi::AtUri::new(entry_ref.uri.as_ref()) 281 - .map_err(|e| CapturedError::from_display(format!("Invalid entry URI: {}", e)))?; 282 let rkey = entry_uri 283 .rkey() 284 .ok_or_else(|| CapturedError::from_display("Entry URI missing rkey"))?; ··· 319 } 320 } 321 322 - Err(CapturedError::from_display(format!( 323 "Image '{}' not found in notebook '{}'", 324 image_name, notebook_key 325 - ))) 326 } 327 328 /// Insert bytes directly into cache (for pre-warming after upload)
··· 8 IntoStatic, 9 bytes::Bytes, 10 prelude::*, 11 + smol_str::{SmolStr, format_smolstr}, 12 types::{cid::Cid, collection::Collection, ident::AtIdentifier, nsid::Nsid, string::Rkey}, 13 xrpc::XrpcExt, 14 }; ··· 136 .build(), 137 ) 138 .await 139 + .map_err(|e| CapturedError::from_display(format_smolstr!("Failed to fetch entry: {}", e).as_str().to_string()))?; 140 141 let record = resp 142 .into_output() 143 + .map_err(|e| CapturedError::from_display(format_smolstr!("Failed to parse entry: {}", e).as_str().to_string()))?; 144 145 // Parse the entry 146 let entry: Entry = jacquard::from_data(&record.value).map_err(|e| { 147 + CapturedError::from_display(format_smolstr!("Failed to deserialize entry: {}", e).as_str().to_string()) 148 })?; 149 150 // Find the image by name ··· 159 }) 160 .map(|img| img.image.blob().cid().clone().into_static()) 161 .ok_or_else(|| { 162 + CapturedError::from_display(format_smolstr!("Image '{}' not found in entry", name).as_str().to_string()) 163 })?; 164 165 // Check cache first ··· 199 ) 200 .await 201 .map_err(|e| { 202 + CapturedError::from_display(format_smolstr!("Failed to fetch PublishedBlob: {}", e).as_str().to_string()) 203 })?; 204 205 let record = resp.into_output().map_err(|e| { 206 + CapturedError::from_display(format_smolstr!("Failed to parse PublishedBlob: {}", e).as_str().to_string()) 207 })?; 208 209 // Parse the PublishedBlob 210 let published: PublishedBlob = jacquard::from_data(&record.value).map_err(|e| { 211 + CapturedError::from_display(format_smolstr!("Failed to deserialize PublishedBlob: {}", e).as_str().to_string()) 212 })?; 213 214 // Get CID from the upload blob ref ··· 237 image_name: &str, 238 ) -> Result<Bytes> { 239 // Try scoped cache key first: {notebook_key}_{image_name} 240 + let cache_key = format_smolstr!("{}_{}", notebook_key, image_name); 241 if let Some(bytes) = self.get_named(&cache_key) { 242 return Ok(bytes); 243 } ··· 248 .get_notebook_by_key(notebook_key) 249 .await? 250 .ok_or_else(|| { 251 + CapturedError::from_display(format_smolstr!("Notebook '{}' not found", notebook_key).as_str().to_string()) 252 })?; 253 254 let (view, entry_refs) = notebook.as_ref(); 255 256 // Get the DID from the notebook URI for blob fetching 257 let notebook_did = jacquard::types::aturi::AtUri::new(view.uri.as_ref()) 258 + .map_err(|e| CapturedError::from_display(format_smolstr!("Invalid notebook URI: {}", e).as_str().to_string()))? 259 .authority() 260 .clone() 261 .into_static(); ··· 278 for entry_ref in entry_refs { 279 // Parse the entry URI to get rkey 280 let entry_uri = jacquard::types::aturi::AtUri::new(entry_ref.uri.as_ref()) 281 + .map_err(|e| CapturedError::from_display(format_smolstr!("Invalid entry URI: {}", e).as_str().to_string()))?; 282 let rkey = entry_uri 283 .rkey() 284 .ok_or_else(|| CapturedError::from_display("Entry URI missing rkey"))?; ··· 319 } 320 } 321 322 + Err(CapturedError::from_display(format_smolstr!( 323 "Image '{}' not found in notebook '{}'", 324 image_name, notebook_key 325 + ).as_str().to_string())) 326 } 327 328 /// Insert bytes directly into cache (for pre-warming after upload)
+19 -19
crates/weaver-app/src/components/collab/api.rs
··· 3 use crate::fetch::Fetcher; 4 use jacquard::IntoStatic; 5 use jacquard::prelude::*; 6 - use jacquard::types::collection::Collection; 7 - use jacquard::types::string::{AtUri, Cid, Datetime, Did, Nsid, RecordKey}; 8 use jacquard::types::uri::Uri; 9 use reqwest::Url; 10 use std::collections::HashSet; 11 use weaver_api::com_atproto::repo::list_records::ListRecords; 12 use weaver_api::com_atproto::repo::strong_ref::StrongRef; 13 use weaver_api::sh_weaver::collab::{accept::Accept, invite::Invite}; 14 - use weaver_api::sh_weaver::notebook::entry::Entry; 15 use weaver_common::WeaverError; 16 use weaver_common::constellation::GetBacklinksQuery; 17 18 const ACCEPT_NSID: &str = "sh.weaver.collab.accept"; 19 const CONSTELLATION_URL: &str = "https://constellation.microcosm.blue"; 20 21 /// An invite sent by the current user. ··· 71 let output = fetcher 72 .create_record(invite, None) 73 .await 74 - .map_err(|e| WeaverError::InvalidNotebook(format!("Failed to create invite: {}", e)))?; 75 76 Ok(output.uri.into_static()) 77 } ··· 91 let output = fetcher 92 .create_record(accept, None) 93 .await 94 - .map_err(|e| WeaverError::InvalidNotebook(format!("Failed to accept invite: {}", e)))?; 95 96 Ok(output.uri.into_static()) 97 } ··· 105 106 let request = ListRecords::new() 107 .repo(did) 108 - .collection(Nsid::raw(Invite::NSID)) 109 .limit(100) 110 .build(); 111 112 let response = fetcher 113 .send(request) 114 .await 115 - .map_err(|e| WeaverError::InvalidNotebook(format!("Failed to list invites: {}", e)))?; 116 117 let output = response.into_output().map_err(|e| { 118 - WeaverError::InvalidNotebook(format!("Failed to parse list response: {}", e)) 119 })?; 120 121 let mut invites = Vec::new(); ··· 147 // Query for sh.weaver.collab.accept records that reference this invite via .invite.uri 148 let query = GetBacklinksQuery { 149 subject: Uri::At(invite_uri.clone().into_static()), 150 - source: format!("{}:invite.uri", ACCEPT_NSID).into(), 151 cursor: None, 152 did: vec![], 153 limit: 1, ··· 176 .ok_or_else(|| WeaverError::InvalidNotebook("Not authenticated".into()))?; 177 178 let constellation_url = Url::parse(CONSTELLATION_URL) 179 - .map_err(|e| WeaverError::InvalidNotebook(format!("Invalid constellation URL: {}", e)))?; 180 181 // Query for sh.weaver.collab.invite records where .invitee = current user's DID 182 let query = GetBacklinksQuery { 183 subject: Uri::Did(did.clone()), 184 - source: format!("{}:invitee", Invite::NSID).into(), 185 cursor: None, 186 did: vec![], 187 limit: 100, ··· 192 .xrpc(constellation_url) 193 .send(&query) 194 .await 195 - .map_err(|e| WeaverError::InvalidNotebook(format!("Constellation query failed: {}", e)))?; 196 197 let output = response.into_output().map_err(|e| { 198 - WeaverError::InvalidNotebook(format!("Failed to parse constellation response: {}", e)) 199 })?; 200 201 // For each RecordId, fetch the actual record from the inviter's PDS ··· 205 let inviter_did = record_id.did.into_static(); 206 207 // Build the AT-URI for the invite record 208 - let uri_string = format!( 209 "at://{}/{}/{}", 210 inviter_did, 211 - Invite::NSID, 212 record_id.rkey.as_ref() 213 ); 214 let Ok(invite_uri) = AtUri::new(&uri_string) else { ··· 259 }; 260 261 let constellation_url = Url::parse(CONSTELLATION_URL) 262 - .map_err(|e| WeaverError::InvalidNotebook(format!("Invalid constellation URL: {}", e)))?; 263 264 // Query for all invite records that reference entries with this rkey 265 // We search for invites where resource.uri contains the rkey 266 // The source pattern matches the JSON path in the invite record 267 let query = GetBacklinksQuery { 268 subject: Uri::At(resource_uri.clone().into_static()), 269 - source: format!("{}:resource.uri", Invite::NSID).into(), 270 cursor: None, 271 did: vec![], 272 limit: 100, ··· 282 participants.insert(record_id.did.clone().into_static()); 283 284 // Now we need to fetch the invite to get the invitee 285 - let uri_string = format!( 286 "at://{}/{}/{}", 287 record_id.did, 288 - Invite::NSID, 289 record_id.rkey.as_ref() 290 ); 291 if let Ok(invite_uri) = AtUri::new(&uri_string) {
··· 3 use crate::fetch::Fetcher; 4 use jacquard::IntoStatic; 5 use jacquard::prelude::*; 6 + use jacquard::smol_str::format_smolstr; 7 + use jacquard::types::string::{AtUri, Cid, Datetime, Did, Nsid}; 8 use jacquard::types::uri::Uri; 9 use reqwest::Url; 10 use std::collections::HashSet; 11 use weaver_api::com_atproto::repo::list_records::ListRecords; 12 use weaver_api::com_atproto::repo::strong_ref::StrongRef; 13 use weaver_api::sh_weaver::collab::{accept::Accept, invite::Invite}; 14 use weaver_common::WeaverError; 15 use weaver_common::constellation::GetBacklinksQuery; 16 17 const ACCEPT_NSID: &str = "sh.weaver.collab.accept"; 18 + const INVITE_NSID: &str = "sh.weaver.collab.invite"; 19 const CONSTELLATION_URL: &str = "https://constellation.microcosm.blue"; 20 21 /// An invite sent by the current user. ··· 71 let output = fetcher 72 .create_record(invite, None) 73 .await 74 + .map_err(|e| WeaverError::InvalidNotebook(jacquard::smol_str::format_smolstr!("Failed to create invite: {}", e).into()))?; 75 76 Ok(output.uri.into_static()) 77 } ··· 91 let output = fetcher 92 .create_record(accept, None) 93 .await 94 + .map_err(|e| WeaverError::InvalidNotebook(jacquard::smol_str::format_smolstr!("Failed to accept invite: {}", e).into()))?; 95 96 Ok(output.uri.into_static()) 97 } ··· 105 106 let request = ListRecords::new() 107 .repo(did) 108 + .collection(Nsid::raw(INVITE_NSID)) 109 .limit(100) 110 .build(); 111 112 let response = fetcher 113 .send(request) 114 .await 115 + .map_err(|e| WeaverError::InvalidNotebook(jacquard::smol_str::format_smolstr!("Failed to list invites: {}", e).into()))?; 116 117 let output = response.into_output().map_err(|e| { 118 + WeaverError::InvalidNotebook(jacquard::smol_str::format_smolstr!("Failed to parse list response: {}", e).into()) 119 })?; 120 121 let mut invites = Vec::new(); ··· 147 // Query for sh.weaver.collab.accept records that reference this invite via .invite.uri 148 let query = GetBacklinksQuery { 149 subject: Uri::At(invite_uri.clone().into_static()), 150 + source: jacquard::smol_str::format_smolstr!("{}:invite.uri", ACCEPT_NSID).into(), 151 cursor: None, 152 did: vec![], 153 limit: 1, ··· 176 .ok_or_else(|| WeaverError::InvalidNotebook("Not authenticated".into()))?; 177 178 let constellation_url = Url::parse(CONSTELLATION_URL) 179 + .map_err(|e| WeaverError::InvalidNotebook(jacquard::smol_str::format_smolstr!("Invalid constellation URL: {}", e).into()))?; 180 181 // Query for sh.weaver.collab.invite records where .invitee = current user's DID 182 let query = GetBacklinksQuery { 183 subject: Uri::Did(did.clone()), 184 + source: jacquard::smol_str::format_smolstr!("{}:invitee", INVITE_NSID).into(), 185 cursor: None, 186 did: vec![], 187 limit: 100, ··· 192 .xrpc(constellation_url) 193 .send(&query) 194 .await 195 + .map_err(|e| WeaverError::InvalidNotebook(jacquard::smol_str::format_smolstr!("Constellation query failed: {}", e).into()))?; 196 197 let output = response.into_output().map_err(|e| { 198 + WeaverError::InvalidNotebook(jacquard::smol_str::format_smolstr!("Failed to parse constellation response: {}", e).into()) 199 })?; 200 201 // For each RecordId, fetch the actual record from the inviter's PDS ··· 205 let inviter_did = record_id.did.into_static(); 206 207 // Build the AT-URI for the invite record 208 + let uri_string = jacquard::smol_str::format_smolstr!( 209 "at://{}/{}/{}", 210 inviter_did, 211 + INVITE_NSID, 212 record_id.rkey.as_ref() 213 ); 214 let Ok(invite_uri) = AtUri::new(&uri_string) else { ··· 259 }; 260 261 let constellation_url = Url::parse(CONSTELLATION_URL) 262 + .map_err(|e| WeaverError::InvalidNotebook(jacquard::smol_str::format_smolstr!("Invalid constellation URL: {}", e).into()))?; 263 264 // Query for all invite records that reference entries with this rkey 265 // We search for invites where resource.uri contains the rkey 266 // The source pattern matches the JSON path in the invite record 267 let query = GetBacklinksQuery { 268 subject: Uri::At(resource_uri.clone().into_static()), 269 + source: jacquard::smol_str::format_smolstr!("{}:resource.uri", INVITE_NSID).into(), 270 cursor: None, 271 did: vec![], 272 limit: 100, ··· 282 participants.insert(record_id.did.clone().into_static()); 283 284 // Now we need to fetch the invite to get the invitee 285 + let uri_string = jacquard::smol_str::format_smolstr!( 286 "at://{}/{}/{}", 287 record_id.did, 288 + INVITE_NSID, 289 record_id.rkey.as_ref() 290 ); 291 if let Ok(invite_uri) = AtUri::new(&uri_string) {
+5 -4
crates/weaver-app/src/components/collab/invite_dialog.rs
··· 5 use crate::components::input::Input; 6 use crate::fetch::Fetcher; 7 use dioxus::prelude::*; 8 use jacquard::types::string::{AtUri, Cid, Handle}; 9 use jacquard::{IntoStatic, prelude::*}; 10 use weaver_api::com_atproto::repo::strong_ref::StrongRef; ··· 56 let handle = match Handle::new(&handle) { 57 Ok(h) => h, 58 Err(e) => { 59 - error.set(Some(format!("Invalid handle: {}", e))); 60 is_sending.set(false); 61 return; 62 } ··· 65 let invitee_did = match fetcher.resolve_handle(&handle).await { 66 Ok(did) => did, 67 Err(e) => { 68 - error.set(Some(format!("Could not resolve handle: {}", e))); 69 is_sending.set(false); 70 return; 71 } ··· 75 let cid = match Cid::new(resource_cid.as_bytes()) { 76 Ok(c) => c.into_static(), 77 Err(e) => { 78 - error.set(Some(format!("Invalid CID: {}", e))); 79 is_sending.set(false); 80 return; 81 } ··· 104 on_close.call(()); 105 } 106 Err(e) => { 107 - error.set(Some(format!("Failed to send invite: {}", e))); 108 } 109 } 110
··· 5 use crate::components::input::Input; 6 use crate::fetch::Fetcher; 7 use dioxus::prelude::*; 8 + use jacquard::smol_str::format_smolstr; 9 use jacquard::types::string::{AtUri, Cid, Handle}; 10 use jacquard::{IntoStatic, prelude::*}; 11 use weaver_api::com_atproto::repo::strong_ref::StrongRef; ··· 57 let handle = match Handle::new(&handle) { 58 Ok(h) => h, 59 Err(e) => { 60 + error.set(Some(format_smolstr!("Invalid handle: {}", e).into())); 61 is_sending.set(false); 62 return; 63 } ··· 66 let invitee_did = match fetcher.resolve_handle(&handle).await { 67 Ok(did) => did, 68 Err(e) => { 69 + error.set(Some(format_smolstr!("Could not resolve handle: {}", e).into())); 70 is_sending.set(false); 71 return; 72 } ··· 76 let cid = match Cid::new(resource_cid.as_bytes()) { 77 Ok(c) => c.into_static(), 78 Err(e) => { 79 + error.set(Some(format_smolstr!("Invalid CID: {}", e).into())); 80 is_sending.set(false); 81 return; 82 } ··· 105 on_close.call(()); 106 } 107 Err(e) => { 108 + error.set(Some(format_smolstr!("Failed to send invite: {}", e).into())); 109 } 110 } 111
+10 -9
crates/weaver-app/src/components/editor/sync.rs
··· 23 use jacquard::bytes::Bytes; 24 use jacquard::cowstr::ToCowStr; 25 use jacquard::prelude::*; 26 use jacquard::types::blob::MimeType; 27 use jacquard::types::collection::Collection; 28 use jacquard::types::ident::AtIdentifier; ··· 225 }; 226 227 // Build AT-URI pointing to actual draft record: at://{did}/sh.weaver.edit.draft/{rkey} 228 - let canonical_uri = format!("at://{}/{}/{}", did, DRAFT_NSID, rkey); 229 230 DocRef { 231 value: DocRefValue::DraftRef(Box::new(DraftRef { ··· 283 284 let query = GetBacklinksQuery { 285 subject: Uri::At(entry_uri.clone().into_static()), 286 - source: format!("{}:doc.value.entry.uri", ROOT_NSID).into(), 287 cursor: None, 288 did: vec![], 289 limit: 1, ··· 342 // Query for edit.root records from this DID that reference entry_uri 343 let query = GetBacklinksQuery { 344 subject: Uri::At(entry_uri.clone().into_static()), 345 - source: format!("{}:doc.value.entry.uri", ROOT_NSID).into(), 346 cursor: None, 347 did: all_dids.clone(), 348 limit: 10, ··· 388 389 let query = GetBacklinksQuery { 390 subject: Uri::At(draft_uri.clone().into_static()), 391 - source: format!("{}:doc.value.draft_key", ROOT_NSID).into(), 392 cursor: None, 393 did: vec![], 394 limit: 1, ··· 421 draft_key.to_string() 422 }; 423 424 - let uri_str = format!("at://{}/{}/{}", did, DRAFT_NSID, rkey); 425 // Safe to unwrap: we're constructing a valid AT-URI 426 AtUri::new(&uri_str).unwrap().into_static() 427 } ··· 564 loop { 565 let query = GetBacklinksQuery { 566 subject: Uri::At(root_uri.clone().into_static()), 567 - source: format!("{}:root.uri", DIFF_NSID).into(), 568 cursor: cursor.map(Into::into), 569 did: vec![], 570 limit: 100, ··· 977 let root_did = root_id.did.clone(); 978 979 // Build root URI to look up last seen diff 980 - let root_uri = AtUri::new(&format!( 981 "at://{}/{}/{}", 982 root_id.did, 983 ROOT_NSID, ··· 1063 after_rkey: Option<&str>, 1064 ) -> Result<Option<PdsEditState>, WeaverError> { 1065 // Build root URI 1066 - let root_uri = AtUri::new(&format!( 1067 "at://{}/{}/{}", 1068 root_id.did, 1069 ROOT_NSID, ··· 1131 } 1132 } 1133 1134 - let diff_uri = AtUri::new(&format!("at://{}/{}/{}", diff_id.did, DIFF_NSID, rkey_str)) 1135 .map_err(|e| WeaverError::InvalidNotebook(format!("Invalid diff URI: {}", e)))? 1136 .into_static(); 1137
··· 23 use jacquard::bytes::Bytes; 24 use jacquard::cowstr::ToCowStr; 25 use jacquard::prelude::*; 26 + use jacquard::smol_str::format_smolstr; 27 use jacquard::types::blob::MimeType; 28 use jacquard::types::collection::Collection; 29 use jacquard::types::ident::AtIdentifier; ··· 226 }; 227 228 // Build AT-URI pointing to actual draft record: at://{did}/sh.weaver.edit.draft/{rkey} 229 + let canonical_uri = format_smolstr!("at://{}/{}/{}", did, DRAFT_NSID, rkey); 230 231 DocRef { 232 value: DocRefValue::DraftRef(Box::new(DraftRef { ··· 284 285 let query = GetBacklinksQuery { 286 subject: Uri::At(entry_uri.clone().into_static()), 287 + source: format_smolstr!("{}:doc.value.entry.uri", ROOT_NSID).into(), 288 cursor: None, 289 did: vec![], 290 limit: 1, ··· 343 // Query for edit.root records from this DID that reference entry_uri 344 let query = GetBacklinksQuery { 345 subject: Uri::At(entry_uri.clone().into_static()), 346 + source: format_smolstr!("{}:doc.value.entry.uri", ROOT_NSID).into(), 347 cursor: None, 348 did: all_dids.clone(), 349 limit: 10, ··· 389 390 let query = GetBacklinksQuery { 391 subject: Uri::At(draft_uri.clone().into_static()), 392 + source: format_smolstr!("{}:doc.value.draft_key", ROOT_NSID).into(), 393 cursor: None, 394 did: vec![], 395 limit: 1, ··· 422 draft_key.to_string() 423 }; 424 425 + let uri_str = format_smolstr!("at://{}/{}/{}", did, DRAFT_NSID, rkey); 426 // Safe to unwrap: we're constructing a valid AT-URI 427 AtUri::new(&uri_str).unwrap().into_static() 428 } ··· 565 loop { 566 let query = GetBacklinksQuery { 567 subject: Uri::At(root_uri.clone().into_static()), 568 + source: format_smolstr!("{}:root.uri", DIFF_NSID).into(), 569 cursor: cursor.map(Into::into), 570 did: vec![], 571 limit: 100, ··· 978 let root_did = root_id.did.clone(); 979 980 // Build root URI to look up last seen diff 981 + let root_uri = AtUri::new(&format_smolstr!( 982 "at://{}/{}/{}", 983 root_id.did, 984 ROOT_NSID, ··· 1064 after_rkey: Option<&str>, 1065 ) -> Result<Option<PdsEditState>, WeaverError> { 1066 // Build root URI 1067 + let root_uri = AtUri::new(&format_smolstr!( 1068 "at://{}/{}/{}", 1069 root_id.did, 1070 ROOT_NSID, ··· 1132 } 1133 } 1134 1135 + let diff_uri = AtUri::new(&format_smolstr!("at://{}/{}/{}", diff_id.did, DIFF_NSID, rkey_str)) 1136 .map_err(|e| WeaverError::InvalidNotebook(format!("Invalid diff URI: {}", e)))? 1137 .into_static(); 1138
+11 -8
crates/weaver-app/src/components/editor/worker.rs
··· 15 use std::collections::HashMap; 16 use weaver_common::transport::PresenceSnapshot; 17 18 /// Input messages to the editor worker. 19 #[derive(Serialize, Deserialize, Debug, Clone)] 20 pub enum WorkerInput { ··· 224 if let Err(e) = new_doc.import(&snapshot) { 225 if let Err(send_err) = scope 226 .send(WorkerOutput::Error { 227 - message: format!("Failed to import snapshot: {e}"), 228 }) 229 .await 230 { ··· 271 Err(e) => { 272 if let Err(send_err) = scope 273 .send(WorkerOutput::Error { 274 - message: format!("Export failed: {e}"), 275 }) 276 .await 277 { ··· 321 Err(e) => { 322 if let Err(send_err) = scope 323 .send(WorkerOutput::Error { 324 - message: format!("Failed to spawn CollabNode: {e}"), 325 }) 326 .await 327 { ··· 450 Err(e) => { 451 if let Err(send_err) = scope 452 .send(WorkerOutput::Error { 453 - message: format!("Failed to join session: {e}"), 454 }) 455 .await 456 { ··· 551 if let Err(e) = new_doc.import(&snapshot) { 552 if let Err(send_err) = scope 553 .send(WorkerOutput::Error { 554 - message: format!("Failed to import snapshot: {e}"), 555 }) 556 .await 557 { ··· 584 let snapshot_bytes = match doc.export(loro::ExportMode::Snapshot) { 585 Ok(bytes) => bytes, 586 Err(e) => { 587 - if let Err(send_err) = scope.send(WorkerOutput::Error { message: format!("Export failed: {e}") }).await { 588 tracing::error!("Failed to send Error to coordinator: {send_err}"); 589 } 590 continue; ··· 728 let at_uri = match AtUri::new_owned(uri_str.clone()) { 729 Ok(u) => u, 730 Err(e) => { 731 - errors.insert(uri_str, format!("Invalid AT URI: {e}")); 732 continue; 733 } 734 }; ··· 770 results.insert(uri_str, html); 771 } 772 Err(e) => { 773 - errors.insert(uri_str, format!("{:?}", e)); 774 } 775 } 776 }
··· 15 use std::collections::HashMap; 16 use weaver_common::transport::PresenceSnapshot; 17 18 + #[cfg(all(target_family = "wasm", target_os = "unknown"))] 19 + use jacquard::smol_str::format_smolstr; 20 + 21 /// Input messages to the editor worker. 22 #[derive(Serialize, Deserialize, Debug, Clone)] 23 pub enum WorkerInput { ··· 227 if let Err(e) = new_doc.import(&snapshot) { 228 if let Err(send_err) = scope 229 .send(WorkerOutput::Error { 230 + message: format_smolstr!("Failed to import snapshot: {e}").to_string(), 231 }) 232 .await 233 { ··· 274 Err(e) => { 275 if let Err(send_err) = scope 276 .send(WorkerOutput::Error { 277 + message: format_smolstr!("Export failed: {e}").to_string(), 278 }) 279 .await 280 { ··· 324 Err(e) => { 325 if let Err(send_err) = scope 326 .send(WorkerOutput::Error { 327 + message: format_smolstr!("Failed to spawn CollabNode: {e}").to_string(), 328 }) 329 .await 330 { ··· 453 Err(e) => { 454 if let Err(send_err) = scope 455 .send(WorkerOutput::Error { 456 + message: format_smolstr!("Failed to join session: {e}").to_string(), 457 }) 458 .await 459 { ··· 554 if let Err(e) = new_doc.import(&snapshot) { 555 if let Err(send_err) = scope 556 .send(WorkerOutput::Error { 557 + message: format_smolstr!("Failed to import snapshot: {e}").to_string(), 558 }) 559 .await 560 { ··· 587 let snapshot_bytes = match doc.export(loro::ExportMode::Snapshot) { 588 Ok(bytes) => bytes, 589 Err(e) => { 590 + if let Err(send_err) = scope.send(WorkerOutput::Error { message: format_smolstr!("Export failed: {e}").to_string() }).await { 591 tracing::error!("Failed to send Error to coordinator: {send_err}"); 592 } 593 continue; ··· 731 let at_uri = match AtUri::new_owned(uri_str.clone()) { 732 Ok(u) => u, 733 Err(e) => { 734 + errors.insert(uri_str, format_smolstr!("Invalid AT URI: {e}").to_string()); 735 continue; 736 } 737 }; ··· 773 results.insert(uri_str, html); 774 } 775 Err(e) => { 776 + errors.insert(uri_str, format_smolstr!("{:?}", e).to_string()); 777 } 778 } 779 }
+3 -1
crates/weaver-app/src/components/entry.rs
··· 145 if cleaned.len() <= max_len { 146 cleaned 147 } else { 148 - format!("{}...", &cleaned[..max_len - 3]) 149 } 150 } 151
··· 145 if cleaned.len() <= max_len { 146 cleaned 147 } else { 148 + // Use char boundary-safe truncation to avoid panic on multibyte chars 149 + let truncated: String = cleaned.chars().take(max_len - 3).collect(); 150 + format!("{}...", truncated) 151 } 152 } 153
+3 -2
crates/weaver-app/src/components/record_editor.rs
··· 1350 if let Some(new_collection_str) = data.type_discriminator() { 1351 let new_collection = Nsid::new(new_collection_str).ok(); 1352 if let Some(new_collection) = new_collection { 1353 - // Create new record 1354 let create_req = CreateRecord::new() 1355 .repo(AtIdentifier::Did(did.clone())) 1356 .collection(new_collection) ··· 1360 match fetcher.send(create_req).await { 1361 Ok(response) => { 1362 if let Ok(create_output) = response.into_output() { 1363 - // Delete old record 1364 if let (Some(old_collection_str), Some(old_rkey)) = (uri.collection(), uri.rkey()) { 1365 let old_collection = Nsid::new(old_collection_str.as_str()).ok(); 1366 if let Some(old_collection) = old_collection {
··· 1350 if let Some(new_collection_str) = data.type_discriminator() { 1351 let new_collection = Nsid::new(new_collection_str).ok(); 1352 if let Some(new_collection) = new_collection { 1353 + // Create new record first - if this fails, user keeps their old record 1354 + // If delete fails after, user has duplicates (recoverable) rather than data loss 1355 let create_req = CreateRecord::new() 1356 .repo(AtIdentifier::Did(did.clone())) 1357 .collection(new_collection) ··· 1361 match fetcher.send(create_req).await { 1362 Ok(response) => { 1363 if let Ok(create_output) = response.into_output() { 1364 + // Delete old record after successful create 1365 if let (Some(old_collection_str), Some(old_rkey)) = (uri.collection(), uri.rkey()) { 1366 let old_collection = Nsid::new(old_collection_str.as_str()).ok(); 1367 if let Some(old_collection) = old_collection {
+2 -2
crates/weaver-app/src/data.rs
··· 18 #[allow(unused_imports)] 19 use jacquard::{ 20 prelude::IdentityResolver, 21 - smol_str::SmolStr, 22 types::{cid::Cid, string::AtIdentifier}, 23 }; 24 #[allow(unused_imports)] ··· 1521 ) -> Result<()> { 1522 let cid = Cid::new_owned(cid.as_bytes())?; 1523 let cache_key = match (&notebook, &name) { 1524 - (Some(nb), Some(n)) => Some(SmolStr::new(format!("{}_{}", nb, n))), 1525 (None, Some(n)) => Some(n.clone()), 1526 _ => None, 1527 };
··· 18 #[allow(unused_imports)] 19 use jacquard::{ 20 prelude::IdentityResolver, 21 + smol_str::{SmolStr, format_smolstr}, 22 types::{cid::Cid, string::AtIdentifier}, 23 }; 24 #[allow(unused_imports)] ··· 1521 ) -> Result<()> { 1522 let cid = Cid::new_owned(cid.as_bytes())?; 1523 let cache_key = match (&notebook, &name) { 1524 + (Some(nb), Some(n)) => Some(format_smolstr!("{}_{}", nb, n)), 1525 (None, Some(n)) => Some(n.clone()), 1526 _ => None, 1527 };
+7 -7
crates/weaver-app/src/fetch.rs
··· 24 use jacquard::types::string::Nsid; 25 use jacquard::xrpc::XrpcResponse; 26 use jacquard::xrpc::*; 27 - use jacquard::{smol_str::SmolStr, types::ident::AtIdentifier}; 28 use serde::{Deserialize, Serialize}; 29 use std::future::Future; 30 use std::{sync::Arc, time::Duration}; ··· 528 529 for ufos_record in records { 530 // Construct URI 531 - let uri_str = format!( 532 "at://{}/{}/{}", 533 ufos_record.did, ufos_record.collection, ufos_record.rkey 534 ); 535 let uri = AtUri::new_owned(uri_str) 536 - .map_err(|e| dioxus::CapturedError::from_display(format!("Invalid URI: {}", e)))?; 537 match client.view_notebook(&uri).await { 538 Ok((notebook, entries)) => { 539 let ident = uri.authority().clone().into_static(); ··· 922 // Try to find notebook context via constellation 923 let entry_uri = entry_view.uri.clone(); 924 let at_uri = AtUri::new(entry_uri.as_ref()).map_err(|e| { 925 - dioxus::CapturedError::from_display(format!("Invalid entry URI: {}", e)) 926 })?; 927 928 let (total, first_notebook) = client ··· 934 let notebook_context = if total == 1 { 935 if let Some(notebook_id) = first_notebook { 936 // Construct notebook URI from RecordId 937 - let notebook_uri_str = format!( 938 "at://{}/{}/{}", 939 notebook_id.did.as_str(), 940 notebook_id.collection.as_str(), 941 notebook_id.rkey.0.as_str() 942 ); 943 let notebook_uri = AtUri::new_owned(notebook_uri_str).map_err(|e| { 944 - dioxus::CapturedError::from_display(format!("Invalid notebook URI: {}", e)) 945 })?; 946 947 // Fetch notebook and find entry position ··· 1032 // Check if entry is in multiple notebooks - if so, clear prev/next 1033 let entry_uri = book_entry_view.entry.uri.clone(); 1034 let at_uri = AtUri::new(entry_uri.as_ref()).map_err(|e| { 1035 - dioxus::CapturedError::from_display(format!("Invalid entry URI: {}", e)) 1036 })?; 1037 1038 let (total, _) = client
··· 24 use jacquard::types::string::Nsid; 25 use jacquard::xrpc::XrpcResponse; 26 use jacquard::xrpc::*; 27 + use jacquard::{smol_str::{SmolStr, format_smolstr}, types::ident::AtIdentifier}; 28 use serde::{Deserialize, Serialize}; 29 use std::future::Future; 30 use std::{sync::Arc, time::Duration}; ··· 528 529 for ufos_record in records { 530 // Construct URI 531 + let uri_str = format_smolstr!( 532 "at://{}/{}/{}", 533 ufos_record.did, ufos_record.collection, ufos_record.rkey 534 ); 535 let uri = AtUri::new_owned(uri_str) 536 + .map_err(|e| dioxus::CapturedError::from_display(format_smolstr!("Invalid URI: {}", e).as_str()))?; 537 match client.view_notebook(&uri).await { 538 Ok((notebook, entries)) => { 539 let ident = uri.authority().clone().into_static(); ··· 922 // Try to find notebook context via constellation 923 let entry_uri = entry_view.uri.clone(); 924 let at_uri = AtUri::new(entry_uri.as_ref()).map_err(|e| { 925 + dioxus::CapturedError::from_display(format_smolstr!("Invalid entry URI: {}", e).as_str()) 926 })?; 927 928 let (total, first_notebook) = client ··· 934 let notebook_context = if total == 1 { 935 if let Some(notebook_id) = first_notebook { 936 // Construct notebook URI from RecordId 937 + let notebook_uri_str = format_smolstr!( 938 "at://{}/{}/{}", 939 notebook_id.did.as_str(), 940 notebook_id.collection.as_str(), 941 notebook_id.rkey.0.as_str() 942 ); 943 let notebook_uri = AtUri::new_owned(notebook_uri_str).map_err(|e| { 944 + dioxus::CapturedError::from_display(format_smolstr!("Invalid notebook URI: {}", e).as_str()) 945 })?; 946 947 // Fetch notebook and find entry position ··· 1032 // Check if entry is in multiple notebooks - if so, clear prev/next 1033 let entry_uri = book_entry_view.entry.uri.clone(); 1034 let at_uri = AtUri::new(entry_uri.as_ref()).map_err(|e| { 1035 + dioxus::CapturedError::from_display(format_smolstr!("Invalid entry URI: {}", e).as_str()) 1036 })?; 1037 1038 let (total, _) = client
+13 -11
crates/weaver-app/src/og/mod.rs
··· 5 6 use crate::cache_impl::{Cache, new_cache}; 7 use askama::Template; 8 use std::sync::OnceLock; 9 use std::time::Duration; 10 11 /// Cache for generated OG images 12 /// Key: "{ident}/{book}/{entry}/{cid}" - includes CID for invalidation 13 - static OG_CACHE: OnceLock<Cache<String, Vec<u8>>> = OnceLock::new(); 14 15 - fn get_cache() -> &'static Cache<String, Vec<u8>> { 16 OG_CACHE.get_or_init(|| { 17 // Cache up to 1000 images for 1 hour 18 new_cache(1000, Duration::from_secs(3600)) ··· 20 } 21 22 /// Generate cache key from entry identifiers 23 - pub fn cache_key(ident: &str, book: &str, entry: &str, cid: &str) -> String { 24 - format!("{}/{}/{}/{}", ident, book, entry, cid) 25 } 26 27 /// Try to get a cached OG image 28 - pub fn get_cached(key: &str) -> Option<Vec<u8>> { 29 - get_cache().get(&key.to_string()) 30 } 31 32 /// Store an OG image in the cache 33 - pub fn cache_image(key: String, image: Vec<u8>) { 34 get_cache().insert(key, image); 35 } 36 ··· 224 } 225 226 /// Generate cache key for notebook OG images 227 - pub fn notebook_cache_key(ident: &str, book: &str, cid: &str) -> String { 228 - format!("notebook/{}/{}/{}", ident, book, cid) 229 } 230 231 /// Generate cache key for profile OG images 232 - pub fn profile_cache_key(ident: &str, cid: &str) -> String { 233 - format!("profile/{}/{}", ident, cid) 234 } 235 236 /// Generate a notebook index OG image
··· 5 6 use crate::cache_impl::{Cache, new_cache}; 7 use askama::Template; 8 + use jacquard::smol_str::SmolStr; 9 + use jacquard::smol_str::format_smolstr; 10 use std::sync::OnceLock; 11 use std::time::Duration; 12 13 /// Cache for generated OG images 14 /// Key: "{ident}/{book}/{entry}/{cid}" - includes CID for invalidation 15 + static OG_CACHE: OnceLock<Cache<SmolStr, Vec<u8>>> = OnceLock::new(); 16 17 + fn get_cache() -> &'static Cache<SmolStr, Vec<u8>> { 18 OG_CACHE.get_or_init(|| { 19 // Cache up to 1000 images for 1 hour 20 new_cache(1000, Duration::from_secs(3600)) ··· 22 } 23 24 /// Generate cache key from entry identifiers 25 + pub fn cache_key(ident: &str, book: &str, entry: &str, cid: &str) -> SmolStr { 26 + format_smolstr!("{}/{}/{}/{}", ident, book, entry, cid) 27 } 28 29 /// Try to get a cached OG image 30 + pub fn get_cached(key: &SmolStr) -> Option<Vec<u8>> { 31 + get_cache().get(key) 32 } 33 34 /// Store an OG image in the cache 35 + pub fn cache_image(key: SmolStr, image: Vec<u8>) { 36 get_cache().insert(key, image); 37 } 38 ··· 226 } 227 228 /// Generate cache key for notebook OG images 229 + pub fn notebook_cache_key(ident: &str, book: &str, cid: &str) -> SmolStr { 230 + format_smolstr!("notebook/{}/{}/{}", ident, book, cid) 231 } 232 233 /// Generate cache key for profile OG images 234 + pub fn profile_cache_key(ident: &str, cid: &str) -> SmolStr { 235 + format_smolstr!("profile/{}/{}", ident, cid) 236 } 237 238 /// Generate a notebook index OG image
+33 -35
crates/weaver-app/src/og/server.rs
··· 13 #[cfg(all(feature = "fullstack-server", feature = "server"))] 14 use std::sync::Arc; 15 16 // Route: /og/{ident}/{book_title}/{entry_title} - OpenGraph image for entry 17 #[cfg(all(feature = "fullstack-server", feature = "server"))] 18 #[get("/og/{ident}/{book_title}/{entry_title}", fetcher: Extension<Arc<fetch::Fetcher>>)] ··· 74 75 // Use book_title from URL - it's the notebook slug/title 76 // TODO: Could fetch actual notebook record to get display title 77 - let notebook_title_str: String = book_title.to_string(); 78 79 let author_handle = book_entry 80 .entry 81 .authors 82 .first() 83 .map(|a| match &a.record.inner { 84 - ProfileDataViewInner::ProfileView(p) => p.handle.as_ref().to_string(), 85 - ProfileDataViewInner::ProfileViewDetailed(p) => p.handle.as_ref().to_string(), 86 - ProfileDataViewInner::TangledProfileView(p) => p.handle.as_ref().to_string(), 87 - _ => "unknown".to_string(), 88 }) 89 - .unwrap_or_else(|| "unknown".to_string()); 90 91 // Check for hero image in embeds 92 let hero_image_data = if let Some(ref embeds) = entry.embeds { ··· 266 .authors 267 .first() 268 .map(|a| match &a.record.inner { 269 - ProfileDataViewInner::ProfileView(p) => p.handle.as_ref().to_string(), 270 - ProfileDataViewInner::ProfileViewDetailed(p) => p.handle.as_ref().to_string(), 271 - ProfileDataViewInner::TangledProfileView(p) => p.handle.as_ref().to_string(), 272 - _ => "unknown".to_string(), 273 }) 274 - .unwrap_or_else(|| "unknown".to_string()); 275 276 // Fetch entries to get entry titles and count 277 let entries_result = fetcher ··· 362 ProfileDataViewInner::ProfileView(p) => ( 363 p.display_name 364 .as_ref() 365 - .map(|n| n.as_ref().to_string()) 366 .unwrap_or_default(), 367 - p.handle.as_ref().to_string(), 368 p.description 369 .as_ref() 370 - .map(|d| d.as_ref().to_string()) 371 .unwrap_or_default(), 372 - p.avatar.as_ref().map(|u| u.as_ref().to_string()), 373 - None::<String>, 374 - p.did.as_ref().to_string(), 375 ), 376 ProfileDataViewInner::ProfileViewDetailed(p) => ( 377 p.display_name 378 .as_ref() 379 - .map(|n| n.as_ref().to_string()) 380 .unwrap_or_default(), 381 - p.handle.as_ref().to_string(), 382 p.description 383 .as_ref() 384 - .map(|d| d.as_ref().to_string()) 385 .unwrap_or_default(), 386 - p.avatar.as_ref().map(|u| u.as_ref().to_string()), 387 - p.banner.as_ref().map(|u| u.as_ref().to_string()), 388 - p.did.as_ref().to_string(), 389 - ), 390 - ProfileDataViewInner::TangledProfileView(p) => ( 391 - String::new(), 392 - p.handle.as_ref().to_string(), 393 - String::new(), 394 - None, 395 - None, 396 - p.did.as_ref().to_string(), 397 ), 398 _ => return Ok((StatusCode::NOT_FOUND, "Profile type not supported").into_response()), 399 }; 400 ··· 418 let notebook_count = notebooks_result.map(|n| n.len()).unwrap_or(0); 419 420 // Fetch avatar as base64 if available 421 - let avatar_data = if let Some(ref url) = avatar_url { 422 match reqwest::get(url).await { 423 Ok(response) if response.status().is_success() => { 424 let content_type = response ··· 426 .get("content-type") 427 .and_then(|v| v.to_str().ok()) 428 .unwrap_or("image/jpeg") 429 - .to_string(); 430 match response.bytes().await { 431 Ok(bytes) => { 432 use base64::Engine; ··· 443 }; 444 445 // Check for banner and generate appropriate template 446 - let png_bytes = if let Some(ref banner_url) = banner_url { 447 // Fetch banner image 448 let banner_data = match reqwest::get(banner_url).await { 449 Ok(response) if response.status().is_success() => { ··· 452 .get("content-type") 453 .and_then(|v| v.to_str().ok()) 454 .unwrap_or("image/jpeg") 455 - .to_string(); 456 match response.bytes().await { 457 Ok(bytes) => { 458 use base64::Engine;
··· 13 #[cfg(all(feature = "fullstack-server", feature = "server"))] 14 use std::sync::Arc; 15 16 + #[cfg(all(feature = "fullstack-server", feature = "server"))] 17 + use jacquard::smol_str::ToSmolStr; 18 + 19 // Route: /og/{ident}/{book_title}/{entry_title} - OpenGraph image for entry 20 #[cfg(all(feature = "fullstack-server", feature = "server"))] 21 #[get("/og/{ident}/{book_title}/{entry_title}", fetcher: Extension<Arc<fetch::Fetcher>>)] ··· 77 78 // Use book_title from URL - it's the notebook slug/title 79 // TODO: Could fetch actual notebook record to get display title 80 + let notebook_title_str: &str = book_title.as_ref(); 81 82 let author_handle = book_entry 83 .entry 84 .authors 85 .first() 86 .map(|a| match &a.record.inner { 87 + ProfileDataViewInner::ProfileView(p) => p.handle.as_ref(), 88 + ProfileDataViewInner::ProfileViewDetailed(p) => p.handle.as_ref(), 89 + ProfileDataViewInner::TangledProfileView(p) => p.handle.as_ref(), 90 + _ => "unknown", 91 }) 92 + .unwrap_or("unknown"); 93 94 // Check for hero image in embeds 95 let hero_image_data = if let Some(ref embeds) = entry.embeds { ··· 269 .authors 270 .first() 271 .map(|a| match &a.record.inner { 272 + ProfileDataViewInner::ProfileView(p) => p.handle.as_ref(), 273 + ProfileDataViewInner::ProfileViewDetailed(p) => p.handle.as_ref(), 274 + ProfileDataViewInner::TangledProfileView(p) => p.handle.as_ref(), 275 + _ => "unknown", 276 }) 277 + .unwrap_or("unknown"); 278 279 // Fetch entries to get entry titles and count 280 let entries_result = fetcher ··· 365 ProfileDataViewInner::ProfileView(p) => ( 366 p.display_name 367 .as_ref() 368 + .map(|n| n.as_ref()) 369 .unwrap_or_default(), 370 + p.handle.as_ref(), 371 p.description 372 .as_ref() 373 + .map(|d| d.as_ref()) 374 .unwrap_or_default(), 375 + p.avatar.as_ref().map(|u| u.as_ref()), 376 + None::<&str>, 377 + p.did.as_ref(), 378 ), 379 ProfileDataViewInner::ProfileViewDetailed(p) => ( 380 p.display_name 381 .as_ref() 382 + .map(|n| n.as_ref()) 383 .unwrap_or_default(), 384 + p.handle.as_ref(), 385 p.description 386 .as_ref() 387 + .map(|d| d.as_ref()) 388 .unwrap_or_default(), 389 + p.avatar.as_ref().map(|u| u.as_ref()), 390 + p.banner.as_ref().map(|u| u.as_ref()), 391 + p.did.as_ref(), 392 ), 393 + ProfileDataViewInner::TangledProfileView(p) => { 394 + ("", p.handle.as_ref(), "", None, None, p.did.as_ref()) 395 + } 396 _ => return Ok((StatusCode::NOT_FOUND, "Profile type not supported").into_response()), 397 }; 398 ··· 416 let notebook_count = notebooks_result.map(|n| n.len()).unwrap_or(0); 417 418 // Fetch avatar as base64 if available 419 + let avatar_data = if let Some(url) = avatar_url { 420 match reqwest::get(url).await { 421 Ok(response) if response.status().is_success() => { 422 let content_type = response ··· 424 .get("content-type") 425 .and_then(|v| v.to_str().ok()) 426 .unwrap_or("image/jpeg") 427 + .to_smolstr(); 428 match response.bytes().await { 429 Ok(bytes) => { 430 use base64::Engine; ··· 441 }; 442 443 // Check for banner and generate appropriate template 444 + let png_bytes = if let Some(banner_url) = banner_url { 445 // Fetch banner image 446 let banner_data = match reqwest::get(banner_url).await { 447 Ok(response) if response.status().is_success() => { ··· 450 .get("content-type") 451 .and_then(|v| v.to_str().ok()) 452 .unwrap_or("image/jpeg") 453 + .to_smolstr(); 454 match response.bytes().await { 455 Ok(bytes) => { 456 use base64::Engine;
+12 -11
crates/weaver-app/src/record_utils.rs
··· 1 use dioxus::prelude::*; 2 use jacquard::bytes::Bytes; 3 use jacquard::common::{Data, IntoStatic}; 4 use jacquard::types::LexiconStringType; 5 use jacquard::types::string::AtprotoStr; 6 use jacquard_lexicon::validation::{ ··· 158 match string_type { 159 LexiconStringType::Datetime => Datetime::from_str(text) 160 .map(AtprotoStr::Datetime) 161 - .map_err(|e| format!("Invalid datetime: {}", e)), 162 LexiconStringType::Did => Did::new(text) 163 .map(|v| AtprotoStr::Did(v.into_static())) 164 - .map_err(|e| format!("Invalid DID: {}", e)), 165 LexiconStringType::Handle => Handle::new(text) 166 .map(|v| AtprotoStr::Handle(v.into_static())) 167 - .map_err(|e| format!("Invalid handle: {}", e)), 168 LexiconStringType::AtUri => AtUri::new(text) 169 .map(|v| AtprotoStr::AtUri(v.into_static())) 170 - .map_err(|e| format!("Invalid AT-URI: {}", e)), 171 LexiconStringType::AtIdentifier => AtIdentifier::new(text) 172 .map(|v| AtprotoStr::AtIdentifier(v.into_static())) 173 - .map_err(|e| format!("Invalid identifier: {}", e)), 174 LexiconStringType::Nsid => Nsid::new(text) 175 .map(|v| AtprotoStr::Nsid(v.into_static())) 176 - .map_err(|e| format!("Invalid NSID: {}", e)), 177 LexiconStringType::Tid => Tid::new(text) 178 .map(|v| AtprotoStr::Tid(v.into_static())) 179 - .map_err(|e| format!("Invalid TID: {}", e)), 180 LexiconStringType::RecordKey => Rkey::new(text) 181 .map(|rk| AtprotoStr::RecordKey(RecordKey::from(rk))) 182 - .map_err(|e| format!("Invalid record key: {}", e)), 183 LexiconStringType::Cid => Cid::new(text.as_bytes()) 184 .map(|v| AtprotoStr::Cid(v.into_static())) 185 - .map_err(|_| "Invalid CID".to_string()), 186 LexiconStringType::Language => Language::new(text) 187 .map(AtprotoStr::Language) 188 - .map_err(|e| format!("Invalid language: {}", e)), 189 LexiconStringType::Uri(_) => Uri::new(text) 190 .map(|u| AtprotoStr::Uri(u.into_static())) 191 - .map_err(|e| format!("Invalid URI: {}", e)), 192 LexiconStringType::String => { 193 // Plain strings: use smart inference 194 use jacquard::types::value::parsing;
··· 1 use dioxus::prelude::*; 2 use jacquard::bytes::Bytes; 3 use jacquard::common::{Data, IntoStatic}; 4 + use jacquard::smol_str::{SmolStr, format_smolstr}; 5 use jacquard::types::LexiconStringType; 6 use jacquard::types::string::AtprotoStr; 7 use jacquard_lexicon::validation::{ ··· 159 match string_type { 160 LexiconStringType::Datetime => Datetime::from_str(text) 161 .map(AtprotoStr::Datetime) 162 + .map_err(|e| format_smolstr!("Invalid datetime: {}", e).to_string()), 163 LexiconStringType::Did => Did::new(text) 164 .map(|v| AtprotoStr::Did(v.into_static())) 165 + .map_err(|e| format_smolstr!("Invalid DID: {}", e).to_string()), 166 LexiconStringType::Handle => Handle::new(text) 167 .map(|v| AtprotoStr::Handle(v.into_static())) 168 + .map_err(|e| format_smolstr!("Invalid handle: {}", e).to_string()), 169 LexiconStringType::AtUri => AtUri::new(text) 170 .map(|v| AtprotoStr::AtUri(v.into_static())) 171 + .map_err(|e| format_smolstr!("Invalid AT-URI: {}", e).to_string()), 172 LexiconStringType::AtIdentifier => AtIdentifier::new(text) 173 .map(|v| AtprotoStr::AtIdentifier(v.into_static())) 174 + .map_err(|e| format_smolstr!("Invalid identifier: {}", e).to_string()), 175 LexiconStringType::Nsid => Nsid::new(text) 176 .map(|v| AtprotoStr::Nsid(v.into_static())) 177 + .map_err(|e| format_smolstr!("Invalid NSID: {}", e).to_string()), 178 LexiconStringType::Tid => Tid::new(text) 179 .map(|v| AtprotoStr::Tid(v.into_static())) 180 + .map_err(|e| format_smolstr!("Invalid TID: {}", e).to_string()), 181 LexiconStringType::RecordKey => Rkey::new(text) 182 .map(|rk| AtprotoStr::RecordKey(RecordKey::from(rk))) 183 + .map_err(|e| format_smolstr!("Invalid record key: {}", e).to_string()), 184 LexiconStringType::Cid => Cid::new(text.as_bytes()) 185 .map(|v| AtprotoStr::Cid(v.into_static())) 186 + .map_err(|_| SmolStr::new_inline("Invalid CID").to_string()), 187 LexiconStringType::Language => Language::new(text) 188 .map(AtprotoStr::Language) 189 + .map_err(|e| format_smolstr!("Invalid language: {}", e).to_string()), 190 LexiconStringType::Uri(_) => Uri::new(text) 191 .map(|u| AtprotoStr::Uri(u.into_static())) 192 + .map_err(|e| format_smolstr!("Invalid URI: {}", e).to_string()), 193 LexiconStringType::String => { 194 // Plain strings: use smart inference 195 use jacquard::types::value::parsing;
+9 -7
crates/weaver-app/src/service_worker.rs
··· 6 use wasm_bindgen_futures::JsFuture; 7 #[cfg(all(target_family = "wasm", target_os = "unknown"))] 8 use web_sys::{RegistrationOptions, ServiceWorkerContainer, Window}; 9 10 #[cfg(all(target_family = "wasm", target_os = "unknown"))] 11 pub async fn register_service_worker() -> Result<(), JsValue> { ··· 54 let cid = image.image.blob().cid(); 55 56 if let Some(name) = &image.name { 57 - let blob_url = format!( 58 "{}xrpc/com.atproto.sync.getBlob?did={}&cid={}", 59 pds_url.as_str(), 60 did.as_ref(), ··· 108 let cid = image.image.blob().cid(); 109 110 if let Some(name) = &image.name { 111 - let blob_url = format!( 112 "{}xrpc/com.atproto.sync.getBlob?did={}&cid={}", 113 pds_url.as_str(), 114 did.as_ref(), ··· 131 #[cfg(all(target_family = "wasm", target_os = "unknown"))] 132 fn send_blob_mappings( 133 notebook: &str, 134 - mappings: std::collections::HashMap<String, String>, 135 ) -> Result<(), JsValue> { 136 let window = web_sys::window().ok_or_else(|| JsValue::from_str("no window"))?; 137 let navigator = window.navigator(); ··· 150 // Convert HashMap to JS Object 151 let blobs_obj = js_sys::Object::new(); 152 for (name, url) in mappings { 153 - js_sys::Reflect::set(&blobs_obj, &name.into(), &url.into())?; 154 } 155 js_sys::Reflect::set(&msg, &"blobs".into(), &blobs_obj)?; 156 ··· 164 fn send_blob_rkey_mappings( 165 rkey: &str, 166 ident: &str, 167 - mappings: std::collections::HashMap<String, String>, 168 ) -> Result<(), JsValue> { 169 let window = web_sys::window().ok_or_else(|| JsValue::from_str("no window"))?; 170 let navigator = window.navigator(); ··· 184 // Convert HashMap to JS Object 185 let blobs_obj = js_sys::Object::new(); 186 for (name, url) in mappings { 187 - js_sys::Reflect::set(&blobs_obj, &name.into(), &url.into())?; 188 } 189 js_sys::Reflect::set(&msg, &"blobs".into(), &blobs_obj)?; 190 ··· 204 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))] 205 pub fn send_blob_mappings( 206 _notebook: &str, 207 - _mappings: std::collections::HashMap<String, String>, 208 ) -> Result<(), String> { 209 Ok(()) 210 }
··· 6 use wasm_bindgen_futures::JsFuture; 7 #[cfg(all(target_family = "wasm", target_os = "unknown"))] 8 use web_sys::{RegistrationOptions, ServiceWorkerContainer, Window}; 9 + #[cfg(all(target_family = "wasm", target_os = "unknown"))] 10 + use jacquard::smol_str::format_smolstr; 11 12 #[cfg(all(target_family = "wasm", target_os = "unknown"))] 13 pub async fn register_service_worker() -> Result<(), JsValue> { ··· 56 let cid = image.image.blob().cid(); 57 58 if let Some(name) = &image.name { 59 + let blob_url = format_smolstr!( 60 "{}xrpc/com.atproto.sync.getBlob?did={}&cid={}", 61 pds_url.as_str(), 62 did.as_ref(), ··· 110 let cid = image.image.blob().cid(); 111 112 if let Some(name) = &image.name { 113 + let blob_url = format_smolstr!( 114 "{}xrpc/com.atproto.sync.getBlob?did={}&cid={}", 115 pds_url.as_str(), 116 did.as_ref(), ··· 133 #[cfg(all(target_family = "wasm", target_os = "unknown"))] 134 fn send_blob_mappings( 135 notebook: &str, 136 + mappings: std::collections::HashMap<String, jacquard::smol_str::SmolStr>, 137 ) -> Result<(), JsValue> { 138 let window = web_sys::window().ok_or_else(|| JsValue::from_str("no window"))?; 139 let navigator = window.navigator(); ··· 152 // Convert HashMap to JS Object 153 let blobs_obj = js_sys::Object::new(); 154 for (name, url) in mappings { 155 + js_sys::Reflect::set(&blobs_obj, &name.into(), &url.as_str().into())?; 156 } 157 js_sys::Reflect::set(&msg, &"blobs".into(), &blobs_obj)?; 158 ··· 166 fn send_blob_rkey_mappings( 167 rkey: &str, 168 ident: &str, 169 + mappings: std::collections::HashMap<String, jacquard::smol_str::SmolStr>, 170 ) -> Result<(), JsValue> { 171 let window = web_sys::window().ok_or_else(|| JsValue::from_str("no window"))?; 172 let navigator = window.navigator(); ··· 186 // Convert HashMap to JS Object 187 let blobs_obj = js_sys::Object::new(); 188 for (name, url) in mappings { 189 + js_sys::Reflect::set(&blobs_obj, &name.into(), &url.as_str().into())?; 190 } 191 js_sys::Reflect::set(&msg, &"blobs".into(), &blobs_obj)?; 192 ··· 206 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))] 207 pub fn send_blob_mappings( 208 _notebook: &str, 209 + _mappings: std::collections::HashMap<String, jacquard::smol_str::SmolStr>, 210 ) -> Result<(), String> { 211 Ok(()) 212 }
+5 -5
crates/weaver-app/src/views/drafts.rs
··· 8 use crate::components::editor::{delete_draft, list_drafts}; 9 use crate::fetch::Fetcher; 10 use dioxus::prelude::*; 11 - use jacquard::smol_str::SmolStr; 12 use jacquard::types::ident::AtIdentifier; 13 use std::collections::HashSet; 14 ··· 161 div { class: "drafts-list", 162 for draft in merged_drafts() { 163 { 164 - let key_for_delete = format!("new:{}", draft.rkey); 165 let is_edit_draft = draft.editing_uri.is_some(); 166 let display_title = if draft.title.is_empty() { 167 "Untitled".to_string() ··· 296 297 // Construct AT-URI for the entry 298 let entry_uri = 299 - use_memo(move || format!("at://{}/sh.weaver.notebook.entry/{}", ident(), rkey())); 300 301 rsx! { 302 EditorCss {} ··· 320 321 // Construct AT-URI for the entry 322 let entry_uri = 323 - use_memo(move || format!("at://{}/sh.weaver.notebook.entry/{}", ident(), rkey())); 324 325 // Fetch notebook entries for wikilink validation 326 let (_entries_resource, entries_memo) = use_notebook_entries(ident, book_title); ··· 337 let path = book_entry.entry.path.as_ref().map(|p| p.as_str()).unwrap_or(""); 338 if !title.is_empty() || !path.is_empty() { 339 // Build canonical URL: /{ident}/{book}/{path} 340 - let canonical_url = format!("/{}/{}/{}", ident_str, book, path); 341 index.add_entry(title, path, canonical_url); 342 } 343 }
··· 8 use crate::components::editor::{delete_draft, list_drafts}; 9 use crate::fetch::Fetcher; 10 use dioxus::prelude::*; 11 + use jacquard::smol_str::{SmolStr, format_smolstr}; 12 use jacquard::types::ident::AtIdentifier; 13 use std::collections::HashSet; 14 ··· 161 div { class: "drafts-list", 162 for draft in merged_drafts() { 163 { 164 + let key_for_delete = format_smolstr!("new:{}", draft.rkey).to_string(); 165 let is_edit_draft = draft.editing_uri.is_some(); 166 let display_title = if draft.title.is_empty() { 167 "Untitled".to_string() ··· 296 297 // Construct AT-URI for the entry 298 let entry_uri = 299 + use_memo(move || format_smolstr!("at://{}/sh.weaver.notebook.entry/{}", ident(), rkey()).to_string()); 300 301 rsx! { 302 EditorCss {} ··· 320 321 // Construct AT-URI for the entry 322 let entry_uri = 323 + use_memo(move || format_smolstr!("at://{}/sh.weaver.notebook.entry/{}", ident(), rkey()).to_string()); 324 325 // Fetch notebook entries for wikilink validation 326 let (_entries_resource, entries_memo) = use_notebook_entries(ident, book_title); ··· 337 let path = book_entry.entry.path.as_ref().map(|p| p.as_str()).unwrap_or(""); 338 if !title.is_empty() || !path.is_empty() { 339 // Build canonical URL: /{ident}/{book}/{path} 340 + let canonical_url = format_smolstr!("/{}/{}/{}", ident_str, book, path).to_string(); 341 index.add_entry(title, path, canonical_url); 342 } 343 }
+27 -27
crates/weaver-app/src/views/entry.rs
··· 1 #![allow(non_snake_case)] 2 3 use dioxus::prelude::*; 4 - use jacquard::smol_str::{SmolStr, ToSmolStr}; 5 use jacquard::types::string::AtIdentifier; 6 7 use crate::components::NotebookCss; ··· 39 .authors 40 .first() 41 .map(|a| match &a.record.inner { 42 - ProfileDataViewInner::ProfileView(p) => p.handle.as_ref().to_string(), 43 - ProfileDataViewInner::ProfileViewDetailed(p) => p.handle.as_ref().to_string(), 44 - ProfileDataViewInner::TangledProfileView(p) => p.handle.as_ref().to_string(), 45 - _ => "unknown".to_string(), 46 }) 47 - .unwrap_or_else(|| "unknown".to_string()); 48 49 let base = if crate::env::WEAVER_APP_ENV == "dev" { 50 - format!("http://127.0.0.1:{}", crate::env::WEAVER_PORT) 51 } else { 52 - crate::env::WEAVER_APP_HOST.to_string() 53 }; 54 - let canonical_url = format!("{}/{}/e/{}", base, ident(), rkey()); 55 let description = extract_preview(&entry_record.content, 160); 56 57 let entry_signal = use_signal(|| data.entry.clone()); ··· 70 title: title.to_string(), 71 description: description.clone(), 72 image_url: String::new(), 73 - canonical_url: canonical_url.clone(), 74 - author_handle: author_handle.clone(), 75 book_title: Some(book_title.to_string()), 76 } 77 document::Link { rel: "stylesheet", href: ENTRY_CSS } ··· 119 title: title.to_string(), 120 description: description.clone(), 121 image_url: String::new(), 122 - canonical_url: canonical_url.clone(), 123 - author_handle: author_handle.clone(), 124 } 125 document::Link { rel: "stylesheet", href: ENTRY_CSS } 126 DefaultNotebookCss {} ··· 175 let entry_path = entry_view 176 .path 177 .as_ref() 178 - .map(|p| p.as_ref().to_string()) 179 - .unwrap_or_else(|| title.to_string()); 180 181 tracing::info!("Entry: {entry_path} - {title}"); 182 ··· 184 .authors 185 .first() 186 .map(|a| match &a.record.inner { 187 - ProfileDataViewInner::ProfileView(p) => p.handle.as_ref().to_string(), 188 - ProfileDataViewInner::ProfileViewDetailed(p) => p.handle.as_ref().to_string(), 189 - ProfileDataViewInner::TangledProfileView(p) => p.handle.as_ref().to_string(), 190 - _ => "unknown".to_string(), 191 }) 192 - .unwrap_or_else(|| "unknown".to_string()); 193 194 let base = if crate::env::WEAVER_APP_ENV == "dev" { 195 - format!("http://127.0.0.1:{}", crate::env::WEAVER_PORT) 196 } else { 197 - crate::env::WEAVER_APP_HOST.to_string() 198 }; 199 - let canonical_url = format!("{}/{}/{}/e/{}", base, ident(), book_title(), rkey()); 200 - let og_image_url = format!( 201 "{}/og/{}/{}/{}.png", 202 base, 203 ident(), ··· 212 EntryOgMeta { 213 title: title.to_string(), 214 description: description, 215 - image_url: og_image_url, 216 - canonical_url: canonical_url, 217 - author_handle: author_handle, 218 book_title: Some(book_title().to_string()), 219 } 220 document::Link { rel: "stylesheet", href: ENTRY_CSS }
··· 1 #![allow(non_snake_case)] 2 3 use dioxus::prelude::*; 4 + use jacquard::smol_str::{SmolStr, ToSmolStr, format_smolstr}; 5 use jacquard::types::string::AtIdentifier; 6 7 use crate::components::NotebookCss; ··· 39 .authors 40 .first() 41 .map(|a| match &a.record.inner { 42 + ProfileDataViewInner::ProfileView(p) => p.handle.as_ref().to_smolstr(), 43 + ProfileDataViewInner::ProfileViewDetailed(p) => p.handle.as_ref().to_smolstr(), 44 + ProfileDataViewInner::TangledProfileView(p) => p.handle.as_ref().to_smolstr(), 45 + _ => "unknown".into(), 46 }) 47 + .unwrap_or_else(|| "unknown".into()); 48 49 let base = if crate::env::WEAVER_APP_ENV == "dev" { 50 + format_smolstr!("http://127.0.0.1:{}", crate::env::WEAVER_PORT) 51 } else { 52 + SmolStr::new_static(crate::env::WEAVER_APP_HOST) 53 }; 54 + let canonical_url = format_smolstr!("{}/{}/e/{}", base, ident(), rkey()); 55 let description = extract_preview(&entry_record.content, 160); 56 57 let entry_signal = use_signal(|| data.entry.clone()); ··· 70 title: title.to_string(), 71 description: description.clone(), 72 image_url: String::new(), 73 + canonical_url: canonical_url.to_string(), 74 + author_handle: author_handle.to_string(), 75 book_title: Some(book_title.to_string()), 76 } 77 document::Link { rel: "stylesheet", href: ENTRY_CSS } ··· 119 title: title.to_string(), 120 description: description.clone(), 121 image_url: String::new(), 122 + canonical_url: canonical_url.to_string(), 123 + author_handle: author_handle.to_string(), 124 } 125 document::Link { rel: "stylesheet", href: ENTRY_CSS } 126 DefaultNotebookCss {} ··· 175 let entry_path = entry_view 176 .path 177 .as_ref() 178 + .map(|p| p.as_ref().to_smolstr()) 179 + .unwrap_or_else(|| title.into()); 180 181 tracing::info!("Entry: {entry_path} - {title}"); 182 ··· 184 .authors 185 .first() 186 .map(|a| match &a.record.inner { 187 + ProfileDataViewInner::ProfileView(p) => p.handle.as_ref().to_smolstr(), 188 + ProfileDataViewInner::ProfileViewDetailed(p) => p.handle.as_ref().to_smolstr(), 189 + ProfileDataViewInner::TangledProfileView(p) => p.handle.as_ref().to_smolstr(), 190 + _ => "unknown".into(), 191 }) 192 + .unwrap_or_else(|| "unknown".into()); 193 194 let base = if crate::env::WEAVER_APP_ENV == "dev" { 195 + format_smolstr!("http://127.0.0.1:{}", crate::env::WEAVER_PORT) 196 } else { 197 + SmolStr::new_static(crate::env::WEAVER_APP_HOST) 198 }; 199 + let canonical_url = format_smolstr!("{}/{}/{}/e/{}", base, ident(), book_title(), rkey()); 200 + let og_image_url = format_smolstr!( 201 "{}/og/{}/{}/{}.png", 202 base, 203 ident(), ··· 212 EntryOgMeta { 213 title: title.to_string(), 214 description: description, 215 + image_url: og_image_url.to_string(), 216 + canonical_url: canonical_url.to_string(), 217 + author_handle: author_handle.to_string(), 218 book_title: Some(book_title().to_string()), 219 } 220 document::Link { rel: "stylesheet", href: ENTRY_CSS }
+4 -4
crates/weaver-app/src/views/home.rs
··· 3 data, 4 }; 5 use dioxus::prelude::*; 6 - use jacquard::smol_str::SmolStr; 7 use jacquard::types::ident::AtIdentifier; 8 use jacquard::types::string::Did; 9 ··· 40 #[component] 41 pub fn SiteOgMeta() -> Element { 42 let base = if crate::env::WEAVER_APP_ENV == "dev" { 43 - format!("http://127.0.0.1:{}", crate::env::WEAVER_PORT) 44 } else { 45 - crate::env::WEAVER_APP_HOST.to_string() 46 }; 47 48 let title = "Weaver"; 49 let description = "Share your words, your way."; 50 - let image_url = format!("{}/og/site.png", base); 51 let canonical_url = base; 52 53 rsx! {
··· 3 data, 4 }; 5 use dioxus::prelude::*; 6 + use jacquard::smol_str::{SmolStr, format_smolstr}; 7 use jacquard::types::ident::AtIdentifier; 8 use jacquard::types::string::Did; 9 ··· 40 #[component] 41 pub fn SiteOgMeta() -> Element { 42 let base = if crate::env::WEAVER_APP_ENV == "dev" { 43 + format_smolstr!("http://127.0.0.1:{}", crate::env::WEAVER_PORT) 44 } else { 45 + SmolStr::new_static(crate::env::WEAVER_APP_HOST) 46 }; 47 48 let title = "Weaver"; 49 let description = "Share your words, your way."; 50 + let image_url = format_smolstr!("{}/og/site.png", base); 51 let canonical_url = base; 52 53 rsx! {
+13 -13
crates/weaver-app/src/views/notebook.rs
··· 7 }; 8 use dioxus::prelude::*; 9 use jacquard::{ 10 - smol_str::{SmolStr, ToSmolStr}, 11 types::ident::AtIdentifier, 12 }; 13 ··· 114 use weaver_api::sh_weaver::actor::ProfileDataViewInner; 115 notebook_view.authors.first() 116 .map(|a| match &a.record.inner { 117 - ProfileDataViewInner::ProfileView(p) => p.handle.as_ref().to_string(), 118 - ProfileDataViewInner::ProfileViewDetailed(p) => p.handle.as_ref().to_string(), 119 - ProfileDataViewInner::TangledProfileView(p) => p.handle.as_ref().to_string(), 120 - _ => "unknown".to_string(), 121 }) 122 - .unwrap_or_else(|| "unknown".to_string()) 123 }; 124 125 // NotebookView doesn't expose description directly, use empty for now 126 let og_description = String::new(); 127 128 let base = if crate::env::WEAVER_APP_ENV == "dev" { 129 - format!("http://127.0.0.1:{}", crate::env::WEAVER_PORT) 130 } else { 131 - crate::env::WEAVER_APP_HOST.to_string() 132 }; 133 - let og_image_url = format!("{}/og/notebook/{}/{}.png", base, ident(), book_title()); 134 - let canonical_url = format!("{}/{}/{}", base, ident(), book_title()); 135 136 rsx! { 137 NotebookOgMeta { 138 title: og_title, 139 description: og_description, 140 - image_url: og_image_url, 141 - canonical_url, 142 - author_handle: og_author, 143 entry_count: entries.len(), 144 } 145 div { class: "notebook-layout",
··· 7 }; 8 use dioxus::prelude::*; 9 use jacquard::{ 10 + smol_str::{SmolStr, ToSmolStr, format_smolstr}, 11 types::ident::AtIdentifier, 12 }; 13 ··· 114 use weaver_api::sh_weaver::actor::ProfileDataViewInner; 115 notebook_view.authors.first() 116 .map(|a| match &a.record.inner { 117 + ProfileDataViewInner::ProfileView(p) => p.handle.as_ref().to_smolstr(), 118 + ProfileDataViewInner::ProfileViewDetailed(p) => p.handle.as_ref().to_smolstr(), 119 + ProfileDataViewInner::TangledProfileView(p) => p.handle.as_ref().to_smolstr(), 120 + _ => "unknown".into(), 121 }) 122 + .unwrap_or_else(|| "unknown".into()) 123 }; 124 125 // NotebookView doesn't expose description directly, use empty for now 126 let og_description = String::new(); 127 128 let base = if crate::env::WEAVER_APP_ENV == "dev" { 129 + format_smolstr!("http://127.0.0.1:{}", crate::env::WEAVER_PORT) 130 } else { 131 + SmolStr::new_static(crate::env::WEAVER_APP_HOST) 132 }; 133 + let og_image_url = format_smolstr!("{}/og/notebook/{}/{}.png", base, ident(), book_title()); 134 + let canonical_url = format_smolstr!("{}/{}/{}", base, ident(), book_title()); 135 136 rsx! { 137 NotebookOgMeta { 138 title: og_title, 139 description: og_description, 140 + image_url: og_image_url.to_string(), 141 + canonical_url: canonical_url.to_string(), 142 + author_handle: og_author.to_string(), 143 entry_count: entries.len(), 144 } 145 div { class: "notebook-layout",