few fixes, incl css

Orual 766491ce 1f3e4380

+504 -404
+42 -151
Cargo.lock
··· 22 ] 23 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 name = "addr2line" 38 version = "0.25.1" 39 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 218 "pin-project-lite", 219 "tokio", 220 ] 221 222 [[package]] 223 name = "async-recursion" ··· 1417 ] 1418 1419 [[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 name = "der" 1435 version = "0.7.10" 1436 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1618 "serde", 1619 "subsecond", 1620 "warnings", 1621 ] 1622 1623 [[package]] ··· 2660 ] 2661 2662 [[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 name = "fastrand" 2676 version = "2.3.0" 2677 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3913 ] 3914 3915 [[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 name = "indexmap" 3962 version = "1.9.3" 3963 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4082 dependencies = [ 4083 "bytes", 4084 "getrandom 0.2.16", 4085 "http", 4086 "jacquard-api", 4087 "jacquard-common", ··· 4090 "jacquard-oauth", 4091 "jose-jwk", 4092 "miette 7.6.0", 4093 - "n0-future", 4094 "regex", 4095 "reqwest", 4096 "serde", ··· 4209 "jacquard-lexicon", 4210 "miette 7.6.0", 4211 "mini-moka", 4212 - "n0-future", 4213 "percent-encoding", 4214 "reqwest", 4215 "serde", ··· 4263 "jose-jwa", 4264 "jose-jwk", 4265 "miette 7.6.0", 4266 - "n0-future", 4267 "p256", 4268 "rand 0.8.5", 4269 "rouille", ··· 4630 checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" 4631 4632 [[package]] 4633 name = "longest-increasing-subsequence" 4634 version = "0.1.0" 4635 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4690 ] 4691 4692 [[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 name = "malloc_buf" 4742 version = "0.0.6" 4743 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 6699 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 6700 6701 [[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 name = "sec1" 6714 version = "0.7.3" 6715 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 7262 source = "registry+https://github.com/rust-lang/crates.io-index" 7263 checksum = "35c6d746902bca4ddf16592357eacf0473631ea26b36072f0dd0b31fa5ccd1f4" 7264 dependencies = [ 7265 - "indexed_db_futures", 7266 "js-sys", 7267 "once_cell", 7268 "thiserror 2.0.17", ··· 8531 ] 8532 8533 [[package]] 8534 name = "wasm-streams" 8535 version = "0.4.2" 8536 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 8658 "chrono", 8659 "console_error_panic_hook", 8660 "dashmap", 8661 - "diesel", 8662 - "diesel_migrations", 8663 "dioxus", 8664 "dioxus-free-icons", 8665 - "dioxus-logger", 8666 "dioxus-primitives", 8667 "dotenvy", 8668 "gloo-storage", 8669 - "hex_fmt", 8670 "http", 8671 "humansize", 8672 "jacquard", 8673 "jacquard-axum", 8674 "jacquard-lexicon", 8675 "js-sys", 8676 "markdown-weaver", 8677 "mime-sniffer", 8678 "mini-moka", ··· 8681 "serde", 8682 "serde_html_form", 8683 "serde_json", 8684 - "sqlite-wasm-rs", 8685 "time", 8686 "tokio", 8687 "tracing",
··· 22 ] 23 24 [[package]] 25 name = "addr2line" 26 version = "0.25.1" 27 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 206 "pin-project-lite", 207 "tokio", 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" 215 216 [[package]] 217 name = "async-recursion" ··· 1411 ] 1412 1413 [[package]] 1414 name = "der" 1415 version = "0.7.10" 1416 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1598 "serde", 1599 "subsecond", 1600 "warnings", 1601 + "wasm-splitter", 1602 ] 1603 1604 [[package]] ··· 2641 ] 2642 2643 [[package]] 2644 name = "fastrand" 2645 version = "2.3.0" 2646 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3882 ] 3883 3884 [[package]] 3885 name = "indexmap" 3886 version = "1.9.3" 3887 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4006 dependencies = [ 4007 "bytes", 4008 "getrandom 0.2.16", 4009 + "gloo-storage", 4010 "http", 4011 "jacquard-api", 4012 "jacquard-common", ··· 4015 "jacquard-oauth", 4016 "jose-jwk", 4017 "miette 7.6.0", 4018 "regex", 4019 "reqwest", 4020 "serde", ··· 4133 "jacquard-lexicon", 4134 "miette 7.6.0", 4135 "mini-moka", 4136 "percent-encoding", 4137 "reqwest", 4138 "serde", ··· 4186 "jose-jwa", 4187 "jose-jwk", 4188 "miette 7.6.0", 4189 "p256", 4190 "rand 0.8.5", 4191 "rouille", ··· 4552 checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" 4553 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]] 4564 name = "longest-increasing-subsequence" 4565 version = "0.1.0" 4566 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4621 ] 4622 4623 [[package]] 4624 name = "malloc_buf" 4625 version = "0.0.6" 4626 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 6582 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 6583 6584 [[package]] 6585 name = "sec1" 6586 version = "0.7.3" 6587 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 7134 source = "registry+https://github.com/rust-lang/crates.io-index" 7135 checksum = "35c6d746902bca4ddf16592357eacf0473631ea26b36072f0dd0b31fa5ccd1f4" 7136 dependencies = [ 7137 "js-sys", 7138 "once_cell", 7139 "thiserror 2.0.17", ··· 8402 ] 8403 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]] 8429 name = "wasm-streams" 8430 version = "0.4.2" 8431 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 8553 "chrono", 8554 "console_error_panic_hook", 8555 "dashmap", 8556 "dioxus", 8557 "dioxus-free-icons", 8558 "dioxus-primitives", 8559 "dotenvy", 8560 "gloo-storage", 8561 "http", 8562 "humansize", 8563 "jacquard", 8564 "jacquard-axum", 8565 "jacquard-lexicon", 8566 "js-sys", 8567 + "lol_alloc", 8568 "markdown-weaver", 8569 "mime-sniffer", 8570 "mini-moka", ··· 8573 "serde", 8574 "serde_html_form", 8575 "serde_json", 8576 "time", 8577 "tokio", 8578 "tracing",
+4
Cargo.toml
··· 59 [profile.wasm-dev] 60 inherits = "dev" 61 opt-level = 1 62 63 [profile.server-dev] 64 inherits = "dev" 65 66 [profile.android-dev] 67 inherits = "dev"
··· 59 [profile.wasm-dev] 60 inherits = "dev" 61 opt-level = 1 62 + lto = true 63 + debug = true 64 65 [profile.server-dev] 66 inherits = "dev" 67 + lto = true 68 + debug = true 69 70 [profile.android-dev] 71 inherits = "dev"
+10 -9
crates/weaver-app/Cargo.toml
··· 8 default = ["web", "fullstack-server", "no-app-index"] 9 # Fullstack mode with SSR and server functions 10 fullstack-server = ["dioxus/fullstack"] 11 no-app-index = [] 12 13 web = ["dioxus/web"] ··· 22 dioxus = { version = "0.7.1", features = ["router"] } 23 #dioxus-router = { version = "0.7.1", features = ["wasm-split"] } 24 weaver-common = { path = "../weaver-common" } 25 - jacquard = { workspace = true, features = ["streaming"] } 26 jacquard-lexicon = { workspace = true } 27 jacquard-axum = { workspace = true, optional = true } 28 - weaver-api = { path = "../weaver-api", features = ["streaming"] } 29 markdown-weaver = { workspace = true } 30 weaver-renderer = { path = "../weaver-renderer" } 31 mini-moka = { git = "https://github.com/moka-rs/mini-moka", rev = "da864e849f5d034f32e02197fee9bb5d5af36d3d" } ··· 34 axum = {version = "0.8.6", optional = true} 35 mime-sniffer = {version = "^0.1"} 36 chrono = { version = "0.4" } 37 - serde = { version = "1.0", features = ["derive"] } 38 serde_json = "1.0" 39 - hex_fmt = "0.3" 40 humansize = "2.0.0" 41 base64 = "0.22" 42 http = "1.3" 43 reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } 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"] } 47 tokio = { version = "1.28", features = ["sync"] } 48 - dioxus-logger = "0.7.1" 49 serde_html_form = "0.2.8" 50 - webbrowser = "1.0.6" 51 tracing.workspace = true 52 53 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"] } 56 time = { version = "0.3", features = ["wasm-bindgen"] } 57 console_error_panic_hook = "0.1" 58 mini-moka = { git = "https://github.com/moka-rs/mini-moka", rev = "da864e849f5d034f32e02197fee9bb5d5af36d3d", features = ["js"] } ··· 62 web-sys = { version = "0.3", features = ["ServiceWorkerContainer", "ServiceWorker", "ServiceWorkerRegistration", "RegistrationOptions", "Window", "Navigator", "MessageEvent", "console", "Document", "Element", "HtmlImageElement"] } 63 js-sys = "0.3" 64 gloo-storage = "0.3" 65 66 [build-dependencies] 67 dotenvy = "0.15.7"
··· 8 default = ["web", "fullstack-server", "no-app-index"] 9 # Fullstack mode with SSR and server functions 10 fullstack-server = ["dioxus/fullstack"] 11 + wasm-split = ["dioxus/wasm-split"] 12 no-app-index = [] 13 14 web = ["dioxus/web"] ··· 23 dioxus = { version = "0.7.1", features = ["router"] } 24 #dioxus-router = { version = "0.7.1", features = ["wasm-split"] } 25 weaver-common = { path = "../weaver-common" } 26 + jacquard = { workspace = true}#, features = ["streaming"] } 27 jacquard-lexicon = { workspace = true } 28 jacquard-axum = { workspace = true, optional = true } 29 + weaver-api = { path = "../weaver-api"}#, features = ["streaming"] } 30 markdown-weaver = { workspace = true } 31 weaver-renderer = { path = "../weaver-renderer" } 32 mini-moka = { git = "https://github.com/moka-rs/mini-moka", rev = "da864e849f5d034f32e02197fee9bb5d5af36d3d" } ··· 35 axum = {version = "0.8.6", optional = true} 36 mime-sniffer = {version = "^0.1"} 37 chrono = { version = "0.4" } 38 + serde = { version = "1.0"} #, features = ["derive"] } 39 serde_json = "1.0" 40 humansize = "2.0.0" 41 base64 = "0.22" 42 http = "1.3" 43 reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } 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"] } 47 tokio = { version = "1.28", features = ["sync"] } 48 serde_html_form = "0.2.8" 49 tracing.workspace = true 50 51 + [target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies] 52 + webbrowser = "1.0.6" 53 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"] } 56 time = { version = "0.3", features = ["wasm-bindgen"] } 57 console_error_panic_hook = "0.1" 58 mini-moka = { git = "https://github.com/moka-rs/mini-moka", rev = "da864e849f5d034f32e02197fee9bb5d5af36d3d", features = ["js"] } ··· 62 web-sys = { version = "0.3", features = ["ServiceWorkerContainer", "ServiceWorker", "ServiceWorkerRegistration", "RegistrationOptions", "Window", "Navigator", "MessageEvent", "console", "Document", "Element", "HtmlImageElement"] } 63 js-sys = "0.3" 64 gloo-storage = "0.3" 65 + lol_alloc = "0.4.1" 66 67 [build-dependencies] 68 dotenvy = "0.15.7"
+41 -1
crates/weaver-app/assets/styling/entry.css
··· 9 max-width: calc(90ch + 400px + 4rem); /* content + gutters + gaps */ 10 margin: 0 auto; 11 padding: 0 1rem 0 0; 12 } 13 14 /* Main content area */ ··· 203 background: var(--color-surface); 204 } 205 206 - /* Responsive layout */ 207 @media (max-width: 1400px) { 208 .entry-page-layout { 209 grid-template-columns: 1fr; ··· 236 order: 1; 237 } 238 }
··· 9 max-width: calc(90ch + 400px + 4rem); /* content + gutters + gaps */ 10 margin: 0 auto; 11 padding: 0 1rem 0 0; 12 + box-sizing: border-box; 13 } 14 15 /* Main content area */ ··· 204 background: var(--color-surface); 205 } 206 207 + /* Responsive layout - Tablet/small desktop */ 208 @media (max-width: 1400px) { 209 .entry-page-layout { 210 grid-template-columns: 1fr; ··· 237 order: 1; 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 934 .accordion-content { 935 grid-template-rows: 1fr; 936 - padding-left: 2.8rem; 937 } 938 939 .accordion-content .section-content { ··· 969 } 970 971 .accordion { 972 - margin-left: -2.8rem; 973 }
··· 933 934 .accordion-content { 935 grid-template-rows: 1fr; 936 + padding-left: 46px; 937 } 938 939 .accordion-content .section-content { ··· 969 } 970 971 .accordion { 972 + margin-left: -46px; 973 }
+3 -2
crates/weaver-app/src/auth/mod.rs
··· 34 use crate::fetch::CachedFetcher; 35 use dioxus::prelude::*; 36 use gloo_storage::{LocalStorage, Storage}; 37 // Look for session keys in localStorage (format: oauth_session_{did}_{session_id}) 38 let keys = LocalStorage::get_all::<serde_json::Value>()?; 39 let mut found_session: Option<(String, String)> = None; ··· 57 58 let (did_str, session_id) = 59 found_session.ok_or(CapturedError::from_display("No saved session found"))?; 60 - let did = jacquard::types::string::Did::new_owned(did_str)?; 61 let fetcher = use_context::<CachedFetcher>(); 62 63 let session = fetcher ··· 77 .set_authenticated(restored_did, session_id); 78 fetcher.upgrade_to_authenticated(session).await; 79 80 - dioxus_logger::tracing::debug!("session restored"); 81 Ok(()) 82 }
··· 34 use crate::fetch::CachedFetcher; 35 use dioxus::prelude::*; 36 use gloo_storage::{LocalStorage, Storage}; 37 + use jacquard::types::string::Did; 38 // Look for session keys in localStorage (format: oauth_session_{did}_{session_id}) 39 let keys = LocalStorage::get_all::<serde_json::Value>()?; 40 let mut found_session: Option<(String, String)> = None; ··· 58 59 let (did_str, session_id) = 60 found_session.ok_or(CapturedError::from_display("No saved session found"))?; 61 + let did = Did::new_owned(did_str)?; 62 let fetcher = use_context::<CachedFetcher>(); 63 64 let session = fetcher ··· 78 .set_authenticated(restored_did, session_id); 79 fetcher.upgrade_to_authenticated(session).await; 80 81 + tracing::debug!("session restored"); 82 Ok(()) 83 }
+1 -1
crates/weaver-app/src/auth/storage.rs
··· 102 Ok(Some(data)) 103 } 104 Err(gloo_storage::errors::StorageError::KeyNotFound(err)) => { 105 - dioxus_logger::tracing::debug!("gloo error: {}", err); 106 Ok(None) 107 } 108 Err(e) => Err(SessionStoreError::Other(
··· 102 Ok(Some(data)) 103 } 104 Err(gloo_storage::errors::StorageError::KeyNotFound(err)) => { 105 + tracing::debug!("gloo error: {}", err); 106 Ok(None) 107 } 108 Err(e) => Err(SessionStoreError::Other(
+1 -1
crates/weaver-app/src/fetch.rs
··· 1 use crate::auth::AuthStore; 2 use crate::cache_impl; 3 use dioxus::Result; 4 - use dioxus::prelude::*; 5 use jacquard::AuthorizationToken; 6 use jacquard::CowStr; 7 use jacquard::IntoStatic; ··· 372 } 373 } 374 375 pub async fn current_did(&self) -> Option<Did<'static>> { 376 let session_slot = self.client.session.read().await; 377 if let Some(session) = session_slot.as_ref() {
··· 1 use crate::auth::AuthStore; 2 use crate::cache_impl; 3 use dioxus::Result; 4 use jacquard::AuthorizationToken; 5 use jacquard::CowStr; 6 use jacquard::IntoStatic; ··· 371 } 372 } 373 374 + #[allow(dead_code)] 375 pub async fn current_did(&self) -> Option<Did<'static>> { 376 let session_slot = self.client.session.read().await; 377 if let Some(session) = session_slot.as_ref() {
+11 -26
crates/weaver-app/src/main.rs
··· 8 use dioxus::fullstack::FullstackContext; 9 #[cfg(all(feature = "fullstack-server", feature = "server"))] 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 - }; 16 #[allow(unused)] 17 use jacquard::{ 18 smol_str::SmolStr, ··· 20 }; 21 #[cfg(feature = "server")] 22 use std::sync::Arc; 23 - use std::sync::{LazyLock, Mutex}; 24 #[allow(unused)] 25 use views::{ 26 Callback, Home, Navbar, Notebook, NotebookIndex, NotebookPage, RecordIndex, RecordView, ··· 45 /// Define a views module that contains the UI for all Layouts and Routes for our app. 46 mod views; 47 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. 53 #[derive(Debug, Clone, Routable, PartialEq)] 54 #[rustfmt::skip] 55 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 #[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 #[route("/")] 62 Home {}, 63 #[layout(ErrorLayout)] ··· 81 Entry { ident: AtIdentifier<'static>, book_title: SmolStr, title: SmolStr } 82 83 } 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 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 const MAIN_CSS: Asset = asset!("/assets/styling/main.css"); 90 91 #[cfg(not(feature = "fullstack-server"))] ··· 104 oauth: OAuthConfig::from_env().as_metadata(), 105 }); 106 fn main() { 107 - dioxus_logger::init(Level::DEBUG).expect("logger failed to init"); 108 // Set up better panic messages for wasm 109 #[cfg(target_arch = "wasm32")] 110 console_error_panic_hook::set_once(); ··· 148 async move { 149 req.extensions_mut().insert(blob_cache); 150 req.extensions_mut().insert(fetcher); 151 - 152 - // And then return the response with `next.run() 153 Ok::<_, Infallible>(next.run(req).await) 154 } 155 } 156 })) 157 }; 158 - // And then return the router 159 Ok(router) 160 }); 161 162 - // When not on the server, just run `launch()` like normal 163 #[cfg(not(feature = "server"))] 164 dioxus::launch(App); 165 } ··· 177 use_effect(move || { 178 spawn(async move { 179 if let Err(e) = auth::restore_session().await { 180 - dioxus_logger::tracing::warn!("Session restoration failed: {}", e); 181 } 182 }); 183 }); 184 185 - // Register service worker on startup (only on web) 186 #[cfg(all( 187 target_family = "wasm", 188 target_os = "unknown",
··· 8 use dioxus::fullstack::FullstackContext; 9 #[cfg(all(feature = "fullstack-server", feature = "server"))] 10 use dioxus::fullstack::response::Extension; 11 + use jacquard::oauth::{client::OAuthClient, session::ClientData}; 12 #[allow(unused)] 13 use jacquard::{ 14 smol_str::SmolStr, ··· 16 }; 17 #[cfg(feature = "server")] 18 use std::sync::Arc; 19 + use std::sync::LazyLock; 20 #[allow(unused)] 21 use views::{ 22 Callback, Home, Navbar, Notebook, NotebookIndex, NotebookPage, RecordIndex, RecordView, ··· 41 /// Define a views module that contains the UI for all Layouts and Routes for our app. 42 mod views; 43 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 + 52 #[derive(Debug, Clone, Routable, PartialEq)] 53 #[rustfmt::skip] 54 enum Route { 55 #[layout(Navbar)] 56 #[route("/")] 57 Home {}, 58 #[layout(ErrorLayout)] ··· 76 Entry { ident: AtIdentifier<'static>, book_title: SmolStr, title: SmolStr } 77 78 } 79 const FAVICON: Asset = asset!("/assets/weaver_photo_sm.jpg"); 80 const MAIN_CSS: Asset = asset!("/assets/styling/main.css"); 81 82 #[cfg(not(feature = "fullstack-server"))] ··· 95 oauth: OAuthConfig::from_env().as_metadata(), 96 }); 97 fn main() { 98 // Set up better panic messages for wasm 99 #[cfg(target_arch = "wasm32")] 100 console_error_panic_hook::set_once(); ··· 138 async move { 139 req.extensions_mut().insert(blob_cache); 140 req.extensions_mut().insert(fetcher); 141 Ok::<_, Infallible>(next.run(req).await) 142 } 143 } 144 })) 145 }; 146 Ok(router) 147 }); 148 149 #[cfg(not(feature = "server"))] 150 dioxus::launch(App); 151 } ··· 163 use_effect(move || { 164 spawn(async move { 165 if let Err(e) = auth::restore_session().await { 166 + tracing::warn!("Session restoration failed: {}", e); 167 } 168 }); 169 }); 170 171 #[cfg(all( 172 target_family = "wasm", 173 target_os = "unknown",
+4
crates/weaver-app/src/service_worker.rs
··· 1 #[cfg(all(target_family = "wasm", target_os = "unknown"))] 2 use wasm_bindgen::prelude::*; 3 ··· 114 ) -> Result<(), String> { 115 Ok(()) 116 }
··· 1 + use dioxus::prelude::*; 2 #[cfg(all(target_family = "wasm", target_os = "unknown"))] 3 use wasm_bindgen::prelude::*; 4 ··· 115 ) -> Result<(), String> { 116 Ok(()) 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 use crate::auth::AuthState; 2 use crate::fetch::CachedFetcher; 3 - use dioxus::logger::tracing::{Level, error, info}; 4 - use dioxus::{CapturedError, prelude::*}; 5 use jacquard::{ 6 IntoStatic, 7 cowstr::ToCowStr, 8 oauth::{error::OAuthError, types::CallbackParams}, 9 smol_str::SmolStr, 10 }; 11 12 #[component] 13 pub fn Callback( ··· 46 47 match &*result.read_unchecked() { 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")] 56 { 57 use gloo_storage::Storage; 58 let mut prev = gloo_storage::LocalStorage::get::<String>("cached_route").ok(); 59 if let Some(prev) = prev.take() { 60 - dioxus_logger::tracing::info!("Navigating to previous page"); 61 let nav = use_navigator(); 62 gloo_storage::LocalStorage::delete("cached_route"); 63 nav.replace(prev);
··· 1 use crate::auth::AuthState; 2 use crate::fetch::CachedFetcher; 3 + use dioxus::prelude::*; 4 use jacquard::{ 5 IntoStatic, 6 cowstr::ToCowStr, 7 oauth::{error::OAuthError, types::CallbackParams}, 8 smol_str::SmolStr, 9 }; 10 + use tracing::{error, info}; 11 12 #[component] 13 pub fn Callback( ··· 46 47 match &*result.read_unchecked() { 48 Some(Ok(())) => { 49 + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] 50 { 51 use gloo_storage::Storage; 52 let mut prev = gloo_storage::LocalStorage::get::<String>("cached_route").ok(); 53 if let Some(prev) = prev.take() { 54 + tracing::info!("Navigating to previous page"); 55 let nav = use_navigator(); 56 gloo_storage::LocalStorage::delete("cached_route"); 57 nav.replace(prev);
+1 -1
crates/weaver-app/src/views/home.rs
··· 1 use crate::{Route, components::identity::NotebookCard, fetch}; 2 use dioxus::prelude::*; 3 - use jacquard::{IntoStatic, smol_str::ToSmolStr, types::aturi::AtUri}; 4 5 const NOTEBOOK_CARD_CSS: Asset = asset!("/assets/styling/notebook-card.css"); 6
··· 1 use crate::{Route, components::identity::NotebookCard, fetch}; 2 use dioxus::prelude::*; 3 + use jacquard::types::aturi::AtUri; 4 5 const NOTEBOOK_CARD_CSS: Asset = asset!("/assets/styling/notebook-card.css"); 6
+37 -68
crates/weaver-app/src/views/record.rs
··· 3 use crate::components::accordion::{Accordion, AccordionContent, AccordionItem, AccordionTrigger}; 4 use crate::components::dialog::{DialogContent, DialogDescription, DialogRoot, DialogTitle}; 5 use crate::fetch::CachedFetcher; 6 use dioxus::{CapturedError, prelude::*}; 7 use humansize::format_size; 8 use jacquard::api::com_atproto::repo::get_record::GetRecordOutput; 9 use jacquard::client::AgentError; 10 use jacquard::common::to_data; 11 - use jacquard::prelude::*; 12 use jacquard::smol_str::ToSmolStr; 13 use jacquard::{ 14 client::AgentSessionExt, 15 common::{Data, IntoStatic}, ··· 795 } 796 797 #[component] 798 - fn HighlightedString(string_type: jacquard::types::string::AtprotoStr<'static>) -> Element { 799 use jacquard::types::string::AtprotoStr; 800 801 match &string_type { ··· 1166 Index(usize), 1167 } 1168 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 /// Get all validation errors at exactly this path (not children) 1200 fn get_errors_at_exact_path( 1201 validation_result: &Option<ValidationResult>, ··· 1287 /// Parse text as specific AtprotoStr type, preserving type information 1288 fn try_parse_as_type( 1289 text: &str, 1290 - string_type: jacquard::types::LexiconStringType, 1291 - ) -> Result<jacquard::types::string::AtprotoStr<'static>, String> { 1292 - use jacquard::types::LexiconStringType; 1293 use jacquard::types::string::*; 1294 use std::str::FromStr; 1295 ··· 1360 } 1361 Data::Object(Object(new_obj)) 1362 } 1363 - 1364 Data::Array(_) => Data::Array(Array(Vec::new())), 1365 - 1366 Data::String(s) => match s.string_type() { 1367 LexiconStringType::Datetime => { 1368 // Sensible default: now 1369 Data::String(AtprotoStr::Datetime(Datetime::now())) 1370 } 1371 _ => { 1372 // Empty string, type inference will handle it 1373 Data::String(AtprotoStr::String("".into())) 1374 } 1375 }, 1376 - 1377 Data::Integer(_) => Data::Integer(0), 1378 Data::Boolean(_) => Data::Boolean(false), 1379 - 1380 Data::Blob(blob) => { 1381 // Placeholder blob 1382 Data::Blob( ··· 1390 .into_static(), 1391 ) 1392 } 1393 - 1394 - Data::Bytes(_) | Data::CidLink(_) | Data::Null => Data::Null, 1395 } 1396 } 1397 ··· 1488 }); 1489 1490 let path_for_mutation = path.clone(); 1491 - let handle_input = move |evt: dioxus::prelude::Event<dioxus::prelude::FormData>| { 1492 let new_text = evt.value(); 1493 input_text.set(new_text.clone()); 1494 ··· 1741 1742 let fetcher = use_context::<CachedFetcher>(); 1743 let path_for_upload = path.clone(); 1744 - let handle_file = move |evt: dioxus::prelude::Event<dioxus::prelude::FormData>| { 1745 let fetcher = fetcher.clone(); 1746 let path_upload_clone = path_for_upload.clone(); 1747 spawn(async move { ··· 1818 }; 1819 1820 let path_for_cid = path.clone(); 1821 - let handle_cid_change = move |evt: dioxus::prelude::Event<dioxus::prelude::FormData>| { 1822 let text = evt.value(); 1823 cid_input.set(text.clone()); 1824 ··· 1838 }; 1839 1840 let path_for_size = path.clone(); 1841 - let handle_size_change = move |evt: dioxus::prelude::Event<dioxus::prelude::FormData>| { 1842 let text = evt.value(); 1843 size_input.set(text.clone()); 1844 ··· 1992 // Find aspectRatio that's a sibling of our blob 1993 // e.g. blob at "embed.images[0].image" -> look for "embed.images[0].aspectRatio" 1994 let blob_parent = blob_path.rsplit_once('.').map(|(parent, _)| parent); 1995 - 1996 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); 1999 2000 // Check if they share the same parent 2001 if blob_parent == aspect_parent { 2002 - Some(aspect_path.to_string()) 2003 } else { 2004 None 2005 } ··· 2009 2010 // Update the aspectRatio if we found a matching field 2011 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)); 2018 2019 root.with_mut(|record_data| { 2020 - record_data.set_at_path(&aspect_path, Data::Object(Object(aspect_obj))); 2021 }); 2022 } 2023 } ··· 2051 }); 2052 2053 let path_for_mutation = path.clone(); 2054 - let handle_input = move |evt: dioxus::prelude::Event<dioxus::prelude::FormData>| { 2055 let text = evt.value(); 2056 input_text.set(text.clone()); 2057 ··· 2202 }); 2203 2204 let path_for_mutation = path.clone(); 2205 - let handle_input = move |evt: dioxus::prelude::Event<dioxus::prelude::FormData>| { 2206 let text = evt.value(); 2207 input_text.set(text.clone()); 2208 ··· 2721 match fetcher.send(request).await { 2722 Ok(output) => { 2723 if output.status() == StatusCode::OK { 2724 - dioxus_logger::tracing::info!("Record updated successfully"); 2725 edit_data.set(data.clone()); 2726 edit_mode.set(false); 2727 } else { 2728 - dioxus_logger::tracing::error!("Unexpected status code: {:?}", output.status()); 2729 } 2730 } 2731 Err(e) => { 2732 - dioxus_logger::tracing::error!("Failed to update record: {:?}", e); 2733 } 2734 } 2735 } ··· 2755 match fetcher.send(request).await { 2756 Ok(response) => { 2757 if let Ok(output) = response.into_output() { 2758 - dioxus_logger::tracing::info!("Record created: {}", output.uri); 2759 let link = format!("{}/record/{}", crate::env::WEAVER_APP_DOMAIN, output.uri); 2760 nav.push(link); 2761 } 2762 } 2763 Err(e) => { 2764 - dioxus_logger::tracing::error!("Failed to create record: {:?}", e); 2765 } 2766 } 2767 } ··· 2800 .build(); 2801 2802 if let Err(e) = fetcher.send(delete_req).await { 2803 - dioxus_logger::tracing::warn!("Created new record but failed to delete old: {:?}", e); 2804 } 2805 } 2806 } 2807 2808 - dioxus_logger::tracing::info!("Record replaced: {}", create_output.uri); 2809 let link = format!("{}/record/{}", crate::env::WEAVER_APP_DOMAIN, create_output.uri); 2810 nav.push(link); 2811 } 2812 } 2813 Err(e) => { 2814 - dioxus_logger::tracing::error!("Failed to replace record: {:?}", e); 2815 } 2816 } 2817 } ··· 2836 2837 match fetcher.send(request).await { 2838 Ok(_) => { 2839 - dioxus_logger::tracing::info!("Record deleted"); 2840 nav.push(Route::Home {}); 2841 } 2842 Err(e) => { 2843 - dioxus_logger::tracing::error!("Failed to delete record: {:?}", e); 2844 } 2845 } 2846 }
··· 3 use crate::components::accordion::{Accordion, AccordionContent, AccordionItem, AccordionTrigger}; 4 use crate::components::dialog::{DialogContent, DialogDescription, DialogRoot, DialogTitle}; 5 use crate::fetch::CachedFetcher; 6 + use dioxus::prelude::Event; 7 use dioxus::{CapturedError, prelude::*}; 8 use humansize::format_size; 9 use jacquard::api::com_atproto::repo::get_record::GetRecordOutput; 10 + use jacquard::bytes::Bytes; 11 use jacquard::client::AgentError; 12 use jacquard::common::to_data; 13 use jacquard::smol_str::ToSmolStr; 14 + use jacquard::types::LexiconStringType; 15 + use jacquard::types::string::AtprotoStr; 16 + use jacquard::{atproto, prelude::*}; 17 use jacquard::{ 18 client::AgentSessionExt, 19 common::{Data, IntoStatic}, ··· 799 } 800 801 #[component] 802 + fn HighlightedString(string_type: AtprotoStr<'static>) -> Element { 803 use jacquard::types::string::AtprotoStr; 804 805 match &string_type { ··· 1170 Index(usize), 1171 } 1172 1173 /// Get all validation errors at exactly this path (not children) 1174 fn get_errors_at_exact_path( 1175 validation_result: &Option<ValidationResult>, ··· 1261 /// Parse text as specific AtprotoStr type, preserving type information 1262 fn try_parse_as_type( 1263 text: &str, 1264 + string_type: LexiconStringType, 1265 + ) -> Result<AtprotoStr<'static>, String> { 1266 use jacquard::types::string::*; 1267 use std::str::FromStr; 1268 ··· 1333 } 1334 Data::Object(Object(new_obj)) 1335 } 1336 Data::Array(_) => Data::Array(Array(Vec::new())), 1337 Data::String(s) => match s.string_type() { 1338 LexiconStringType::Datetime => { 1339 // Sensible default: now 1340 Data::String(AtprotoStr::Datetime(Datetime::now())) 1341 } 1342 + LexiconStringType::Tid => Data::String(AtprotoStr::Tid(Tid::now_0())), 1343 _ => { 1344 // Empty string, type inference will handle it 1345 Data::String(AtprotoStr::String("".into())) 1346 } 1347 }, 1348 Data::Integer(_) => Data::Integer(0), 1349 Data::Boolean(_) => Data::Boolean(false), 1350 Data::Blob(blob) => { 1351 // Placeholder blob 1352 Data::Blob( ··· 1360 .into_static(), 1361 ) 1362 } 1363 + Data::CidLink(_) => Data::CidLink(Cid::str( 1364 + "bafkreiaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 1365 + )), 1366 + Data::Bytes(_) => Data::Bytes(Bytes::new()), 1367 + Data::Null => Data::Null, 1368 } 1369 } 1370 ··· 1461 }); 1462 1463 let path_for_mutation = path.clone(); 1464 + let handle_input = move |evt: Event<FormData>| { 1465 let new_text = evt.value(); 1466 input_text.set(new_text.clone()); 1467 ··· 1714 1715 let fetcher = use_context::<CachedFetcher>(); 1716 let path_for_upload = path.clone(); 1717 + let handle_file = move |evt: Event<dioxus::prelude::FormData>| { 1718 let fetcher = fetcher.clone(); 1719 let path_upload_clone = path_for_upload.clone(); 1720 spawn(async move { ··· 1791 }; 1792 1793 let path_for_cid = path.clone(); 1794 + let handle_cid_change = move |evt: Event<dioxus::prelude::FormData>| { 1795 let text = evt.value(); 1796 cid_input.set(text.clone()); 1797 ··· 1811 }; 1812 1813 let path_for_size = path.clone(); 1814 + let handle_size_change = move |evt: Event<dioxus::prelude::FormData>| { 1815 let text = evt.value(); 1816 size_input.set(text.clone()); 1817 ··· 1965 // Find aspectRatio that's a sibling of our blob 1966 // e.g. blob at "embed.images[0].image" -> look for "embed.images[0].aspectRatio" 1967 let blob_parent = blob_path.rsplit_once('.').map(|(parent, _)| parent); 1968 matches.iter().find_map(|query_match| { 1969 + let aspect_parent = query_match.path.rsplit_once('.').map(|(parent, _)| parent); 1970 1971 // Check if they share the same parent 1972 if blob_parent == aspect_parent { 1973 + Some(query_match.path.clone()) 1974 } else { 1975 None 1976 } ··· 1980 1981 // Update the aspectRatio if we found a matching field 1982 if let Some(aspect_path) = aspect_path_to_update { 1983 + let aspect_obj = atproto! {{ 1984 + "width": width, 1985 + "height": height 1986 + }}; 1987 1988 root.with_mut(|record_data| { 1989 + record_data.set_at_path(&aspect_path, aspect_obj); 1990 }); 1991 } 1992 } ··· 2020 }); 2021 2022 let path_for_mutation = path.clone(); 2023 + let handle_input = move |evt: Event<dioxus::prelude::FormData>| { 2024 let text = evt.value(); 2025 input_text.set(text.clone()); 2026 ··· 2171 }); 2172 2173 let path_for_mutation = path.clone(); 2174 + let handle_input = move |evt: Event<dioxus::prelude::FormData>| { 2175 let text = evt.value(); 2176 input_text.set(text.clone()); 2177 ··· 2690 match fetcher.send(request).await { 2691 Ok(output) => { 2692 if output.status() == StatusCode::OK { 2693 + tracing::info!("Record updated successfully"); 2694 edit_data.set(data.clone()); 2695 edit_mode.set(false); 2696 } else { 2697 + tracing::error!("Unexpected status code: {:?}", output.status()); 2698 } 2699 } 2700 Err(e) => { 2701 + tracing::error!("Failed to update record: {:?}", e); 2702 } 2703 } 2704 } ··· 2724 match fetcher.send(request).await { 2725 Ok(response) => { 2726 if let Ok(output) = response.into_output() { 2727 + tracing::info!("Record created: {}", output.uri); 2728 let link = format!("{}/record/{}", crate::env::WEAVER_APP_DOMAIN, output.uri); 2729 nav.push(link); 2730 } 2731 } 2732 Err(e) => { 2733 + tracing::error!("Failed to create record: {:?}", e); 2734 } 2735 } 2736 } ··· 2769 .build(); 2770 2771 if let Err(e) = fetcher.send(delete_req).await { 2772 + tracing::warn!("Created new record but failed to delete old: {:?}", e); 2773 } 2774 } 2775 } 2776 2777 + tracing::info!("Record replaced: {}", create_output.uri); 2778 let link = format!("{}/record/{}", crate::env::WEAVER_APP_DOMAIN, create_output.uri); 2779 nav.push(link); 2780 } 2781 } 2782 Err(e) => { 2783 + tracing::error!("Failed to replace record: {:?}", e); 2784 } 2785 } 2786 } ··· 2805 2806 match fetcher.send(request).await { 2807 Ok(_) => { 2808 + tracing::info!("Record deleted"); 2809 nav.push(Route::Home {}); 2810 } 2811 Err(e) => { 2812 + tracing::error!("Failed to delete record: {:?}", e); 2813 } 2814 } 2815 }
+126 -103
crates/weaver-common/src/agent.rs
··· 614 AgentError::from(ClientError::invalid_request("Invalid weaver profile URI")) 615 }, 616 )?; 617 - if let Ok(weaver_record) = self.fetch_record(&weaver_uri).await { 618 - // Convert blobs to CDN URLs 619 - let avatar = weaver_record 620 .value 621 .avatar 622 .as_ref() ··· 631 .map_err(|_| { 632 AgentError::from(ClientError::invalid_request("Invalid avatar URI")) 633 })?; 634 - let banner = weaver_record 635 .value 636 .banner 637 .as_ref() 638 .map(|blob| { 639 let cid = blob.blob().cid(); 640 jacquard::types::string::Uri::new_owned(format!( 641 - "https://cdn.bsky.app/img/banner/plain/{}/{}", 642 did, cid 643 )) 644 }) ··· 647 AgentError::from(ClientError::invalid_request("Invalid banner URI")) 648 })?; 649 650 - let profile_view = ProfileView::new() 651 .did(did.clone()) 652 .handle(handle.clone()) 653 - .maybe_display_name(weaver_record.value.display_name.clone()) 654 - .maybe_description(weaver_record.value.description.clone()) 655 .maybe_avatar(avatar) 656 .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 .indexed_at(jacquard::types::string::Datetime::now()) 665 - .maybe_created_at(weaver_record.value.created_at) 666 .build(); 667 668 - return Ok(( 669 - Some(weaver_uri.as_uri().clone().into_static()), 670 ProfileDataView::new() 671 - .inner(ProfileDataViewInner::ProfileView(Box::new(profile_view))) 672 .build() 673 .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(); 749 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 - )) 759 } 760 } 761
··· 614 AgentError::from(ClientError::invalid_request("Invalid weaver profile URI")) 615 }, 616 )?; 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 725 .value 726 .avatar 727 .as_ref() ··· 736 .map_err(|_| { 737 AgentError::from(ClientError::invalid_request("Invalid avatar URI")) 738 })?; 739 + let banner = bsky_record 740 .value 741 .banner 742 .as_ref() 743 .map(|blob| { 744 let cid = blob.blob().cid(); 745 jacquard::types::string::Uri::new_owned(format!( 746 + "https://cdn.bsky.app/img/feed_fullsize/plain/{}/{}", 747 did, cid 748 )) 749 }) ··· 752 AgentError::from(ClientError::invalid_request("Invalid banner URI")) 753 })?; 754 755 + let profile_detailed = ProfileViewDetailed::new() 756 .did(did.clone()) 757 .handle(handle.clone()) 758 + .maybe_display_name(bsky_record.value.display_name.clone()) 759 + .maybe_description(bsky_record.value.description.clone()) 760 .maybe_avatar(avatar) 761 .maybe_banner(banner) 762 .indexed_at(jacquard::types::string::Datetime::now()) 763 + .maybe_created_at(bsky_record.value.created_at) 764 .build(); 765 766 + Ok(( 767 + Some(bsky_uri.as_uri().clone().into_static()), 768 ProfileDataView::new() 769 + .inner(ProfileDataViewInner::ProfileViewDetailed(Box::new( 770 + profile_detailed, 771 + ))) 772 .build() 773 .into_static(), 774 + )) 775 + }; 776 777 + n0_future::future::or( 778 + weaver_future, 779 + n0_future::future::or(bsky_appview_future, bsky_future), 780 + ) 781 + .await 782 } 783 } 784
+40
crates/weaver-renderer/src/css.rs
··· 90 max-width: 90ch; 91 margin: 0 auto; 92 padding: 2rem 1rem; 93 }} 94 95 /* Typography */ ··· 124 125 p {{ 126 margin-bottom: 1rem; 127 }} 128 129 a {{ ··· 209 border-collapse: collapse; 210 width: 100%; 211 margin-bottom: 1rem; 212 }} 213 214 th, td {{ ··· 259 border: none; 260 border-top: 2px solid var(--color-border); 261 margin: 2rem 0; 262 }} 263 "#, 264 // Light mode colours
··· 90 max-width: 90ch; 91 margin: 0 auto; 92 padding: 2rem 1rem; 93 + word-wrap: break-word; 94 + overflow-wrap: break-word; 95 }} 96 97 /* Typography */ ··· 126 127 p {{ 128 margin-bottom: 1rem; 129 + word-wrap: break-word; 130 + overflow-wrap: break-word; 131 }} 132 133 a {{ ··· 213 border-collapse: collapse; 214 width: 100%; 215 margin-bottom: 1rem; 216 + display: block; 217 + overflow-x: auto; 218 + max-width: 100%; 219 }} 220 221 th, td {{ ··· 266 border: none; 267 border-top: 2px solid var(--color-border); 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 + }} 302 }} 303 "#, 304 // Light mode colours
+15 -15
flake.lock
··· 3 "advisory-db": { 4 "flake": false, 5 "locked": { 6 - "lastModified": 1761631338, 7 - "narHash": "sha256-F6dlUrDiShwhMfPR+WoVmaQguGdEwjW9SI4nKlkay7c=", 8 "owner": "rustsec", 9 "repo": "advisory-db", 10 - "rev": "2e45336771e36acf5bcefe7c99280ab214719707", 11 "type": "github" 12 }, 13 "original": { ··· 18 }, 19 "crane": { 20 "locked": { 21 - "lastModified": 1760924934, 22 - "narHash": "sha256-tuuqY5aU7cUkR71sO2TraVKK2boYrdW3gCSXUkF4i44=", 23 "owner": "ipetkov", 24 "repo": "crane", 25 - "rev": "c6b4d5308293d0d04fcfeee92705017537cad02f", 26 "type": "github" 27 }, 28 "original": { ··· 39 "systems": "systems" 40 }, 41 "locked": { 42 - "lastModified": 1761886294, 43 - "narHash": "sha256-GEyOUuFbqWzpQIKO6mT2oCEp2JnUvNkOW/WsrHOFK6I=", 44 "owner": "DioxusLabs", 45 "repo": "dioxus", 46 - "rev": "2ab9a5586dc6b4d95e210eded692f50e7ab7575e", 47 "type": "github" 48 }, 49 "original": { ··· 137 }, 138 "nixpkgs_3": { 139 "locked": { 140 - "lastModified": 1761880412, 141 - "narHash": "sha256-QoJjGd4NstnyOG4mm4KXF+weBzA2AH/7gn1Pmpfcb0A=", 142 "owner": "NixOS", 143 "repo": "nixpkgs", 144 - "rev": "a7fc11be66bdfb5cdde611ee5ce381c183da8386", 145 "type": "github" 146 }, 147 "original": { ··· 200 "nixpkgs": "nixpkgs_4" 201 }, 202 "locked": { 203 - "lastModified": 1761878277, 204 - "narHash": "sha256-6fCtyVdTzoQejwoextAu7dCLoy5fyD3WVh+Qm7t2Nhg=", 205 "owner": "oxalica", 206 "repo": "rust-overlay", 207 - "rev": "6604534e44090c917db714faa58d47861657690c", 208 "type": "github" 209 }, 210 "original": {
··· 3 "advisory-db": { 4 "flake": false, 5 "locked": { 6 + "lastModified": 1763052940, 7 + "narHash": "sha256-dt2Eze8JMyeCqpUPT1TF6XrGJR8QeD7UvUDaIrKngrE=", 8 "owner": "rustsec", 9 "repo": "advisory-db", 10 + "rev": "4b6acc7020a23b61ae4963a075f0a4058cef79f2", 11 "type": "github" 12 }, 13 "original": { ··· 18 }, 19 "crane": { 20 "locked": { 21 + "lastModified": 1762538466, 22 + "narHash": "sha256-8zrIPl6J+wLm9MH5ksHcW7BUHo7jSNOu0/hA0ohOOaM=", 23 "owner": "ipetkov", 24 "repo": "crane", 25 + "rev": "0cea393fffb39575c46b7a0318386467272182fe", 26 "type": "github" 27 }, 28 "original": { ··· 39 "systems": "systems" 40 }, 41 "locked": { 42 + "lastModified": 1763000036, 43 + "narHash": "sha256-yJiLpG1W2+yn3xvdeVTpt9CUYC2/VBLCCpKhEifdcsc=", 44 "owner": "DioxusLabs", 45 "repo": "dioxus", 46 + "rev": "bd2d1de873ae9c0e7c4656ec0c991ea9aafd3293", 47 "type": "github" 48 }, 49 "original": { ··· 137 }, 138 "nixpkgs_3": { 139 "locked": { 140 + "lastModified": 1762943920, 141 + "narHash": "sha256-ITeH8GBpQTw9457ICZBddQEBjlXMmilML067q0e6vqY=", 142 "owner": "NixOS", 143 "repo": "nixpkgs", 144 + "rev": "91c9a64ce2a84e648d0cf9671274bb9c2fb9ba60", 145 "type": "github" 146 }, 147 "original": { ··· 200 "nixpkgs": "nixpkgs_4" 201 }, 202 "locked": { 203 + "lastModified": 1763087910, 204 + "narHash": "sha256-eB9Z1mWd1U6N61+F8qwDggX0ihM55s4E0CluwNukJRU=", 205 "owner": "oxalica", 206 "repo": "rust-overlay", 207 + "rev": "cf4a68749733d45c0420726596367acd708eb2e8", 208 "type": "github" 209 }, 210 "original": {
+1
flake.nix
··· 262 diesel-cli 263 postgresql 264 cargo-insta 265 jq 266 dioxus-cli 267 wasm-bindgen-cli
··· 262 diesel-cli 263 postgresql 264 cargo-insta 265 + cargo-bloat 266 jq 267 dioxus-cli 268 wasm-bindgen-cli
+53 -14
weaver_notes/.obsidian/workspace.json
··· 8 "type": "tabs", 9 "children": [ 10 { 11 - "id": "f41bdf1f327c8668", 12 "type": "leaf", 13 "state": { 14 "type": "markdown", 15 "state": { 16 - "file": "Arch.md", 17 "mode": "source", 18 "source": false 19 }, 20 "icon": "lucide-file", 21 - "title": "Arch" 22 } 23 } 24 - ] 25 } 26 ], 27 "direction": "vertical" ··· 94 "state": { 95 "type": "backlink", 96 "state": { 97 - "file": "Arch.md", 98 "collapseAll": false, 99 "extraContext": false, 100 "sortOrder": "alphabetical", ··· 104 "unlinkedCollapsed": true 105 }, 106 "icon": "links-coming-in", 107 - "title": "Backlinks for Arch" 108 } 109 }, 110 { ··· 142 "state": { 143 "type": "outline", 144 "state": { 145 - "file": "Weaver - Long-form writing.md", 146 "followCursor": false, 147 "showSearch": false, 148 "searchQuery": "" 149 }, 150 "icon": "lucide-list", 151 - "title": "Outline of Weaver - Long-form writing" 152 } 153 } 154 - ] 155 } 156 ], 157 "direction": "horizontal", 158 - "width": 300 159 }, 160 "left-ribbon": { 161 "hiddenItems": { ··· 168 "bases:Create new base": false 169 } 170 }, 171 - "active": "f41bdf1f327c8668", 172 "lastOpenFiles": [ 173 - "Weaver - Long-form writing.md", 174 "Arch.md", 175 "weaver_photo_med.jpg", 176 "xkcd_345_excerpt.png", 177 - "light_mode_excerpt.png", 178 - "notebook_entry_preview.png" 179 ] 180 }
··· 8 "type": "tabs", 9 "children": [ 10 { 11 + "id": "70e4a46d11e8ec6f", 12 "type": "leaf", 13 "state": { 14 "type": "markdown", 15 "state": { 16 + "file": "Why I rewrote pdsls in Rust (tm).md", 17 "mode": "source", 18 "source": false 19 }, 20 "icon": "lucide-file", 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" 50 } 51 } 52 + ], 53 + "currentTab": 2 54 } 55 ], 56 "direction": "vertical" ··· 123 "state": { 124 "type": "backlink", 125 "state": { 126 + "file": "Why I rewrote pdsls in Rust (tm).md", 127 "collapseAll": false, 128 "extraContext": false, 129 "sortOrder": "alphabetical", ··· 133 "unlinkedCollapsed": true 134 }, 135 "icon": "links-coming-in", 136 + "title": "Backlinks for Why I rewrote pdsls in Rust (tm)" 137 } 138 }, 139 { ··· 171 "state": { 172 "type": "outline", 173 "state": { 174 + "file": "Why I rewrote pdsls in Rust (tm).md", 175 "followCursor": false, 176 "showSearch": false, 177 "searchQuery": "" 178 }, 179 "icon": "lucide-list", 180 + "title": "Outline of Why I rewrote pdsls in Rust (tm)" 181 } 182 } 183 + ], 184 + "currentTab": 3 185 } 186 ], 187 "direction": "horizontal", 188 + "width": 300, 189 + "collapsed": true 190 }, 191 "left-ribbon": { 192 "hiddenItems": { ··· 199 "bases:Create new base": false 200 } 201 }, 202 + "active": "6029beecc3d03bce", 203 "lastOpenFiles": [ 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", 213 "Arch.md", 214 + "Weaver - Long-form writing.md", 215 "weaver_photo_med.jpg", 216 "xkcd_345_excerpt.png", 217 + "light_mode_excerpt.png" 218 ] 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.