few fixes, incl css

Orual 766491ce 1f3e4380

+504 -404
+42 -151
Cargo.lock
··· 22 22 ] 23 23 24 24 [[package]] 25 - name = "accessory" 26 - version = "2.1.0" 27 - source = "registry+https://github.com/rust-lang/crates.io-index" 28 - checksum = "28e416a3ab45838bac2ab2d81b1088d738d7b2d2c5272a54d39366565a29bd80" 29 - dependencies = [ 30 - "macroific", 31 - "proc-macro2", 32 - "quote", 33 - "syn 2.0.110", 34 - ] 35 - 36 - [[package]] 37 25 name = "addr2line" 38 26 version = "0.25.1" 39 27 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 218 206 "pin-project-lite", 219 207 "tokio", 220 208 ] 209 + 210 + [[package]] 211 + name = "async-once-cell" 212 + version = "0.5.4" 213 + source = "registry+https://github.com/rust-lang/crates.io-index" 214 + checksum = "4288f83726785267c6f2ef073a3d83dc3f9b81464e9f99898240cced85fce35a" 221 215 222 216 [[package]] 223 217 name = "async-recursion" ··· 1417 1411 ] 1418 1412 1419 1413 [[package]] 1420 - name = "delegate-display" 1421 - version = "3.0.0" 1422 - source = "registry+https://github.com/rust-lang/crates.io-index" 1423 - checksum = "9926686c832494164c33a36bf65118f4bd6e704000b58c94681bf62e9ad67a74" 1424 - dependencies = [ 1425 - "impartial-ord", 1426 - "itoa", 1427 - "macroific", 1428 - "proc-macro2", 1429 - "quote", 1430 - "syn 2.0.110", 1431 - ] 1432 - 1433 - [[package]] 1434 1414 name = "der" 1435 1415 version = "0.7.10" 1436 1416 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1618 1598 "serde", 1619 1599 "subsecond", 1620 1600 "warnings", 1601 + "wasm-splitter", 1621 1602 ] 1622 1603 1623 1604 [[package]] ··· 2660 2641 ] 2661 2642 2662 2643 [[package]] 2663 - name = "fancy_constructor" 2664 - version = "2.1.0" 2665 - source = "registry+https://github.com/rust-lang/crates.io-index" 2666 - checksum = "28a27643a5d05f3a22f5afd6e0d0e6e354f92d37907006f97b84b9cb79082198" 2667 - dependencies = [ 2668 - "macroific", 2669 - "proc-macro2", 2670 - "quote", 2671 - "syn 2.0.110", 2672 - ] 2673 - 2674 - [[package]] 2675 2644 name = "fastrand" 2676 2645 version = "2.3.0" 2677 2646 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3913 3882 ] 3914 3883 3915 3884 [[package]] 3916 - name = "impartial-ord" 3917 - version = "1.0.6" 3918 - source = "registry+https://github.com/rust-lang/crates.io-index" 3919 - checksum = "0ab604ee7085efba6efc65e4ebca0e9533e3aff6cb501d7d77b211e3a781c6d5" 3920 - dependencies = [ 3921 - "proc-macro2", 3922 - "quote", 3923 - "syn 2.0.110", 3924 - ] 3925 - 3926 - [[package]] 3927 - name = "indexed_db_futures" 3928 - version = "0.6.4" 3929 - source = "registry+https://github.com/rust-lang/crates.io-index" 3930 - checksum = "69ff41758cbd104e91033bb53bc449bec7eea65652960c81eddf3fc146ecea19" 3931 - dependencies = [ 3932 - "accessory", 3933 - "cfg-if", 3934 - "delegate-display", 3935 - "derive_more 2.0.1", 3936 - "fancy_constructor", 3937 - "indexed_db_futures_macros_internal", 3938 - "js-sys", 3939 - "sealed", 3940 - "smallvec", 3941 - "thiserror 2.0.17", 3942 - "tokio", 3943 - "wasm-bindgen", 3944 - "wasm-bindgen-futures", 3945 - "web-sys", 3946 - ] 3947 - 3948 - [[package]] 3949 - name = "indexed_db_futures_macros_internal" 3950 - version = "1.0.0" 3951 - source = "registry+https://github.com/rust-lang/crates.io-index" 3952 - checksum = "caeba94923b68f254abef921cea7e7698bf4675fdd89d7c58bf1ed885b49a27d" 3953 - dependencies = [ 3954 - "macroific", 3955 - "proc-macro2", 3956 - "quote", 3957 - "syn 2.0.110", 3958 - ] 3959 - 3960 - [[package]] 3961 3885 name = "indexmap" 3962 3886 version = "1.9.3" 3963 3887 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4082 4006 dependencies = [ 4083 4007 "bytes", 4084 4008 "getrandom 0.2.16", 4009 + "gloo-storage", 4085 4010 "http", 4086 4011 "jacquard-api", 4087 4012 "jacquard-common", ··· 4090 4015 "jacquard-oauth", 4091 4016 "jose-jwk", 4092 4017 "miette 7.6.0", 4093 - "n0-future", 4094 4018 "regex", 4095 4019 "reqwest", 4096 4020 "serde", ··· 4209 4133 "jacquard-lexicon", 4210 4134 "miette 7.6.0", 4211 4135 "mini-moka", 4212 - "n0-future", 4213 4136 "percent-encoding", 4214 4137 "reqwest", 4215 4138 "serde", ··· 4263 4186 "jose-jwa", 4264 4187 "jose-jwk", 4265 4188 "miette 7.6.0", 4266 - "n0-future", 4267 4189 "p256", 4268 4190 "rand 0.8.5", 4269 4191 "rouille", ··· 4630 4552 checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" 4631 4553 4632 4554 [[package]] 4555 + name = "lol_alloc" 4556 + version = "0.4.1" 4557 + source = "registry+https://github.com/rust-lang/crates.io-index" 4558 + checksum = "83e5106554cabc97552dcadf54f57560ae6af3276652f82ca2be06120dc4c5dc" 4559 + dependencies = [ 4560 + "spin 0.9.8", 4561 + ] 4562 + 4563 + [[package]] 4633 4564 name = "longest-increasing-subsequence" 4634 4565 version = "0.1.0" 4635 4566 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4690 4621 ] 4691 4622 4692 4623 [[package]] 4693 - name = "macroific" 4694 - version = "2.0.0" 4695 - source = "registry+https://github.com/rust-lang/crates.io-index" 4696 - checksum = "89f276537b4b8f981bf1c13d79470980f71134b7bdcc5e6e911e910e556b0285" 4697 - dependencies = [ 4698 - "macroific_attr_parse", 4699 - "macroific_core", 4700 - "macroific_macro", 4701 - ] 4702 - 4703 - [[package]] 4704 - name = "macroific_attr_parse" 4705 - version = "2.0.0" 4706 - source = "registry+https://github.com/rust-lang/crates.io-index" 4707 - checksum = "ad4023761b45fcd36abed8fb7ae6a80456b0a38102d55e89a57d9a594a236be9" 4708 - dependencies = [ 4709 - "proc-macro2", 4710 - "quote", 4711 - "sealed", 4712 - "syn 2.0.110", 4713 - ] 4714 - 4715 - [[package]] 4716 - name = "macroific_core" 4717 - version = "2.0.0" 4718 - source = "registry+https://github.com/rust-lang/crates.io-index" 4719 - checksum = "d0a7594d3c14916fa55bef7e9d18c5daa9ed410dd37504251e4b75bbdeec33e3" 4720 - dependencies = [ 4721 - "proc-macro2", 4722 - "quote", 4723 - "sealed", 4724 - "syn 2.0.110", 4725 - ] 4726 - 4727 - [[package]] 4728 - name = "macroific_macro" 4729 - version = "2.0.0" 4730 - source = "registry+https://github.com/rust-lang/crates.io-index" 4731 - checksum = "4da6f2ed796261b0a74e2b52b42c693bb6dee1effba3a482c49592659f824b3b" 4732 - dependencies = [ 4733 - "macroific_attr_parse", 4734 - "macroific_core", 4735 - "proc-macro2", 4736 - "quote", 4737 - "syn 2.0.110", 4738 - ] 4739 - 4740 - [[package]] 4741 4624 name = "malloc_buf" 4742 4625 version = "0.0.6" 4743 4626 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 6699 6582 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 6700 6583 6701 6584 [[package]] 6702 - name = "sealed" 6703 - version = "0.6.0" 6704 - source = "registry+https://github.com/rust-lang/crates.io-index" 6705 - checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107" 6706 - dependencies = [ 6707 - "proc-macro2", 6708 - "quote", 6709 - "syn 2.0.110", 6710 - ] 6711 - 6712 - [[package]] 6713 6585 name = "sec1" 6714 6586 version = "0.7.3" 6715 6587 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 7262 7134 source = "registry+https://github.com/rust-lang/crates.io-index" 7263 7135 checksum = "35c6d746902bca4ddf16592357eacf0473631ea26b36072f0dd0b31fa5ccd1f4" 7264 7136 dependencies = [ 7265 - "indexed_db_futures", 7266 7137 "js-sys", 7267 7138 "once_cell", 7268 7139 "thiserror 2.0.17", ··· 8531 8402 ] 8532 8403 8533 8404 [[package]] 8405 + name = "wasm-split-macro" 8406 + version = "0.7.1" 8407 + source = "registry+https://github.com/rust-lang/crates.io-index" 8408 + checksum = "8a12f9d65e85c7ea30ad753fd6c84e5e7a0ff9e581e1ed1860d173807018f955" 8409 + dependencies = [ 8410 + "base16", 8411 + "digest", 8412 + "proc-macro2", 8413 + "quote", 8414 + "sha2", 8415 + "syn 2.0.110", 8416 + ] 8417 + 8418 + [[package]] 8419 + name = "wasm-splitter" 8420 + version = "0.7.1" 8421 + source = "registry+https://github.com/rust-lang/crates.io-index" 8422 + checksum = "7e0bdc944727dcfb8cb919213d19f2946b508370740bf7c1c6e215acd98dd5b4" 8423 + dependencies = [ 8424 + "async-once-cell", 8425 + "wasm-split-macro", 8426 + ] 8427 + 8428 + [[package]] 8534 8429 name = "wasm-streams" 8535 8430 version = "0.4.2" 8536 8431 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 8658 8553 "chrono", 8659 8554 "console_error_panic_hook", 8660 8555 "dashmap", 8661 - "diesel", 8662 - "diesel_migrations", 8663 8556 "dioxus", 8664 8557 "dioxus-free-icons", 8665 - "dioxus-logger", 8666 8558 "dioxus-primitives", 8667 8559 "dotenvy", 8668 8560 "gloo-storage", 8669 - "hex_fmt", 8670 8561 "http", 8671 8562 "humansize", 8672 8563 "jacquard", 8673 8564 "jacquard-axum", 8674 8565 "jacquard-lexicon", 8675 8566 "js-sys", 8567 + "lol_alloc", 8676 8568 "markdown-weaver", 8677 8569 "mime-sniffer", 8678 8570 "mini-moka", ··· 8681 8573 "serde", 8682 8574 "serde_html_form", 8683 8575 "serde_json", 8684 - "sqlite-wasm-rs", 8685 8576 "time", 8686 8577 "tokio", 8687 8578 "tracing",
+4
Cargo.toml
··· 59 59 [profile.wasm-dev] 60 60 inherits = "dev" 61 61 opt-level = 1 62 + lto = true 63 + debug = true 62 64 63 65 [profile.server-dev] 64 66 inherits = "dev" 67 + lto = true 68 + debug = true 65 69 66 70 [profile.android-dev] 67 71 inherits = "dev"
+10 -9
crates/weaver-app/Cargo.toml
··· 8 8 default = ["web", "fullstack-server", "no-app-index"] 9 9 # Fullstack mode with SSR and server functions 10 10 fullstack-server = ["dioxus/fullstack"] 11 + wasm-split = ["dioxus/wasm-split"] 11 12 no-app-index = [] 12 13 13 14 web = ["dioxus/web"] ··· 22 23 dioxus = { version = "0.7.1", features = ["router"] } 23 24 #dioxus-router = { version = "0.7.1", features = ["wasm-split"] } 24 25 weaver-common = { path = "../weaver-common" } 25 - jacquard = { workspace = true, features = ["streaming"] } 26 + jacquard = { workspace = true}#, features = ["streaming"] } 26 27 jacquard-lexicon = { workspace = true } 27 28 jacquard-axum = { workspace = true, optional = true } 28 - weaver-api = { path = "../weaver-api", features = ["streaming"] } 29 + weaver-api = { path = "../weaver-api"}#, features = ["streaming"] } 29 30 markdown-weaver = { workspace = true } 30 31 weaver-renderer = { path = "../weaver-renderer" } 31 32 mini-moka = { git = "https://github.com/moka-rs/mini-moka", rev = "da864e849f5d034f32e02197fee9bb5d5af36d3d" } ··· 34 35 axum = {version = "0.8.6", optional = true} 35 36 mime-sniffer = {version = "^0.1"} 36 37 chrono = { version = "0.4" } 37 - serde = { version = "1.0", features = ["derive"] } 38 + serde = { version = "1.0"} #, features = ["derive"] } 38 39 serde_json = "1.0" 39 - hex_fmt = "0.3" 40 40 humansize = "2.0.0" 41 41 base64 = "0.22" 42 42 http = "1.3" 43 43 reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } 44 44 dioxus-free-icons = { version = "0.10.0" } 45 - diesel = { version = "2.3", features = ["sqlite", "returning_clauses_for_sqlite_3_35", "chrono", "serde_json"] } 46 - diesel_migrations = { version = "2.3", features = ["sqlite"] } 45 + # diesel = { version = "2.3", features = ["sqlite", "returning_clauses_for_sqlite_3_35", "chrono", "serde_json"] } 46 + # diesel_migrations = { version = "2.3", features = ["sqlite"] } 47 47 tokio = { version = "1.28", features = ["sync"] } 48 - dioxus-logger = "0.7.1" 49 48 serde_html_form = "0.2.8" 50 - webbrowser = "1.0.6" 51 49 tracing.workspace = true 52 50 51 + [target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies] 52 + webbrowser = "1.0.6" 53 53 54 54 [target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies] 55 - sqlite-wasm-rs = { version = "0.4", default-features = false, features = ["precompiled", "relaxed-idb"] } 55 + #sqlite-wasm-rs = { version = "0.4", default-features = false, features = ["precompiled", "relaxed-idb"] } 56 56 time = { version = "0.3", features = ["wasm-bindgen"] } 57 57 console_error_panic_hook = "0.1" 58 58 mini-moka = { git = "https://github.com/moka-rs/mini-moka", rev = "da864e849f5d034f32e02197fee9bb5d5af36d3d", features = ["js"] } ··· 62 62 web-sys = { version = "0.3", features = ["ServiceWorkerContainer", "ServiceWorker", "ServiceWorkerRegistration", "RegistrationOptions", "Window", "Navigator", "MessageEvent", "console", "Document", "Element", "HtmlImageElement"] } 63 63 js-sys = "0.3" 64 64 gloo-storage = "0.3" 65 + lol_alloc = "0.4.1" 65 66 66 67 [build-dependencies] 67 68 dotenvy = "0.15.7"
+41 -1
crates/weaver-app/assets/styling/entry.css
··· 9 9 max-width: calc(90ch + 400px + 4rem); /* content + gutters + gaps */ 10 10 margin: 0 auto; 11 11 padding: 0 1rem 0 0; 12 + box-sizing: border-box; 12 13 } 13 14 14 15 /* Main content area */ ··· 203 204 background: var(--color-surface); 204 205 } 205 206 206 - /* Responsive layout */ 207 + /* Responsive layout - Tablet/small desktop */ 207 208 @media (max-width: 1400px) { 208 209 .entry-page-layout { 209 210 grid-template-columns: 1fr; ··· 236 237 order: 1; 237 238 } 238 239 } 240 + 241 + /* Tablet and smaller */ 242 + @media (max-width: 900px) { 243 + .entry-page-layout { 244 + max-width: 100%; 245 + grid-template-columns: minmax(0, 1fr); 246 + } 247 + } 248 + 249 + /* Small mobile phones */ 250 + @media (max-width: 480px) { 251 + .entry-content-main { 252 + padding: 1rem 0.75rem; 253 + } 254 + 255 + .nav-gutter { 256 + padding: 0 0.75rem; 257 + } 258 + 259 + .nav-button { 260 + padding: 0.75rem; 261 + } 262 + 263 + .entry-metadata { 264 + margin-bottom: 1rem; 265 + padding-bottom: 0.75rem; 266 + } 267 + 268 + .entry-title { 269 + font-size: 1.65rem; 270 + margin-top: 0.5rem; 271 + margin-bottom: 0.75rem; 272 + } 273 + 274 + .entry-meta-info { 275 + gap: 0.75rem; 276 + font-size: 0.85rem; 277 + } 278 + }
+2 -2
crates/weaver-app/assets/styling/record-view.css
··· 933 933 934 934 .accordion-content { 935 935 grid-template-rows: 1fr; 936 - padding-left: 2.8rem; 936 + padding-left: 46px; 937 937 } 938 938 939 939 .accordion-content .section-content { ··· 969 969 } 970 970 971 971 .accordion { 972 - margin-left: -2.8rem; 972 + margin-left: -46px; 973 973 }
+3 -2
crates/weaver-app/src/auth/mod.rs
··· 34 34 use crate::fetch::CachedFetcher; 35 35 use dioxus::prelude::*; 36 36 use gloo_storage::{LocalStorage, Storage}; 37 + use jacquard::types::string::Did; 37 38 // Look for session keys in localStorage (format: oauth_session_{did}_{session_id}) 38 39 let keys = LocalStorage::get_all::<serde_json::Value>()?; 39 40 let mut found_session: Option<(String, String)> = None; ··· 57 58 58 59 let (did_str, session_id) = 59 60 found_session.ok_or(CapturedError::from_display("No saved session found"))?; 60 - let did = jacquard::types::string::Did::new_owned(did_str)?; 61 + let did = Did::new_owned(did_str)?; 61 62 let fetcher = use_context::<CachedFetcher>(); 62 63 63 64 let session = fetcher ··· 77 78 .set_authenticated(restored_did, session_id); 78 79 fetcher.upgrade_to_authenticated(session).await; 79 80 80 - dioxus_logger::tracing::debug!("session restored"); 81 + tracing::debug!("session restored"); 81 82 Ok(()) 82 83 }
+1 -1
crates/weaver-app/src/auth/storage.rs
··· 102 102 Ok(Some(data)) 103 103 } 104 104 Err(gloo_storage::errors::StorageError::KeyNotFound(err)) => { 105 - dioxus_logger::tracing::debug!("gloo error: {}", err); 105 + tracing::debug!("gloo error: {}", err); 106 106 Ok(None) 107 107 } 108 108 Err(e) => Err(SessionStoreError::Other(
+1 -1
crates/weaver-app/src/fetch.rs
··· 1 1 use crate::auth::AuthStore; 2 2 use crate::cache_impl; 3 3 use dioxus::Result; 4 - use dioxus::prelude::*; 5 4 use jacquard::AuthorizationToken; 6 5 use jacquard::CowStr; 7 6 use jacquard::IntoStatic; ··· 372 371 } 373 372 } 374 373 374 + #[allow(dead_code)] 375 375 pub async fn current_did(&self) -> Option<Did<'static>> { 376 376 let session_slot = self.client.session.read().await; 377 377 if let Some(session) = session_slot.as_ref() {
+11 -26
crates/weaver-app/src/main.rs
··· 8 8 use dioxus::fullstack::FullstackContext; 9 9 #[cfg(all(feature = "fullstack-server", feature = "server"))] 10 10 use dioxus::fullstack::response::Extension; 11 - use dioxus_logger::tracing::Level; 12 - use jacquard::{ 13 - oauth::{client::OAuthClient, session::ClientData}, 14 - types::aturi::AtUri, 15 - }; 11 + use jacquard::oauth::{client::OAuthClient, session::ClientData}; 16 12 #[allow(unused)] 17 13 use jacquard::{ 18 14 smol_str::SmolStr, ··· 20 16 }; 21 17 #[cfg(feature = "server")] 22 18 use std::sync::Arc; 23 - use std::sync::{LazyLock, Mutex}; 19 + use std::sync::LazyLock; 24 20 #[allow(unused)] 25 21 use views::{ 26 22 Callback, Home, Navbar, Notebook, NotebookIndex, NotebookPage, RecordIndex, RecordView, ··· 45 41 /// Define a views module that contains the UI for all Layouts and Routes for our app. 46 42 mod views; 47 43 48 - /// The Route enum is used to define the structure of internal routes in our app. All route enums need to derive 49 - /// the [`Routable`] trait, which provides the necessary methods for the router to work. 50 - /// 51 - /// Each variant represents a different URL pattern that can be matched by the router. If that pattern is matched, 52 - /// the components for that route will be rendered. 44 + #[cfg(target_arch = "wasm32")] 45 + use lol_alloc::{FreeListAllocator, LockedAllocator}; 46 + 47 + #[cfg(target_arch = "wasm32")] 48 + #[global_allocator] 49 + static ALLOCATOR: LockedAllocator<FreeListAllocator> = 50 + LockedAllocator::new(FreeListAllocator::new()); 51 + 53 52 #[derive(Debug, Clone, Routable, PartialEq)] 54 53 #[rustfmt::skip] 55 54 enum Route { 56 - // The layout attribute defines a wrapper for all routes under the layout. Layouts are great for wrapping 57 - // many routes with a common UI like a navbar. 58 55 #[layout(Navbar)] 59 - // The route attribute defines the URL pattern that a specific route matches. If that pattern matches the URL, 60 - // the component for that route will be rendered. The component name that is rendered defaults to the variant name. 61 56 #[route("/")] 62 57 Home {}, 63 58 #[layout(ErrorLayout)] ··· 81 76 Entry { ident: AtIdentifier<'static>, book_title: SmolStr, title: SmolStr } 82 77 83 78 } 84 - 85 - // We can import assets in dioxus with the `asset!` macro. This macro takes a path to an asset relative to the crate root. 86 - // The macro returns an `Asset` type that will display as the path to the asset in the browser or a local path in desktop bundles. 87 79 const FAVICON: Asset = asset!("/assets/weaver_photo_sm.jpg"); 88 - // The asset macro also minifies some assets like CSS and JS to make bundled smaller 89 80 const MAIN_CSS: Asset = asset!("/assets/styling/main.css"); 90 81 91 82 #[cfg(not(feature = "fullstack-server"))] ··· 104 95 oauth: OAuthConfig::from_env().as_metadata(), 105 96 }); 106 97 fn main() { 107 - dioxus_logger::init(Level::DEBUG).expect("logger failed to init"); 108 98 // Set up better panic messages for wasm 109 99 #[cfg(target_arch = "wasm32")] 110 100 console_error_panic_hook::set_once(); ··· 148 138 async move { 149 139 req.extensions_mut().insert(blob_cache); 150 140 req.extensions_mut().insert(fetcher); 151 - 152 - // And then return the response with `next.run() 153 141 Ok::<_, Infallible>(next.run(req).await) 154 142 } 155 143 } 156 144 })) 157 145 }; 158 - // And then return the router 159 146 Ok(router) 160 147 }); 161 148 162 - // When not on the server, just run `launch()` like normal 163 149 #[cfg(not(feature = "server"))] 164 150 dioxus::launch(App); 165 151 } ··· 177 163 use_effect(move || { 178 164 spawn(async move { 179 165 if let Err(e) = auth::restore_session().await { 180 - dioxus_logger::tracing::warn!("Session restoration failed: {}", e); 166 + tracing::warn!("Session restoration failed: {}", e); 181 167 } 182 168 }); 183 169 }); 184 170 185 - // Register service worker on startup (only on web) 186 171 #[cfg(all( 187 172 target_family = "wasm", 188 173 target_os = "unknown",
+4
crates/weaver-app/src/service_worker.rs
··· 1 + use dioxus::prelude::*; 1 2 #[cfg(all(target_family = "wasm", target_os = "unknown"))] 2 3 use wasm_bindgen::prelude::*; 3 4 ··· 114 115 ) -> Result<(), String> { 115 116 Ok(()) 116 117 } 118 + 119 + // #[used] 120 + // static BINDINGS_JS: Asset = asset!("/assets/sw.js", AssetOptions::js().with_hash_suffix(false));
+4 -10
crates/weaver-app/src/views/callback.rs
··· 1 1 use crate::auth::AuthState; 2 2 use crate::fetch::CachedFetcher; 3 - use dioxus::logger::tracing::{Level, error, info}; 4 - use dioxus::{CapturedError, prelude::*}; 3 + use dioxus::prelude::*; 5 4 use jacquard::{ 6 5 IntoStatic, 7 6 cowstr::ToCowStr, 8 7 oauth::{error::OAuthError, types::CallbackParams}, 9 8 smol_str::SmolStr, 10 9 }; 10 + use tracing::{error, info}; 11 11 12 12 #[component] 13 13 pub fn Callback( ··· 46 46 47 47 match &*result.read_unchecked() { 48 48 Some(Ok(())) => { 49 - // #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] 50 - // { 51 - // if let Some(window) = web_sys::window() { 52 - // window.close().ok(); 53 - // } 54 - // } 55 - #[cfg(target_arch = "wasm32")] 49 + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] 56 50 { 57 51 use gloo_storage::Storage; 58 52 let mut prev = gloo_storage::LocalStorage::get::<String>("cached_route").ok(); 59 53 if let Some(prev) = prev.take() { 60 - dioxus_logger::tracing::info!("Navigating to previous page"); 54 + tracing::info!("Navigating to previous page"); 61 55 let nav = use_navigator(); 62 56 gloo_storage::LocalStorage::delete("cached_route"); 63 57 nav.replace(prev);
+1 -1
crates/weaver-app/src/views/home.rs
··· 1 1 use crate::{Route, components::identity::NotebookCard, fetch}; 2 2 use dioxus::prelude::*; 3 - use jacquard::{IntoStatic, smol_str::ToSmolStr, types::aturi::AtUri}; 3 + use jacquard::types::aturi::AtUri; 4 4 5 5 const NOTEBOOK_CARD_CSS: Asset = asset!("/assets/styling/notebook-card.css"); 6 6
+37 -68
crates/weaver-app/src/views/record.rs
··· 3 3 use crate::components::accordion::{Accordion, AccordionContent, AccordionItem, AccordionTrigger}; 4 4 use crate::components::dialog::{DialogContent, DialogDescription, DialogRoot, DialogTitle}; 5 5 use crate::fetch::CachedFetcher; 6 + use dioxus::prelude::Event; 6 7 use dioxus::{CapturedError, prelude::*}; 7 8 use humansize::format_size; 8 9 use jacquard::api::com_atproto::repo::get_record::GetRecordOutput; 10 + use jacquard::bytes::Bytes; 9 11 use jacquard::client::AgentError; 10 12 use jacquard::common::to_data; 11 - use jacquard::prelude::*; 12 13 use jacquard::smol_str::ToSmolStr; 14 + use jacquard::types::LexiconStringType; 15 + use jacquard::types::string::AtprotoStr; 16 + use jacquard::{atproto, prelude::*}; 13 17 use jacquard::{ 14 18 client::AgentSessionExt, 15 19 common::{Data, IntoStatic}, ··· 795 799 } 796 800 797 801 #[component] 798 - fn HighlightedString(string_type: jacquard::types::string::AtprotoStr<'static>) -> Element { 802 + fn HighlightedString(string_type: AtprotoStr<'static>) -> Element { 799 803 use jacquard::types::string::AtprotoStr; 800 804 801 805 match &string_type { ··· 1166 1170 Index(usize), 1167 1171 } 1168 1172 1169 - /// Check if a validation path matches or is a child of the given UI path 1170 - /// Filters out UnionVariant segments from validation path for comparison 1171 - fn validation_path_matches_ui(validation_path: &ValidationPath, ui_path: &str) -> bool { 1172 - use jacquard_lexicon::validation::PathSegment; 1173 - 1174 - let ui_segments = parse_ui_path(ui_path); 1175 - 1176 - // Convert validation path to UI segments by filtering out UnionVariant 1177 - let validation_ui_segments: Vec<_> = validation_path 1178 - .segments() 1179 - .iter() 1180 - .filter_map(|seg| match seg { 1181 - PathSegment::Field(name) => Some(UiPathSegment::Field(name.to_string())), 1182 - PathSegment::Index(idx) => Some(UiPathSegment::Index(*idx)), 1183 - PathSegment::UnionVariant(_) => None, // Skip union discriminators 1184 - }) 1185 - .collect(); 1186 - 1187 - // Check if validation path matches or is a child of UI path 1188 - if validation_ui_segments.len() < ui_segments.len() { 1189 - return false; // Validation path can't be shorter than UI path 1190 - } 1191 - 1192 - // Check if all UI segments match the start of validation segments 1193 - ui_segments 1194 - .iter() 1195 - .zip(validation_ui_segments.iter()) 1196 - .all(|(a, b)| a == b) 1197 - } 1198 - 1199 1173 /// Get all validation errors at exactly this path (not children) 1200 1174 fn get_errors_at_exact_path( 1201 1175 validation_result: &Option<ValidationResult>, ··· 1287 1261 /// Parse text as specific AtprotoStr type, preserving type information 1288 1262 fn try_parse_as_type( 1289 1263 text: &str, 1290 - string_type: jacquard::types::LexiconStringType, 1291 - ) -> Result<jacquard::types::string::AtprotoStr<'static>, String> { 1292 - use jacquard::types::LexiconStringType; 1264 + string_type: LexiconStringType, 1265 + ) -> Result<AtprotoStr<'static>, String> { 1293 1266 use jacquard::types::string::*; 1294 1267 use std::str::FromStr; 1295 1268 ··· 1360 1333 } 1361 1334 Data::Object(Object(new_obj)) 1362 1335 } 1363 - 1364 1336 Data::Array(_) => Data::Array(Array(Vec::new())), 1365 - 1366 1337 Data::String(s) => match s.string_type() { 1367 1338 LexiconStringType::Datetime => { 1368 1339 // Sensible default: now 1369 1340 Data::String(AtprotoStr::Datetime(Datetime::now())) 1370 1341 } 1342 + LexiconStringType::Tid => Data::String(AtprotoStr::Tid(Tid::now_0())), 1371 1343 _ => { 1372 1344 // Empty string, type inference will handle it 1373 1345 Data::String(AtprotoStr::String("".into())) 1374 1346 } 1375 1347 }, 1376 - 1377 1348 Data::Integer(_) => Data::Integer(0), 1378 1349 Data::Boolean(_) => Data::Boolean(false), 1379 - 1380 1350 Data::Blob(blob) => { 1381 1351 // Placeholder blob 1382 1352 Data::Blob( ··· 1390 1360 .into_static(), 1391 1361 ) 1392 1362 } 1393 - 1394 - Data::Bytes(_) | Data::CidLink(_) | Data::Null => Data::Null, 1363 + Data::CidLink(_) => Data::CidLink(Cid::str( 1364 + "bafkreiaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 1365 + )), 1366 + Data::Bytes(_) => Data::Bytes(Bytes::new()), 1367 + Data::Null => Data::Null, 1395 1368 } 1396 1369 } 1397 1370 ··· 1488 1461 }); 1489 1462 1490 1463 let path_for_mutation = path.clone(); 1491 - let handle_input = move |evt: dioxus::prelude::Event<dioxus::prelude::FormData>| { 1464 + let handle_input = move |evt: Event<FormData>| { 1492 1465 let new_text = evt.value(); 1493 1466 input_text.set(new_text.clone()); 1494 1467 ··· 1741 1714 1742 1715 let fetcher = use_context::<CachedFetcher>(); 1743 1716 let path_for_upload = path.clone(); 1744 - let handle_file = move |evt: dioxus::prelude::Event<dioxus::prelude::FormData>| { 1717 + let handle_file = move |evt: Event<dioxus::prelude::FormData>| { 1745 1718 let fetcher = fetcher.clone(); 1746 1719 let path_upload_clone = path_for_upload.clone(); 1747 1720 spawn(async move { ··· 1818 1791 }; 1819 1792 1820 1793 let path_for_cid = path.clone(); 1821 - let handle_cid_change = move |evt: dioxus::prelude::Event<dioxus::prelude::FormData>| { 1794 + let handle_cid_change = move |evt: Event<dioxus::prelude::FormData>| { 1822 1795 let text = evt.value(); 1823 1796 cid_input.set(text.clone()); 1824 1797 ··· 1838 1811 }; 1839 1812 1840 1813 let path_for_size = path.clone(); 1841 - let handle_size_change = move |evt: dioxus::prelude::Event<dioxus::prelude::FormData>| { 1814 + let handle_size_change = move |evt: Event<dioxus::prelude::FormData>| { 1842 1815 let text = evt.value(); 1843 1816 size_input.set(text.clone()); 1844 1817 ··· 1992 1965 // Find aspectRatio that's a sibling of our blob 1993 1966 // e.g. blob at "embed.images[0].image" -> look for "embed.images[0].aspectRatio" 1994 1967 let blob_parent = blob_path.rsplit_once('.').map(|(parent, _)| parent); 1995 - 1996 1968 matches.iter().find_map(|query_match| { 1997 - let aspect_path = query_match.path.as_str(); 1998 - let aspect_parent = aspect_path.rsplit_once('.').map(|(parent, _)| parent); 1969 + let aspect_parent = query_match.path.rsplit_once('.').map(|(parent, _)| parent); 1999 1970 2000 1971 // Check if they share the same parent 2001 1972 if blob_parent == aspect_parent { 2002 - Some(aspect_path.to_string()) 1973 + Some(query_match.path.clone()) 2003 1974 } else { 2004 1975 None 2005 1976 } ··· 2009 1980 2010 1981 // Update the aspectRatio if we found a matching field 2011 1982 if let Some(aspect_path) = aspect_path_to_update { 2012 - use jacquard::types::value::Object; 2013 - use std::collections::BTreeMap; 2014 - 2015 - let mut aspect_obj = BTreeMap::new(); 2016 - aspect_obj.insert("width".into(), Data::Integer(width)); 2017 - aspect_obj.insert("height".into(), Data::Integer(height)); 1983 + let aspect_obj = atproto! {{ 1984 + "width": width, 1985 + "height": height 1986 + }}; 2018 1987 2019 1988 root.with_mut(|record_data| { 2020 - record_data.set_at_path(&aspect_path, Data::Object(Object(aspect_obj))); 1989 + record_data.set_at_path(&aspect_path, aspect_obj); 2021 1990 }); 2022 1991 } 2023 1992 } ··· 2051 2020 }); 2052 2021 2053 2022 let path_for_mutation = path.clone(); 2054 - let handle_input = move |evt: dioxus::prelude::Event<dioxus::prelude::FormData>| { 2023 + let handle_input = move |evt: Event<dioxus::prelude::FormData>| { 2055 2024 let text = evt.value(); 2056 2025 input_text.set(text.clone()); 2057 2026 ··· 2202 2171 }); 2203 2172 2204 2173 let path_for_mutation = path.clone(); 2205 - let handle_input = move |evt: dioxus::prelude::Event<dioxus::prelude::FormData>| { 2174 + let handle_input = move |evt: Event<dioxus::prelude::FormData>| { 2206 2175 let text = evt.value(); 2207 2176 input_text.set(text.clone()); 2208 2177 ··· 2721 2690 match fetcher.send(request).await { 2722 2691 Ok(output) => { 2723 2692 if output.status() == StatusCode::OK { 2724 - dioxus_logger::tracing::info!("Record updated successfully"); 2693 + tracing::info!("Record updated successfully"); 2725 2694 edit_data.set(data.clone()); 2726 2695 edit_mode.set(false); 2727 2696 } else { 2728 - dioxus_logger::tracing::error!("Unexpected status code: {:?}", output.status()); 2697 + tracing::error!("Unexpected status code: {:?}", output.status()); 2729 2698 } 2730 2699 } 2731 2700 Err(e) => { 2732 - dioxus_logger::tracing::error!("Failed to update record: {:?}", e); 2701 + tracing::error!("Failed to update record: {:?}", e); 2733 2702 } 2734 2703 } 2735 2704 } ··· 2755 2724 match fetcher.send(request).await { 2756 2725 Ok(response) => { 2757 2726 if let Ok(output) = response.into_output() { 2758 - dioxus_logger::tracing::info!("Record created: {}", output.uri); 2727 + tracing::info!("Record created: {}", output.uri); 2759 2728 let link = format!("{}/record/{}", crate::env::WEAVER_APP_DOMAIN, output.uri); 2760 2729 nav.push(link); 2761 2730 } 2762 2731 } 2763 2732 Err(e) => { 2764 - dioxus_logger::tracing::error!("Failed to create record: {:?}", e); 2733 + tracing::error!("Failed to create record: {:?}", e); 2765 2734 } 2766 2735 } 2767 2736 } ··· 2800 2769 .build(); 2801 2770 2802 2771 if let Err(e) = fetcher.send(delete_req).await { 2803 - dioxus_logger::tracing::warn!("Created new record but failed to delete old: {:?}", e); 2772 + tracing::warn!("Created new record but failed to delete old: {:?}", e); 2804 2773 } 2805 2774 } 2806 2775 } 2807 2776 2808 - dioxus_logger::tracing::info!("Record replaced: {}", create_output.uri); 2777 + tracing::info!("Record replaced: {}", create_output.uri); 2809 2778 let link = format!("{}/record/{}", crate::env::WEAVER_APP_DOMAIN, create_output.uri); 2810 2779 nav.push(link); 2811 2780 } 2812 2781 } 2813 2782 Err(e) => { 2814 - dioxus_logger::tracing::error!("Failed to replace record: {:?}", e); 2783 + tracing::error!("Failed to replace record: {:?}", e); 2815 2784 } 2816 2785 } 2817 2786 } ··· 2836 2805 2837 2806 match fetcher.send(request).await { 2838 2807 Ok(_) => { 2839 - dioxus_logger::tracing::info!("Record deleted"); 2808 + tracing::info!("Record deleted"); 2840 2809 nav.push(Route::Home {}); 2841 2810 } 2842 2811 Err(e) => { 2843 - dioxus_logger::tracing::error!("Failed to delete record: {:?}", e); 2812 + tracing::error!("Failed to delete record: {:?}", e); 2844 2813 } 2845 2814 } 2846 2815 }
+126 -103
crates/weaver-common/src/agent.rs
··· 614 614 AgentError::from(ClientError::invalid_request("Invalid weaver profile URI")) 615 615 }, 616 616 )?; 617 - if let Ok(weaver_record) = self.fetch_record(&weaver_uri).await { 618 - // Convert blobs to CDN URLs 619 - let avatar = weaver_record 617 + let weaver_future = async { 618 + if let Ok(weaver_record) = self.fetch_record(&weaver_uri).await { 619 + // Convert blobs to CDN URLs 620 + let avatar = weaver_record 621 + .value 622 + .avatar 623 + .as_ref() 624 + .map(|blob| { 625 + let cid = blob.blob().cid(); 626 + jacquard::types::string::Uri::new_owned(format!( 627 + "https://cdn.bsky.app/img/avatar/plain/{}/{}", 628 + did, cid 629 + )) 630 + }) 631 + .transpose() 632 + .map_err(|_| { 633 + AgentError::from(ClientError::invalid_request("Invalid avatar URI")) 634 + })?; 635 + let banner = weaver_record 636 + .value 637 + .banner 638 + .as_ref() 639 + .map(|blob| { 640 + let cid = blob.blob().cid(); 641 + jacquard::types::string::Uri::new_owned(format!( 642 + "https://cdn.bsky.app/img/banner/plain/{}/{}", 643 + did, cid 644 + )) 645 + }) 646 + .transpose() 647 + .map_err(|_| { 648 + AgentError::from(ClientError::invalid_request("Invalid banner URI")) 649 + })?; 650 + 651 + let profile_view = ProfileView::new() 652 + .did(did.clone()) 653 + .handle(handle.clone()) 654 + .maybe_display_name(weaver_record.value.display_name.clone()) 655 + .maybe_description(weaver_record.value.description.clone()) 656 + .maybe_avatar(avatar) 657 + .maybe_banner(banner) 658 + .maybe_bluesky(weaver_record.value.bluesky) 659 + .maybe_tangled(weaver_record.value.tangled) 660 + .maybe_streamplace(weaver_record.value.streamplace) 661 + .maybe_location(weaver_record.value.location.clone()) 662 + .maybe_links(weaver_record.value.links.clone()) 663 + .maybe_pronouns(weaver_record.value.pronouns.clone()) 664 + .maybe_pinned(weaver_record.value.pinned.clone()) 665 + .indexed_at(jacquard::types::string::Datetime::now()) 666 + .maybe_created_at(weaver_record.value.created_at) 667 + .build(); 668 + 669 + Ok(( 670 + Some(weaver_uri.as_uri().clone().into_static()), 671 + ProfileDataView::new() 672 + .inner(ProfileDataViewInner::ProfileView(Box::new(profile_view))) 673 + .build() 674 + .into_static(), 675 + )) 676 + } else { 677 + Err(WeaverError::Agent( 678 + ClientError::invalid_request("Invalid weaver profile URI").into(), 679 + )) 680 + } 681 + }; 682 + let bsky_appview_future = async { 683 + if let Ok(bsky_resp) = self 684 + .send(GetProfile::new().actor(did.clone()).build()) 685 + .await 686 + { 687 + if let Ok(output) = bsky_resp.parse() { 688 + let bsky_uri = 689 + BskyProfile::uri(format!("at://{}/app.bsky.actor.profile/self", did)) 690 + .map_err(|_| { 691 + AgentError::from(ClientError::invalid_request( 692 + "Invalid bsky profile URI", 693 + )) 694 + })?; 695 + Ok(( 696 + Some(bsky_uri.as_uri().clone().into_static()), 697 + ProfileDataView::new() 698 + .inner(ProfileDataViewInner::ProfileViewDetailed(Box::new( 699 + output.value.into_static(), 700 + ))) 701 + .build() 702 + .into_static(), 703 + )) 704 + } else { 705 + Err(WeaverError::Agent( 706 + ClientError::invalid_request("Invalid bsky profile URI").into(), 707 + )) 708 + } 709 + } else { 710 + Err(WeaverError::Agent( 711 + ClientError::invalid_request("Invalid bsky profile URI").into(), 712 + )) 713 + } 714 + }; 715 + // Fallback: fetch bsky profile record directly and construct minimal ProfileViewDetailed 716 + let bsky_uri = BskyProfile::uri(format!("at://{}/app.bsky.actor.profile/self", did)) 717 + .map_err(|_| { 718 + AgentError::from(ClientError::invalid_request("Invalid bsky profile URI")) 719 + })?; 720 + 721 + let bsky_future = async { 722 + let bsky_record = self.fetch_record(&bsky_uri).await?; 723 + 724 + let avatar = bsky_record 620 725 .value 621 726 .avatar 622 727 .as_ref() ··· 631 736 .map_err(|_| { 632 737 AgentError::from(ClientError::invalid_request("Invalid avatar URI")) 633 738 })?; 634 - let banner = weaver_record 739 + let banner = bsky_record 635 740 .value 636 741 .banner 637 742 .as_ref() 638 743 .map(|blob| { 639 744 let cid = blob.blob().cid(); 640 745 jacquard::types::string::Uri::new_owned(format!( 641 - "https://cdn.bsky.app/img/banner/plain/{}/{}", 746 + "https://cdn.bsky.app/img/feed_fullsize/plain/{}/{}", 642 747 did, cid 643 748 )) 644 749 }) ··· 647 752 AgentError::from(ClientError::invalid_request("Invalid banner URI")) 648 753 })?; 649 754 650 - let profile_view = ProfileView::new() 755 + let profile_detailed = ProfileViewDetailed::new() 651 756 .did(did.clone()) 652 757 .handle(handle.clone()) 653 - .maybe_display_name(weaver_record.value.display_name.clone()) 654 - .maybe_description(weaver_record.value.description.clone()) 758 + .maybe_display_name(bsky_record.value.display_name.clone()) 759 + .maybe_description(bsky_record.value.description.clone()) 655 760 .maybe_avatar(avatar) 656 761 .maybe_banner(banner) 657 - .maybe_bluesky(weaver_record.value.bluesky) 658 - .maybe_tangled(weaver_record.value.tangled) 659 - .maybe_streamplace(weaver_record.value.streamplace) 660 - .maybe_location(weaver_record.value.location.clone()) 661 - .maybe_links(weaver_record.value.links.clone()) 662 - .maybe_pronouns(weaver_record.value.pronouns.clone()) 663 - .maybe_pinned(weaver_record.value.pinned.clone()) 664 762 .indexed_at(jacquard::types::string::Datetime::now()) 665 - .maybe_created_at(weaver_record.value.created_at) 763 + .maybe_created_at(bsky_record.value.created_at) 666 764 .build(); 667 765 668 - return Ok(( 669 - Some(weaver_uri.as_uri().clone().into_static()), 766 + Ok(( 767 + Some(bsky_uri.as_uri().clone().into_static()), 670 768 ProfileDataView::new() 671 - .inner(ProfileDataViewInner::ProfileView(Box::new(profile_view))) 769 + .inner(ProfileDataViewInner::ProfileViewDetailed(Box::new( 770 + profile_detailed, 771 + ))) 672 772 .build() 673 773 .into_static(), 674 - )); 675 - } 676 - 677 - if let Ok(bsky_resp) = self 678 - .send(GetProfile::new().actor(did.clone()).build()) 679 - .await 680 - { 681 - if let Ok(output) = bsky_resp.parse() { 682 - let bsky_uri = 683 - BskyProfile::uri(format!("at://{}/app.bsky.actor.profile/self", did)) 684 - .map_err(|_| { 685 - AgentError::from(ClientError::invalid_request( 686 - "Invalid bsky profile URI", 687 - )) 688 - })?; 689 - return Ok(( 690 - Some(bsky_uri.as_uri().clone().into_static()), 691 - ProfileDataView::new() 692 - .inner(ProfileDataViewInner::ProfileViewDetailed(Box::new( 693 - output.value.into_static(), 694 - ))) 695 - .build() 696 - .into_static(), 697 - )); 698 - } 699 - } 700 - 701 - // Fallback: fetch bsky profile record directly and construct minimal ProfileViewDetailed 702 - let bsky_uri = BskyProfile::uri(format!("at://{}/app.bsky.actor.profile/self", did)) 703 - .map_err(|_| { 704 - AgentError::from(ClientError::invalid_request("Invalid bsky profile URI")) 705 - })?; 706 - let bsky_record = self.fetch_record(&bsky_uri).await?; 707 - 708 - let avatar = bsky_record 709 - .value 710 - .avatar 711 - .as_ref() 712 - .map(|blob| { 713 - let cid = blob.blob().cid(); 714 - jacquard::types::string::Uri::new_owned(format!( 715 - "https://cdn.bsky.app/img/avatar/plain/{}/{}", 716 - did, cid 717 - )) 718 - }) 719 - .transpose() 720 - .map_err(|_| { 721 - AgentError::from(ClientError::invalid_request("Invalid avatar URI")) 722 - })?; 723 - let banner = bsky_record 724 - .value 725 - .banner 726 - .as_ref() 727 - .map(|blob| { 728 - let cid = blob.blob().cid(); 729 - jacquard::types::string::Uri::new_owned(format!( 730 - "https://cdn.bsky.app/img/feed_fullsize/plain/{}/{}", 731 - did, cid 732 - )) 733 - }) 734 - .transpose() 735 - .map_err(|_| { 736 - AgentError::from(ClientError::invalid_request("Invalid banner URI")) 737 - })?; 738 - 739 - let profile_detailed = ProfileViewDetailed::new() 740 - .did(did.clone()) 741 - .handle(handle.clone()) 742 - .maybe_display_name(bsky_record.value.display_name.clone()) 743 - .maybe_description(bsky_record.value.description.clone()) 744 - .maybe_avatar(avatar) 745 - .maybe_banner(banner) 746 - .indexed_at(jacquard::types::string::Datetime::now()) 747 - .maybe_created_at(bsky_record.value.created_at) 748 - .build(); 774 + )) 775 + }; 749 776 750 - Ok(( 751 - Some(bsky_uri.as_uri().clone().into_static()), 752 - ProfileDataView::new() 753 - .inner(ProfileDataViewInner::ProfileViewDetailed(Box::new( 754 - profile_detailed, 755 - ))) 756 - .build() 757 - .into_static(), 758 - )) 777 + n0_future::future::or( 778 + weaver_future, 779 + n0_future::future::or(bsky_appview_future, bsky_future), 780 + ) 781 + .await 759 782 } 760 783 } 761 784
+40
crates/weaver-renderer/src/css.rs
··· 90 90 max-width: 90ch; 91 91 margin: 0 auto; 92 92 padding: 2rem 1rem; 93 + word-wrap: break-word; 94 + overflow-wrap: break-word; 93 95 }} 94 96 95 97 /* Typography */ ··· 124 126 125 127 p {{ 126 128 margin-bottom: 1rem; 129 + word-wrap: break-word; 130 + overflow-wrap: break-word; 127 131 }} 128 132 129 133 a {{ ··· 209 213 border-collapse: collapse; 210 214 width: 100%; 211 215 margin-bottom: 1rem; 216 + display: block; 217 + overflow-x: auto; 218 + max-width: 100%; 212 219 }} 213 220 214 221 th, td {{ ··· 259 266 border: none; 260 267 border-top: 2px solid var(--color-border); 261 268 margin: 2rem 0; 269 + }} 270 + 271 + /* Tablet and mobile responsiveness */ 272 + @media (max-width: 900px) {{ 273 + .notebook-content {{ 274 + padding: 1.5rem 1rem; 275 + max-width: 100%; 276 + }} 277 + 278 + h1 {{ font-size: 1.85rem; }} 279 + h2 {{ font-size: 1.4rem; }} 280 + h3 {{ font-size: 1.2rem; }} 281 + 282 + blockquote {{ 283 + margin-left: 0; 284 + margin-right: 0; 285 + }} 286 + }} 287 + 288 + /* Small mobile phones */ 289 + @media (max-width: 480px) {{ 290 + .notebook-content {{ 291 + padding: 1rem 0.75rem; 292 + }} 293 + 294 + h1 {{ font-size: 1.65rem; }} 295 + h2 {{ font-size: 1.3rem; }} 296 + h3 {{ font-size: 1.1rem; }} 297 + 298 + blockquote {{ 299 + padding-left: 0.75rem; 300 + padding-right: 0.75rem; 301 + }} 262 302 }} 263 303 "#, 264 304 // Light mode colours
+15 -15
flake.lock
··· 3 3 "advisory-db": { 4 4 "flake": false, 5 5 "locked": { 6 - "lastModified": 1761631338, 7 - "narHash": "sha256-F6dlUrDiShwhMfPR+WoVmaQguGdEwjW9SI4nKlkay7c=", 6 + "lastModified": 1763052940, 7 + "narHash": "sha256-dt2Eze8JMyeCqpUPT1TF6XrGJR8QeD7UvUDaIrKngrE=", 8 8 "owner": "rustsec", 9 9 "repo": "advisory-db", 10 - "rev": "2e45336771e36acf5bcefe7c99280ab214719707", 10 + "rev": "4b6acc7020a23b61ae4963a075f0a4058cef79f2", 11 11 "type": "github" 12 12 }, 13 13 "original": { ··· 18 18 }, 19 19 "crane": { 20 20 "locked": { 21 - "lastModified": 1760924934, 22 - "narHash": "sha256-tuuqY5aU7cUkR71sO2TraVKK2boYrdW3gCSXUkF4i44=", 21 + "lastModified": 1762538466, 22 + "narHash": "sha256-8zrIPl6J+wLm9MH5ksHcW7BUHo7jSNOu0/hA0ohOOaM=", 23 23 "owner": "ipetkov", 24 24 "repo": "crane", 25 - "rev": "c6b4d5308293d0d04fcfeee92705017537cad02f", 25 + "rev": "0cea393fffb39575c46b7a0318386467272182fe", 26 26 "type": "github" 27 27 }, 28 28 "original": { ··· 39 39 "systems": "systems" 40 40 }, 41 41 "locked": { 42 - "lastModified": 1761886294, 43 - "narHash": "sha256-GEyOUuFbqWzpQIKO6mT2oCEp2JnUvNkOW/WsrHOFK6I=", 42 + "lastModified": 1763000036, 43 + "narHash": "sha256-yJiLpG1W2+yn3xvdeVTpt9CUYC2/VBLCCpKhEifdcsc=", 44 44 "owner": "DioxusLabs", 45 45 "repo": "dioxus", 46 - "rev": "2ab9a5586dc6b4d95e210eded692f50e7ab7575e", 46 + "rev": "bd2d1de873ae9c0e7c4656ec0c991ea9aafd3293", 47 47 "type": "github" 48 48 }, 49 49 "original": { ··· 137 137 }, 138 138 "nixpkgs_3": { 139 139 "locked": { 140 - "lastModified": 1761880412, 141 - "narHash": "sha256-QoJjGd4NstnyOG4mm4KXF+weBzA2AH/7gn1Pmpfcb0A=", 140 + "lastModified": 1762943920, 141 + "narHash": "sha256-ITeH8GBpQTw9457ICZBddQEBjlXMmilML067q0e6vqY=", 142 142 "owner": "NixOS", 143 143 "repo": "nixpkgs", 144 - "rev": "a7fc11be66bdfb5cdde611ee5ce381c183da8386", 144 + "rev": "91c9a64ce2a84e648d0cf9671274bb9c2fb9ba60", 145 145 "type": "github" 146 146 }, 147 147 "original": { ··· 200 200 "nixpkgs": "nixpkgs_4" 201 201 }, 202 202 "locked": { 203 - "lastModified": 1761878277, 204 - "narHash": "sha256-6fCtyVdTzoQejwoextAu7dCLoy5fyD3WVh+Qm7t2Nhg=", 203 + "lastModified": 1763087910, 204 + "narHash": "sha256-eB9Z1mWd1U6N61+F8qwDggX0ihM55s4E0CluwNukJRU=", 205 205 "owner": "oxalica", 206 206 "repo": "rust-overlay", 207 - "rev": "6604534e44090c917db714faa58d47861657690c", 207 + "rev": "cf4a68749733d45c0420726596367acd708eb2e8", 208 208 "type": "github" 209 209 }, 210 210 "original": {
+1
flake.nix
··· 262 262 diesel-cli 263 263 postgresql 264 264 cargo-insta 265 + cargo-bloat 265 266 jq 266 267 dioxus-cli 267 268 wasm-bindgen-cli
+53 -14
weaver_notes/.obsidian/workspace.json
··· 8 8 "type": "tabs", 9 9 "children": [ 10 10 { 11 - "id": "f41bdf1f327c8668", 11 + "id": "70e4a46d11e8ec6f", 12 12 "type": "leaf", 13 13 "state": { 14 14 "type": "markdown", 15 15 "state": { 16 - "file": "Arch.md", 16 + "file": "Why I rewrote pdsls in Rust (tm).md", 17 17 "mode": "source", 18 18 "source": false 19 19 }, 20 20 "icon": "lucide-file", 21 - "title": "Arch" 21 + "title": "Why I rewrote pdsls in Rust (tm)" 22 + } 23 + }, 24 + { 25 + "id": "bf69cdf6fbb617c2", 26 + "type": "leaf", 27 + "state": { 28 + "type": "markdown", 29 + "state": { 30 + "file": "Why I rewrote pdsls in Rust (tm).md", 31 + "mode": "source", 32 + "source": false 33 + }, 34 + "icon": "lucide-file", 35 + "title": "Why I rewrote pdsls in Rust (tm)" 36 + } 37 + }, 38 + { 39 + "id": "6029beecc3d03bce", 40 + "type": "leaf", 41 + "state": { 42 + "type": "markdown", 43 + "state": { 44 + "file": "bug notes.md", 45 + "mode": "source", 46 + "source": false 47 + }, 48 + "icon": "lucide-file", 49 + "title": "bug notes" 22 50 } 23 51 } 24 - ] 52 + ], 53 + "currentTab": 2 25 54 } 26 55 ], 27 56 "direction": "vertical" ··· 94 123 "state": { 95 124 "type": "backlink", 96 125 "state": { 97 - "file": "Arch.md", 126 + "file": "Why I rewrote pdsls in Rust (tm).md", 98 127 "collapseAll": false, 99 128 "extraContext": false, 100 129 "sortOrder": "alphabetical", ··· 104 133 "unlinkedCollapsed": true 105 134 }, 106 135 "icon": "links-coming-in", 107 - "title": "Backlinks for Arch" 136 + "title": "Backlinks for Why I rewrote pdsls in Rust (tm)" 108 137 } 109 138 }, 110 139 { ··· 142 171 "state": { 143 172 "type": "outline", 144 173 "state": { 145 - "file": "Weaver - Long-form writing.md", 174 + "file": "Why I rewrote pdsls in Rust (tm).md", 146 175 "followCursor": false, 147 176 "showSearch": false, 148 177 "searchQuery": "" 149 178 }, 150 179 "icon": "lucide-list", 151 - "title": "Outline of Weaver - Long-form writing" 180 + "title": "Outline of Why I rewrote pdsls in Rust (tm)" 152 181 } 153 182 } 154 - ] 183 + ], 184 + "currentTab": 3 155 185 } 156 186 ], 157 187 "direction": "horizontal", 158 - "width": 300 188 + "width": 300, 189 + "collapsed": true 159 190 }, 160 191 "left-ribbon": { 161 192 "hiddenItems": { ··· 168 199 "bases:Create new base": false 169 200 } 170 201 }, 171 - "active": "f41bdf1f327c8668", 202 + "active": "6029beecc3d03bce", 172 203 "lastOpenFiles": [ 173 - "Weaver - Long-form writing.md", 204 + "bug notes.md", 205 + "Why I rewrote pdsls in Rust (tm).md", 206 + "meta.png", 207 + "Pasted image 20251114125028.png", 208 + "invalid_record.png", 209 + "Pasted image 20251114125031.png", 210 + "Pasted image 20251114121431.png", 211 + "json_editor_with_errors.png", 212 + "pretty_editor.png", 174 213 "Arch.md", 214 + "Weaver - Long-form writing.md", 175 215 "weaver_photo_med.jpg", 176 216 "xkcd_345_excerpt.png", 177 - "light_mode_excerpt.png", 178 - "notebook_entry_preview.png" 217 + "light_mode_excerpt.png" 179 218 ] 180 219 }
weaver_notes/.trash/Pasted image 20251114121431.png

This is a binary file and will not be displayed.

weaver_notes/.trash/Pasted image 20251114125028.png

This is a binary file and will not be displayed.

weaver_notes/.trash/Pasted image 20251114125031.png

This is a binary file and will not be displayed.

weaver_notes/Arch.md weaver_notes/.trash/Arch.md
+105
weaver_notes/Why I rewrote pdsls in Rust (tm).md
··· 1 + ![[pretty_editor.png]] 2 + 3 + There are a few generic atproto record viewers out there [pdsls.dev](https://pdsls.dev), [atp.tools](https://atp.tools), a number of others, [anisota.net](https://anisota.net) recently just built one into their webapp), but only really one editor, that being pdsls. It's a good webapp. It's simple and does what it says on the tin, runs entirely client-side. So why an alternative? There's one personal motivation which I'll leave to the wayside, so I'll focus on the UX. Its record editing is exactly what you need. You get a nice little editor window to type or paste or upload JSON into. It has some syntax highlighting. It optionally validates that against a lexicon (determined by the `$type` NSID, if it can resolve it), gives you a little indicator if it's valid according the the schema, and you can make a new record, or update the current one, or delete it. This is all well and good. 4 + 5 + But what if you want to know where a schema violation occurs in the record and what it is? What if you want to add another item to an array where the item schema is big and complex? You can copy-paste text around, but the editor has no notion of the abstract syntax tree of an atproto data model type, it simply gives you a thumbs-up, thumbs-down. And if you want to do something like upload a blob, like an image, ~~you have to figure out how to do that separately and populate all the info manually~~ whoops, it does have that now, missed it in the interface writing the initial version of this. Nothing wrong with that, it all works well and there's lots of clever little convenience features like direct links to the getRecord url and query for a record, constellation backlinks, a really nice OpenAPI-esque lexicon schema display, and so on. 6 + ## Debugging tools and learning 7 + 8 + But regardless, I was frustrated with it, I needed a debugging tool for Weaver records (as I'd already evolved a schema or two in ways that invalidated my own records during testing, which required manual editing), felt the atproto ecosystem deserved a second option for this use case, and I also wanted to exercise some skills before I built the next major part of Weaver, that being the editor. 9 + 10 + The first pass at that isn't going to have the collaborative editing I'd like it to have, there's more back-end and protocol work required on that front yet. But I want it to be a nice, solid markdown editor which feels good to use. Specifically, I want it to feel basically like a simpler version of Obsidian's editor, which is what I'm using to compose this. The hybrid compose view it defaults to is perfect for markdown. It will likely have a toolbar rather than rely entirely on key combinations and text input, as I know that that will be useful to people who aren't familiar with Markdown's syntax, but it won't have a preview window, unless you want it to, and it should convey accurately what you're going to get. 11 + 12 + ![[json_editor_with_errors.png]] 13 + 14 + That meant I needed to get more familiar with how Dioxus does UI. The notebook entry viewer is basically a very thin UI wrapper around a markdown-to-html converter. The editor can take advantage of that to some degree, but the needs of editing mean it can't just be that plus a text box, not if there's to be a chance in hell of me using it, versus pasting content in from Obsidian, or manually publishing from the CLI. Plus, I have something of a specific aesthetic I'm trying to go for with Weaver, and I wanted more space to refine that vision and explore stylistic choices. 15 + 16 + >I feel I should clarify that making the record editor use Iosevka mimicking Berkeley Mono as its only font doesn't reflect my vision for the rest of the interface, it just kinda felt right for this specific thing. 17 + 18 + So, what should an evolution on a record viewer and editor have? Programmer's text editors and language servers have a lot to teach us here. Your editor should tell you what type a thing is if it's not already obvious. It should show you where an error happened, it should tell you what the error is, and it should help guide you into not making errors, as well as providing convenience features for making common changes. 19 + 20 + ![[invalid_record.png]] 21 + 22 + This helps people when they're manually editing, but it also helps people check that what their app is generating is valid, so long as they have a schema we can resolve and validate it against. ATproto apps tend to be pretty permissive when it comes to what they accept and display, as is [generally wise](https://en.wikipedia.org/wiki/Robustness_principle), but the above record, for example, breaks Jetstream because whatever tool was used to create it set that empty string `$type` field, perhaps instead of skipping the embed field for a post with no embeds. 23 + ## Field-testing Jacquard's features 24 + Another driver behind this was a desire to field-test a number of the unique features of the atproto library I built for Weaver, [Jacquard](https://tangled.org/@nonbinary.computer/jacquard). I've written more about one aspect of its design philosophy and architecture [here](https://alpha.weaver.sh/did:plc:yfvwmnlztr4dwkb7hwz55r2g/Jacquard/Jacquard%20Magic) but since that post I've added a couple pretty powerful features and I needed to give them a shakedown run. One was runtime lexicon resolution and schema validation. The other was the new tools for working with generic atproto data without strong types, like the path query syntax and functions. 25 + 26 + For the former, I first had to get lexicon resolution working in WebAssembly, which mean getting DNS resolution working in WebAssembly (the `dns` feature in `jacquard-identity` uses `hickory-resolver`, which only includes support for tokio and async-std runtimes by default, neither of which support `wasm32-unknown-unknown`, the web wasm target. I went with the DNS over HTTPS route, making calls to Cloudflare's API when the `dns` feature is disabled for DNS TXT resolution (for both handles and lexicons). At some point I'll make this somewhat more pluggable, so as not to introduce a direct dependency on a specific vendor API, but for the moment this works well. 27 + 28 + For the latter, well that's literally what drives the pretty editor. There's a single `Data<'static>` stored in a Dioxus `Signal`, which is path-indexed into directly for the field display and for editing. 29 + ```rust 30 + let string_type = use_memo(move || { 31 + root.read() 32 + .get_at_path(&path_for_type) 33 + .and_then(|d| match d { 34 + Data::String(s) => Some(s.string_type()), 35 + _ => None, 36 + }) 37 + .unwrap_or(LexiconStringType::String) 38 + }); 39 + /* --- SNIP --- */ 40 + let path_for_mutation = path.clone(); 41 + let handle_input = move |evt: Event<FormData>| { 42 + let new_text = evt.value(); 43 + input_text.set(new_text.clone()); 44 + 45 + match try_parse_as_type(&new_text, string_type()) { 46 + Ok(new_atproto_str) => { 47 + parse_error.set(None); 48 + let mut new_data = root.read().clone(); 49 + new_data.set_at_path(&path_for_mutation, Data::String(new_atproto_str)); 50 + root.set(new_data); 51 + } 52 + Err(e) => { 53 + parse_error.set(Some(e)); 54 + } 55 + } 56 + }; 57 + ``` 58 + 59 + And the path queries are what make the blob upload interface able to do things like auto-populate sibling fields in an image embed, like the aspect ratio. 60 + ```rust 61 + fn populate_aspect_ratio( 62 + mut root: Signal<Data<'static>>, 63 + blob_path: &str, 64 + width: i64, 65 + height: i64, 66 + ) { 67 + // Query for all aspectRatio fields and collect the path we want 68 + let aspect_path_to_update = { 69 + let data = root.read(); 70 + let query_result = data.query("...aspectRatio"); 71 + query_result.multiple().and_then(|matches| { 72 + // Find aspectRatio that's a sibling of our blob 73 + // e.g. blob at "embed.images[0].image" -> look for "embed.images[0].aspectRatio" 74 + let blob_parent = blob_path.rsplit_once('.').map(|(parent, _)| parent); 75 + 76 + matches.iter().find_map(|query_match| { 77 + let aspect_parent = query_match.path.rsplit_once('.').map(|(parent, _)| parent); 78 + if blob_parent == aspect_parent { 79 + Some(query_match.path.clone()) 80 + } else { 81 + None 82 + } 83 + }) 84 + }) 85 + }; 86 + // Update the aspectRatio if we found a matching field 87 + if let Some(aspect_path) = aspect_path_to_update { 88 + let aspect_obj = atproto! {{ 89 + "width": width, 90 + "height": height 91 + }}; 92 + root.with_mut(|record_data| { 93 + record_data.set_at_path(&aspect_path, aspect_obj); 94 + }); 95 + } 96 + } 97 + ``` 98 + 99 + They also drive the in-situ error display you saw earlier. The lexicon schema validator reports the path of an error in the data, and we can then check that path as we iterate through during the render to know where we should display said error. And yes, I did have to make further improvements to the querying (including adding the mutable reference queries and `set_at_path()` method to enable editing). 100 + 101 + >You might also notice the use of the `atproto!{}` macro above. This works just like the `json!` macro from `serde_json` and the `ipld!` macro from `ipld_core` (in fact it's *mostly* cribbed from the latter). It's been in Jacquard since almost the beginning, but I haven't shown it off much. It's not super well-developed beyond the simple cases, but it works reasonably well and is more compact than building the object manually. 102 + 103 + The upshot of all this is that building this meant I discovered a bunch of bugs in my own code, found a bunch of places where my interfaces weren't as nice as they could be, and made some stuff that I'll probably upstream into my own library after testing them in the Weaver web-app (like an abstraction over unauthenticated requests and an authenticated OAuth session, or an OAuth storage implementation using browser LocalStorage, and so on). Working on this also meant that the webapp got enough in it that I felt comfortable doing a bit of a soft-launch of something real under the [*.weaver.sh](https://alpha.weaver.sh) domain. 104 + ## Amusing meta image 105 + ![[meta.png]]
+3
weaver_notes/bug notes.md
··· 1 + - renderer/processor doesn't add http(s):// to urls without them (or handle autolinks currently) 2 + - mobile view doesn't narrow correctly 3 + -
weaver_notes/invalid_record.png

This is a binary file and will not be displayed.

weaver_notes/json_editor_with_errors.png

This is a binary file and will not be displayed.

weaver_notes/meta.png

This is a binary file and will not be displayed.

weaver_notes/pretty_editor.png

This is a binary file and will not be displayed.