clean up

Orual b95932ff 51c54057

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