diff --git a/Cargo.lock b/Cargo.lock index 5d5a441..5e11320 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -131,22 +131,22 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -357,9 +357,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "axum" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" +checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425" dependencies = [ "axum-core", "axum-macros", @@ -710,9 +710,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" dependencies = [ "serde", ] @@ -763,9 +763,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.45" +version = "1.2.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe" +checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" dependencies = [ "find-msvc-tools", "jobserver", @@ -894,9 +894,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.51" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", "clap_derive", @@ -904,9 +904,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.51" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ "anstream", "anstyle", @@ -988,16 +988,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "compact_string" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5255b88d8ea09573f588088dea17fbea682b4442abea6761a15d1da2c3a76c5c" -dependencies = [ - "serde", - "thiserror 1.0.69", -] - [[package]] name = "compression-codecs" version = "0.4.32" @@ -1864,15 +1854,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "dioxus-free-icons" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d356e0f9edad0930bc1cc76744360c0ecca020cb943acaadf42cb774f28284" -dependencies = [ - "dioxus", -] - [[package]] name = "dioxus-fullstack" version = "0.7.1" @@ -2109,7 +2090,7 @@ dependencies = [ [[package]] name = "dioxus-primitives" version = "0.0.1" -source = "git+https://github.com/DioxusLabs/components#98067ce2da493651b0c089db91e9903714c211f7" +source = "git+https://github.com/DioxusLabs/components#a15f329d1dc2bd76cd4030b27ecd70edb9fd8c6b" dependencies = [ "dioxus", "dioxus-time", @@ -2490,27 +2471,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" -[[package]] -name = "dynosaur" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "277b2cb52d2df4acece06bb16bc0bb0a006970c7bf504eac2d310927a6f65890" -dependencies = [ - "dynosaur_derive", - "trait-variant", -] - -[[package]] -name = "dynosaur_derive" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a4102713839a8c01c77c165bc38ef2e83948f6397fa1e1dcfacec0f07b149d3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.110", -] - [[package]] name = "ecdsa" version = "0.16.9" @@ -2777,9 +2737,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" [[package]] name = "flate2" @@ -3463,7 +3423,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.12.0", + "indexmap 2.12.1", "slab", "tokio", "tokio-util", @@ -3520,9 +3480,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", @@ -3762,9 +3722,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1744436df46f0bde35af3eda22aeaba453aada65d8f1c171cd8a5f59030bd69f" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ "atomic-waker", "bytes", @@ -3800,27 +3760,11 @@ dependencies = [ "webpki-roots", ] -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - [[package]] name = "hyper-util" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" dependencies = [ "base64 0.22.1", "bytes", @@ -4003,12 +3947,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "serde", "serde_core", ] @@ -4033,9 +3977,9 @@ dependencies = [ [[package]] name = "insta" -version = "1.43.2" +version = "1.44.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46fdb647ebde000f43b5b53f773c30cf9b0cb4300453208713fa38b2c70935a0" +checksum = "e8732d3774162a0851e3f2b150eb98f31a9885dd75985099421d393385a01dfd" dependencies = [ "console", "once_cell", @@ -4123,7 +4067,7 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jacquard" -version = "0.9.0" +version = "0.9.4" dependencies = [ "bytes", "getrandom 0.2.16", @@ -4137,6 +4081,7 @@ dependencies = [ "jose-jwk", "miette 7.6.0", "regex", + "regex-lite", "reqwest", "serde", "serde_html_form", @@ -4152,7 +4097,7 @@ dependencies = [ [[package]] name = "jacquard-api" -version = "0.9.0" +version = "0.9.2" dependencies = [ "bon", "bytes", @@ -4169,7 +4114,7 @@ dependencies = [ [[package]] name = "jacquard-axum" -version = "0.9.0" +version = "0.9.2" dependencies = [ "axum", "bytes", @@ -4190,7 +4135,7 @@ dependencies = [ [[package]] name = "jacquard-common" -version = "0.9.0" +version = "0.9.2" dependencies = [ "base64 0.22.1", "bon", @@ -4213,6 +4158,7 @@ dependencies = [ "p256", "rand 0.9.2", "regex", + "regex-lite", "reqwest", "serde", "serde_html_form", @@ -4232,7 +4178,7 @@ dependencies = [ [[package]] name = "jacquard-derive" -version = "0.9.0" +version = "0.9.4" dependencies = [ "heck 0.5.0", "jacquard-lexicon", @@ -4243,7 +4189,7 @@ dependencies = [ [[package]] name = "jacquard-identity" -version = "0.9.1" +version = "0.9.2" dependencies = [ "bon", "bytes", @@ -4253,7 +4199,7 @@ dependencies = [ "jacquard-common", "jacquard-lexicon", "miette 7.6.0", - "mini-moka", + "mini-moka 0.10.99", "percent-encoding", "reqwest", "serde", @@ -4269,7 +4215,7 @@ dependencies = [ [[package]] name = "jacquard-lexicon" -version = "0.9.1" +version = "0.9.2" dependencies = [ "cid", "dashmap", @@ -4294,7 +4240,7 @@ dependencies = [ [[package]] name = "jacquard-oauth" -version = "0.9.0" +version = "0.9.2" dependencies = [ "base64 0.22.1", "bytes", @@ -4499,7 +4445,7 @@ checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" dependencies = [ "cssparser", "html5ever 0.29.1", - "indexmap 2.12.0", + "indexmap 2.12.1", "selectors", ] @@ -4705,7 +4651,7 @@ version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96051b46fc183dc9cd4a223960ef37b9af631b55191852a8274bfef064cda20f" dependencies = [ - "hashbrown 0.16.0", + "hashbrown 0.16.1", ] [[package]] @@ -5030,6 +4976,19 @@ dependencies = [ "unicase", ] +[[package]] +name = "mini-moka" +version = "0.10.99" +dependencies = [ + "crossbeam-channel", + "crossbeam-utils", + "dashmap", + "smallvec", + "tagptr", + "triomphe", + "web-time", +] + [[package]] name = "mini-moka" version = "0.11.0" @@ -5944,7 +5903,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64 0.22.1", - "indexmap 2.12.0", + "indexmap 2.12.1", "quick-xml 0.38.4", "serde", "time", @@ -6422,6 +6381,12 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-lite" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" + [[package]] name = "regex-syntax" version = "0.8.8" @@ -6448,13 +6413,11 @@ dependencies = [ "http-body-util", "hyper", "hyper-rustls", - "hyper-tls", "hyper-util", "js-sys", "log", "mime", "mime_guess", - "native-tls", "percent-encoding", "pin-project-lite", "quinn", @@ -6465,7 +6428,6 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-native-tls", "tokio-rustls", "tokio-util", "tower", @@ -6481,9 +6443,9 @@ dependencies = [ [[package]] name = "resolv-conf" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b3789b30bd25ba102de4beabd95d21ac45b69b1be7d14522bab988c526d6799" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" [[package]] name = "rfc6979" @@ -6559,9 +6521,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" dependencies = [ "const-oid", "digest", @@ -6924,7 +6886,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" dependencies = [ "form_urlencoded", - "indexmap 2.12.0", + "indexmap 2.12.1", "itoa", "ryu", "serde_core", @@ -6948,7 +6910,7 @@ version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.12.1", "itoa", "memchr", "ryu", @@ -7021,15 +6983,15 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.15.1" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa66c845eee442168b2c8134fec70ac50dc20e760769c8ba0ad1319ca1959b04" +checksum = "10574371d41b0d9b2cff89418eda27da52bcaff2cc8741db26382a77c29131f1" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.12.0", + "indexmap 2.12.1", "schemars 0.9.0", "schemars 1.1.0", "serde_core", @@ -7040,9 +7002,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.15.1" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91a903660542fced4e99881aa481bdbaec1634568ee02e0b8bd57c64cb38955" +checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b" dependencies = [ "darling", "proc-macro2", @@ -8000,16 +7962,6 @@ dependencies = [ "syn 2.0.110", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.4" @@ -8112,7 +8064,7 @@ version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.12.1", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", @@ -8156,7 +8108,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.12.1", "toml_datetime 0.6.11", "winnow 0.5.40", ] @@ -8167,7 +8119,7 @@ version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.12.1", "toml_datetime 0.6.11", "winnow 0.5.40", ] @@ -8178,7 +8130,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.12.1", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", @@ -8192,7 +8144,7 @@ version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.12.1", "toml_datetime 0.7.3", "toml_parser", "winnow 0.7.13", @@ -8945,7 +8897,6 @@ dependencies = [ "console_error_panic_hook", "dashmap", "dioxus", - "dioxus-free-icons", "dioxus-primitives", "dotenvy", "gloo-storage", @@ -8953,16 +8904,18 @@ dependencies = [ "humansize", "jacquard", "jacquard-axum", + "jacquard-identity", "jacquard-lexicon", "js-sys", "lol_alloc", "markdown-weaver", "mime-sniffer", - "mini-moka", + "mini-moka 0.11.0", "n0-future", "reqwest", "serde", "serde_html_form", + "serde_ipld_dagcbor", "serde_json", "time", "tokio", @@ -8983,7 +8936,6 @@ dependencies = [ "clap", "dirs", "jacquard", - "jacquard-api", "kdl", "markdown-weaver", "markdown-weaver-escape", @@ -9010,17 +8962,13 @@ dependencies = [ "markdown-weaver-escape", "miette 7.6.0", "mime-sniffer", - "minijinja", "n0-future", - "owo-colors", "pin-project", "regex", + "regex-lite", "reqwest", "send_wrapper", "serde", - "serde_bytes", - "serde_html_form", - "serde_ipld_dagcbor", "serde_json", "thiserror 2.0.17", "tokio", @@ -9049,7 +8997,6 @@ dependencies = [ "futures-util", "hyper", "jacquard", - "jacquard-api", "jacquard-axum", "jose", "jose-jwk", @@ -9087,9 +9034,7 @@ name = "weaver-renderer" version = "0.1.0" dependencies = [ "bitflags 2.10.0", - "compact_string", "dashmap", - "dynosaur", "http", "ignore", "insta", @@ -9102,6 +9047,7 @@ dependencies = [ "pin-project", "pin-utils", "regex", + "regex-lite", "reqwest", "smol_str", "syntect", @@ -9395,13 +9341,13 @@ dependencies = [ [[package]] name = "windows-registry" -version = "0.5.3" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] @@ -10006,18 +9952,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "43fa6694ed34d6e57407afbccdeecfa268c470a7d2a5b0cf49ce9fcc345afb90" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 76949bc..dfc6cf3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,39 +21,39 @@ name = "weaver-workspace" [workspace.dependencies] - serde = { version = "1.0", features = ["derive"] } bytes = "1.10" -minijinja = { version = "2.9.0", default-features = false } -minijinja-contrib = { version = "2.9.0", default-features = false } miette = { version = "7.6" } -owo-colors = { version = "4.2.0" } thiserror = "2.0" syntect = { version = "5.2.0", default-features = false } -jane-eyre = "0.6.12" n0-future = "=0.1.3" tracing = { version = "0.1.41", default-features = false, features = ["std"] } -lexicon_cid = { package = "cid", version = "0.10.1", features = [ - "serde-codec", -] } - markdown-weaver = { git = "https://github.com/rsform/markdown-weaver" } markdown-weaver-escape = { git = "https://github.com/rsform/markdown-weaver" } # jacquard = { git = "https://tangled.org/@nonbinary.computer/jacquard", default-features = false, features = ["derive", "api_bluesky", "tracing"] } -# jacquard-api = { git = "https://tangled.org/@nonbinary.computer/jacquard" } +# jacquard-identity = { git = "https://tangled.org/@nonbinary.computer/jacquard", features = ["cache"] } # jacquard-common = { git = "https://tangled.org/@nonbinary.computer/jacquard" } # jacquard-axum = { git = "https://tangled.org/@nonbinary.computer/jacquard" } # jacquard-derive = { git = "https://tangled.org/@nonbinary.computer/jacquard" } # jacquard-lexicon = { git = "https://tangled.org/@nonbinary.computer/jacquard", default-features = false } -jacquard = { path = "../jacquard/crates/jacquard", default-features = false, features = ["derive", "api_bluesky", "tracing"] } +jacquard = { path = "../jacquard/crates/jacquard", default-features = false, features = ["derive", "api_bluesky", "tracing", "cache"] } +jacquard-identity = { path = "../jacquard/crates/jacquard-identity", features = ["cache"] } jacquard-api = { path = "../jacquard/crates/jacquard-api" } jacquard-common = { path = "../jacquard/crates/jacquard-common" } jacquard-axum = {path = "../jacquard/crates/jacquard-axum" } jacquard-derive = { path = "../jacquard/crates/jacquard-derive" } jacquard-lexicon = { path = "../jacquard/crates/jacquard-lexicon", default-features = false } +# jacquard = { path = "../jacquard-facet/crates/jacquard", default-features = false, features = ["derive", "api_bluesky", "tracing", "serde"] } +# jacquard-identity = { path = "../jacquard-facet/crates/jacquard-identity", features = ["cache"] } +# jacquard-api = { path = "../jacquard-facet/crates/jacquard-api" } +# jacquard-common = { path = "../jacquard-facet/crates/jacquard-common" } +# jacquard-axum = {path = "../jacquard-facet/crates/jacquard-axum" } +# jacquard-derive = { path = "../jacquard-facet/crates/jacquard-derive" } +# jacquard-lexicon = { path = "../jacquard-facet/crates/jacquard-lexicon", default-features = false } + [profile] [profile.wasm-dev] diff --git a/crates/weaver-api/Cargo.toml b/crates/weaver-api/Cargo.toml index 460a147..e7fb000 100644 --- a/crates/weaver-api/Cargo.toml +++ b/crates/weaver-api/Cargo.toml @@ -6,14 +6,14 @@ version = "0.1.0" authors.workspace = true [dependencies] -bytes = { workspace = true, features = ["serde"] } +bytes = { workspace = true } jacquard-common = { workspace = true } jacquard-derive = { workspace = true } jacquard-lexicon = { workspace = true } miette.workspace = true rustversion = "1.0" serde.workspace = true -serde_ipld_dagcbor = "0.6" +serde_ipld_dagcbor = { version = "0.6", optional = true } thiserror.workspace = true unicode-segmentation = "1.12" @@ -23,8 +23,10 @@ unicode-segmentation = "1.12" non_snake_case = "allow" [features] -default = ["sh_weaver", "com_atproto"] +default = ["sh_weaver", "com_atproto", "serde"] streaming = ["jacquard-common/websocket"] +serde = ["bytes/serde", "dep:serde_ipld_dagcbor"] + # --- generated --- # Generated namespace features diff --git a/crates/weaver-api/lexicons/tools_ozone_moderation_defs.json b/crates/weaver-api/lexicons/tools_ozone_moderation_defs.json index 26749a2..a414b7d 100644 --- a/crates/weaver-api/lexicons/tools_ozone_moderation_defs.json +++ b/crates/weaver-api/lexicons/tools_ozone_moderation_defs.json @@ -326,6 +326,10 @@ "type": "string", "description": "The content of the email sent to the user." }, + "isDelivered": { + "type": "boolean", + "description": "Indicates whether the email was successfully delivered to the user's inbox." + }, "policies": { "type": "array", "description": "Names/Keywords of the policies that necessitated the email.", @@ -555,6 +559,17 @@ "type": "string", "description": "When the strike should expire. If not provided, the strike never expires.", "format": "datetime" + }, + "targetServices": { + "type": "array", + "description": "List of services where the takedown should be applied. If empty or not provided, takedown is applied on all configured services.", + "items": { + "type": "string", + "knownValues": [ + "appview", + "pds" + ] + } } } }, diff --git a/crates/weaver-api/lexicons/tools_ozone_moderation_queryStatuses.json b/crates/weaver-api/lexicons/tools_ozone_moderation_queryStatuses.json index 203868d..b311536 100644 --- a/crates/weaver-api/lexicons/tools_ozone_moderation_queryStatuses.json +++ b/crates/weaver-api/lexicons/tools_ozone_moderation_queryStatuses.json @@ -149,7 +149,13 @@ }, "reviewState": { "type": "string", - "description": "Specify when fetching subjects in a certain state" + "description": "Specify when fetching subjects in a certain state", + "knownValues": [ + "tools.ozone.moderation.defs#reviewOpen", + "tools.ozone.moderation.defs#reviewClosed", + "tools.ozone.moderation.defs#reviewEscalated", + "tools.ozone.moderation.defs#reviewNone" + ] }, "reviewedAfter": { "type": "string", diff --git a/crates/weaver-api/lexicons/tools_ozone_moderation_scheduleAction.json b/crates/weaver-api/lexicons/tools_ozone_moderation_scheduleAction.json index 168d927..10cb145 100644 --- a/crates/weaver-api/lexicons/tools_ozone_moderation_scheduleAction.json +++ b/crates/weaver-api/lexicons/tools_ozone_moderation_scheduleAction.json @@ -133,6 +133,14 @@ "type": "integer", "description": "Indicates how long the takedown should be in effect before automatically expiring." }, + "emailContent": { + "type": "string", + "description": "Email content to be sent to the user upon takedown." + }, + "emailSubject": { + "type": "string", + "description": "Subject of the email to be sent to the user upon takedown." + }, "policies": { "type": "array", "description": "Names/Keywords of the policies that drove the decision.", @@ -140,6 +148,19 @@ "type": "string" }, "maxLength": 5 + }, + "severityLevel": { + "type": "string", + "description": "Severity level of the violation (e.g., 'sev-0', 'sev-1', 'sev-2', etc.)." + }, + "strikeCount": { + "type": "integer", + "description": "Number of strikes to assign to the user when takedown is applied." + }, + "strikeExpiresAt": { + "type": "string", + "description": "When the strike should expire. If not provided, the strike never expires.", + "format": "datetime" } } } diff --git a/crates/weaver-api/src/app_bsky/actor.rs b/crates/weaver-api/src/app_bsky/actor.rs index 2e09d9b..8abc794 100644 --- a/crates/weaver-api/src/app_bsky/actor.rs +++ b/crates/weaver-api/src/app_bsky/actor.rs @@ -3502,13 +3502,13 @@ pub struct MutedWord<'a> { /// Groups of users to apply the muted word to. If undefined, applies to all users. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub actor_target: Option>, + pub actor_target: std::option::Option>, /// The date and time at which the muted word will expire and no longer be applied. #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub expires_at: Option, + pub expires_at: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub id: Option>, + pub id: std::option::Option>, /// The intended targets of the muted word. #[serde(borrow)] pub targets: Vec>, @@ -3998,10 +3998,10 @@ pub struct Nux<'a> { /// Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub data: Option>, + pub data: std::option::Option>, /// The date and time at which the NUX will expire and should be considered completed. #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub expires_at: Option, + pub expires_at: std::option::Option, #[serde(borrow)] pub id: jacquard_common::CowStr<'a>, } @@ -4551,43 +4551,43 @@ impl<'a> ::jacquard_lexicon::schema::LexiconSchema for ProfileAssociatedChat<'a> pub struct ProfileView<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub associated: Option>, + pub associated: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub avatar: Option>, + pub avatar: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub created_at: Option, + pub created_at: std::option::Option, /// Debug information for internal development #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub debug: Option>, + pub debug: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub description: Option>, + pub description: std::option::Option>, #[serde(borrow)] pub did: jacquard_common::types::string::Did<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub display_name: Option>, + pub display_name: std::option::Option>, #[serde(borrow)] pub handle: jacquard_common::types::string::Handle<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub indexed_at: Option, + pub indexed_at: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub labels: Option>>, + pub labels: std::option::Option>>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub pronouns: Option>, + pub pronouns: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub status: Option>, + pub status: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub verification: Option>, + pub verification: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub viewer: Option>, + pub viewer: std::option::Option>, } pub mod profile_view_state { @@ -5097,38 +5097,38 @@ impl<'a> ::jacquard_lexicon::schema::LexiconSchema for ProfileView<'a> { pub struct ProfileViewBasic<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub associated: Option>, + pub associated: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub avatar: Option>, + pub avatar: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub created_at: Option, + pub created_at: std::option::Option, /// Debug information for internal development #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub debug: Option>, + pub debug: std::option::Option>, #[serde(borrow)] pub did: jacquard_common::types::string::Did<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub display_name: Option>, + pub display_name: std::option::Option>, #[serde(borrow)] pub handle: jacquard_common::types::string::Handle<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub labels: Option>>, + pub labels: std::option::Option>>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub pronouns: Option>, + pub pronouns: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub status: Option>, + pub status: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub verification: Option>, + pub verification: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub viewer: Option>, + pub viewer: std::option::Option>, } pub mod profile_view_basic_state { @@ -5562,63 +5562,65 @@ impl<'a> ::jacquard_lexicon::schema::LexiconSchema for ProfileViewBasic<'a> { pub struct ProfileViewDetailed<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub associated: Option>, + pub associated: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub avatar: Option>, + pub avatar: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub banner: Option>, + pub banner: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub created_at: Option, + pub created_at: std::option::Option, /// Debug information for internal development #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub debug: Option>, + pub debug: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub description: Option>, + pub description: std::option::Option>, #[serde(borrow)] pub did: jacquard_common::types::string::Did<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub display_name: Option>, + pub display_name: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub followers_count: Option, + pub followers_count: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub follows_count: Option, + pub follows_count: std::option::Option, #[serde(borrow)] pub handle: jacquard_common::types::string::Handle<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub indexed_at: Option, + pub indexed_at: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub joined_via_starter_pack: Option< + pub joined_via_starter_pack: std::option::Option< crate::app_bsky::graph::StarterPackViewBasic<'a>, >, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub labels: Option>>, + pub labels: std::option::Option>>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub pinned_post: Option>, + pub pinned_post: std::option::Option< + crate::com_atproto::repo::strong_ref::StrongRef<'a>, + >, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub posts_count: Option, + pub posts_count: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub pronouns: Option>, + pub pronouns: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub status: Option>, + pub status: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub verification: Option>, + pub verification: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub viewer: Option>, + pub viewer: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub website: Option>, + pub website: std::option::Option>, } pub mod profile_view_detailed_state { @@ -6528,7 +6530,7 @@ pub struct SavedFeedsPref<'a> { #[serde(borrow)] pub saved: Vec>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub timeline_index: Option, + pub timeline_index: std::option::Option, } pub mod saved_feeds_pref_state { @@ -6857,13 +6859,13 @@ pub struct StatusView<'a> { /// An optional embed associated with the status. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub embed: Option>, + pub embed: std::option::Option>, /// The date when this status will expire. The application might choose to no longer return the status after expiration. #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub expires_at: Option, + pub expires_at: std::option::Option, /// True if the status is not expired, false if it is expired. Only present if expiration was set. #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub is_active: Option, + pub is_active: std::option::Option, #[serde(borrow)] pub record: jacquard_common::types::value::Data<'a>, /// The status for the account. diff --git a/crates/weaver-api/src/app_bsky/actor/profile.rs b/crates/weaver-api/src/app_bsky/actor/profile.rs index d73b207..0b2299c 100644 --- a/crates/weaver-api/src/app_bsky/actor/profile.rs +++ b/crates/weaver-api/src/app_bsky/actor/profile.rs @@ -21,39 +21,41 @@ pub struct Profile<'a> { /// Small image to be displayed next to posts from account. AKA, 'profile picture' #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub avatar: Option>, + pub avatar: std::option::Option>, /// Larger horizontal image to display behind profile view. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub banner: Option>, + pub banner: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub created_at: Option, + pub created_at: std::option::Option, /// Free-form profile description text. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub description: Option>, + pub description: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub display_name: Option>, + pub display_name: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub joined_via_starter_pack: Option< + pub joined_via_starter_pack: std::option::Option< crate::com_atproto::repo::strong_ref::StrongRef<'a>, >, /// Self-label values, specific to the Bluesky application, on the overall account. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub labels: Option>, + pub labels: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub pinned_post: Option>, + pub pinned_post: std::option::Option< + crate::com_atproto::repo::strong_ref::StrongRef<'a>, + >, /// Free-form pronouns text. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub pronouns: Option>, + pub pronouns: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub website: Option>, + pub website: std::option::Option>, } pub mod profile_state { diff --git a/crates/weaver-api/src/app_bsky/actor/status.rs b/crates/weaver-api/src/app_bsky/actor/status.rs index 4105506..1d27df6 100644 --- a/crates/weaver-api/src/app_bsky/actor/status.rs +++ b/crates/weaver-api/src/app_bsky/actor/status.rs @@ -39,11 +39,11 @@ pub struct Status<'a> { pub created_at: jacquard_common::types::string::Datetime, /// The duration of the status in minutes. Applications can choose to impose minimum and maximum limits. #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub duration_minutes: Option, + pub duration_minutes: std::option::Option, /// An optional embed associated with the status. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub embed: Option>, + pub embed: std::option::Option>, /// The status for the account. #[serde(borrow)] pub status: jacquard_common::CowStr<'a>, diff --git a/crates/weaver-api/src/app_bsky/bookmark.rs b/crates/weaver-api/src/app_bsky/bookmark.rs index 3b38380..626984f 100644 --- a/crates/weaver-api/src/app_bsky/bookmark.rs +++ b/crates/weaver-api/src/app_bsky/bookmark.rs @@ -263,7 +263,7 @@ impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Bookmark<'a> { #[serde(rename_all = "camelCase")] pub struct BookmarkView<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub created_at: Option, + pub created_at: std::option::Option, #[serde(borrow)] pub item: BookmarkViewItem<'a>, /// A strong ref to the bookmarked record. diff --git a/crates/weaver-api/src/app_bsky/embed/external.rs b/crates/weaver-api/src/app_bsky/embed/external.rs index 709ac8c..7b5f686 100644 --- a/crates/weaver-api/src/app_bsky/embed/external.rs +++ b/crates/weaver-api/src/app_bsky/embed/external.rs @@ -21,7 +21,7 @@ pub struct External<'a> { pub description: jacquard_common::CowStr<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub thumb: Option>, + pub thumb: std::option::Option>, #[serde(borrow)] pub title: jacquard_common::CowStr<'a>, #[serde(borrow)] @@ -767,7 +767,7 @@ pub struct ViewExternal<'a> { pub description: jacquard_common::CowStr<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub thumb: Option>, + pub thumb: std::option::Option>, #[serde(borrow)] pub title: jacquard_common::CowStr<'a>, #[serde(borrow)] diff --git a/crates/weaver-api/src/app_bsky/embed/images.rs b/crates/weaver-api/src/app_bsky/embed/images.rs index b855007..700418d 100644 --- a/crates/weaver-api/src/app_bsky/embed/images.rs +++ b/crates/weaver-api/src/app_bsky/embed/images.rs @@ -22,7 +22,7 @@ pub struct Image<'a> { pub alt: jacquard_common::CowStr<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub aspect_ratio: Option>, + pub aspect_ratio: std::option::Option>, #[serde(borrow)] pub image: jacquard_common::types::blob::BlobRef<'a>, } @@ -741,7 +741,7 @@ pub struct ViewImage<'a> { pub alt: jacquard_common::CowStr<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub aspect_ratio: Option>, + pub aspect_ratio: std::option::Option>, /// Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View. #[serde(borrow)] pub fullsize: jacquard_common::types::string::Uri<'a>, diff --git a/crates/weaver-api/src/app_bsky/embed/record.rs b/crates/weaver-api/src/app_bsky/embed/record.rs index 75d2c56..a936c52 100644 --- a/crates/weaver-api/src/app_bsky/embed/record.rs +++ b/crates/weaver-api/src/app_bsky/embed/record.rs @@ -1273,19 +1273,19 @@ pub struct ViewRecord<'a> { pub cid: jacquard_common::types::string::Cid<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub embeds: Option>>, + pub embeds: std::option::Option>>, pub indexed_at: jacquard_common::types::string::Datetime, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub labels: Option>>, + pub labels: std::option::Option>>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub like_count: Option, + pub like_count: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub quote_count: Option, + pub quote_count: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub reply_count: Option, + pub reply_count: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub repost_count: Option, + pub repost_count: std::option::Option, #[serde(borrow)] pub uri: jacquard_common::types::string::AtUri<'a>, /// The record data itself. diff --git a/crates/weaver-api/src/app_bsky/embed/video.rs b/crates/weaver-api/src/app_bsky/embed/video.rs index bbfd5c2..73ef658 100644 --- a/crates/weaver-api/src/app_bsky/embed/video.rs +++ b/crates/weaver-api/src/app_bsky/embed/video.rs @@ -414,13 +414,13 @@ pub struct Video<'a> { /// Alt text description of the video, for accessibility. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub alt: Option>, + pub alt: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub aspect_ratio: Option>, + pub aspect_ratio: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub captions: Option>>, + pub captions: std::option::Option>>, /// The mp4 video file. May be up to 100mb, formerly limited to 50mb. #[serde(borrow)] pub video: jacquard_common::types::blob::BlobRef<'a>, @@ -664,17 +664,17 @@ impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Video<'a> { pub struct View<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub alt: Option>, + pub alt: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub aspect_ratio: Option>, + pub aspect_ratio: std::option::Option>, #[serde(borrow)] pub cid: jacquard_common::types::string::Cid<'a>, #[serde(borrow)] pub playlist: jacquard_common::types::string::Uri<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub thumbnail: Option>, + pub thumbnail: std::option::Option>, } pub mod view_state { diff --git a/crates/weaver-api/src/app_bsky/feed.rs b/crates/weaver-api/src/app_bsky/feed.rs index 5cf8e7e..2b23ffd 100644 --- a/crates/weaver-api/src/app_bsky/feed.rs +++ b/crates/weaver-api/src/app_bsky/feed.rs @@ -46,7 +46,7 @@ pub struct BlockedAuthor<'a> { pub did: jacquard_common::types::string::Did<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub viewer: Option>, + pub viewer: std::option::Option>, } pub mod blocked_author_state { @@ -1916,19 +1916,19 @@ pub struct FeedViewPost<'a> { /// Context provided by feed generator that may be passed back alongside interactions. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub feed_context: Option>, + pub feed_context: std::option::Option>, #[serde(borrow)] pub post: crate::app_bsky::feed::PostView<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub reason: Option>, + pub reason: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub reply: Option>, + pub reply: std::option::Option>, /// Unique identifier per request that may be passed back alongside interactions. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub req_id: Option>, + pub req_id: std::option::Option>, } pub mod feed_view_post_state { @@ -2188,23 +2188,25 @@ impl<'a> ::jacquard_lexicon::schema::LexiconSchema for FeedViewPost<'a> { #[serde(rename_all = "camelCase")] pub struct GeneratorView<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub accepts_interactions: Option, + pub accepts_interactions: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub avatar: Option>, + pub avatar: std::option::Option>, #[serde(borrow)] pub cid: jacquard_common::types::string::Cid<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub content_mode: Option>, + pub content_mode: std::option::Option>, #[serde(borrow)] pub creator: crate::app_bsky::actor::ProfileView<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub description: Option>, + pub description: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub description_facets: Option>>, + pub description_facets: std::option::Option< + Vec>, + >, #[serde(borrow)] pub did: jacquard_common::types::string::Did<'a>, #[serde(borrow)] @@ -2212,14 +2214,14 @@ pub struct GeneratorView<'a> { pub indexed_at: jacquard_common::types::string::Datetime, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub labels: Option>>, + pub labels: std::option::Option>>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub like_count: Option, + pub like_count: std::option::Option, #[serde(borrow)] pub uri: jacquard_common::types::string::AtUri<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub viewer: Option>, + pub viewer: std::option::Option>, } pub mod generator_view_state { @@ -3161,38 +3163,38 @@ pub struct PostView<'a> { #[serde(borrow)] pub author: crate::app_bsky::actor::ProfileViewBasic<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub bookmark_count: Option, + pub bookmark_count: std::option::Option, #[serde(borrow)] pub cid: jacquard_common::types::string::Cid<'a>, /// Debug information for internal development #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub debug: Option>, + pub debug: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub embed: Option>, + pub embed: std::option::Option>, pub indexed_at: jacquard_common::types::string::Datetime, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub labels: Option>>, + pub labels: std::option::Option>>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub like_count: Option, + pub like_count: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub quote_count: Option, + pub quote_count: std::option::Option, #[serde(borrow)] pub record: jacquard_common::types::value::Data<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub reply_count: Option, + pub reply_count: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub repost_count: Option, + pub repost_count: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub threadgate: Option>, + pub threadgate: std::option::Option>, #[serde(borrow)] pub uri: jacquard_common::types::string::AtUri<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub viewer: Option>, + pub viewer: std::option::Option>, } pub mod post_view_state { @@ -3740,11 +3742,11 @@ pub struct ReasonRepost<'a> { pub by: crate::app_bsky::actor::ProfileViewBasic<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub cid: Option>, + pub cid: std::option::Option>, pub indexed_at: jacquard_common::types::string::Datetime, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub uri: Option>, + pub uri: std::option::Option>, } pub mod reason_repost_state { @@ -3963,7 +3965,9 @@ pub struct ReplyRef<'a> { /// When parent is a reply to another post, this is the author of that post. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub grandparent_author: Option>, + pub grandparent_author: std::option::Option< + crate::app_bsky::actor::ProfileViewBasic<'a>, + >, #[serde(borrow)] pub parent: ReplyRefParent<'a>, #[serde(borrow)] @@ -4242,12 +4246,12 @@ pub struct SkeletonFeedPost<'a> { /// Context that will be passed through to client and may be passed to feed generator back alongside interactions. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub feed_context: Option>, + pub feed_context: std::option::Option>, #[serde(borrow)] pub post: jacquard_common::types::string::AtUri<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub reason: Option>, + pub reason: std::option::Option>, } pub mod skeleton_feed_post_state { @@ -4665,15 +4669,15 @@ impl<'a> ::jacquard_lexicon::schema::LexiconSchema for ThreadContext<'a> { pub struct ThreadViewPost<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub parent: Option>, + pub parent: std::option::Option>, #[serde(borrow)] pub post: crate::app_bsky::feed::PostView<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub replies: Option>>, + pub replies: std::option::Option>>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub thread_context: Option>, + pub thread_context: std::option::Option>, } pub mod thread_view_post_state { diff --git a/crates/weaver-api/src/app_bsky/feed/generator.rs b/crates/weaver-api/src/app_bsky/feed/generator.rs index f1cf2c0..113990d 100644 --- a/crates/weaver-api/src/app_bsky/feed/generator.rs +++ b/crates/weaver-api/src/app_bsky/feed/generator.rs @@ -20,20 +20,22 @@ pub struct Generator<'a> { /// Declaration that a feed accepts feedback interactions from a client through app.bsky.feed.sendInteractions #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub accepts_interactions: Option, + pub accepts_interactions: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub avatar: Option>, + pub avatar: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub content_mode: Option>, + pub content_mode: std::option::Option>, pub created_at: jacquard_common::types::string::Datetime, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub description: Option>, + pub description: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub description_facets: Option>>, + pub description_facets: std::option::Option< + Vec>, + >, #[serde(borrow)] pub did: jacquard_common::types::string::Did<'a>, #[serde(borrow)] @@ -41,7 +43,7 @@ pub struct Generator<'a> { /// Self-label values #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub labels: Option>, + pub labels: std::option::Option>, } pub mod generator_state { diff --git a/crates/weaver-api/src/app_bsky/feed/like.rs b/crates/weaver-api/src/app_bsky/feed/like.rs index 96dac6f..7d60480 100644 --- a/crates/weaver-api/src/app_bsky/feed/like.rs +++ b/crates/weaver-api/src/app_bsky/feed/like.rs @@ -23,7 +23,7 @@ pub struct Like<'a> { pub subject: crate::com_atproto::repo::strong_ref::StrongRef<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub via: Option>, + pub via: std::option::Option>, } pub mod like_state { diff --git a/crates/weaver-api/src/app_bsky/feed/post.rs b/crates/weaver-api/src/app_bsky/feed/post.rs index 96d8e43..2112aa4 100644 --- a/crates/weaver-api/src/app_bsky/feed/post.rs +++ b/crates/weaver-api/src/app_bsky/feed/post.rs @@ -584,29 +584,29 @@ pub struct Post<'a> { pub created_at: jacquard_common::types::string::Datetime, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub embed: Option>, + pub embed: std::option::Option>, /// DEPRECATED: replaced by app.bsky.richtext.facet. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub entities: Option>>, + pub entities: std::option::Option>>, /// Annotations of text (mentions, URLs, hashtags, etc) #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub facets: Option>>, + pub facets: std::option::Option>>, /// Self-label values for this post. Effectively content warnings. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub labels: Option>, + pub labels: std::option::Option>, /// Indicates human language of post primary text content. #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub langs: Option>, + pub langs: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub reply: Option>, + pub reply: std::option::Option>, /// Additional hashtags, in addition to any included in post text and facets. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub tags: Option>>, + pub tags: std::option::Option>>, /// The primary post content. May be an empty string, if there are embeds. #[serde(borrow)] pub text: jacquard_common::CowStr<'a>, diff --git a/crates/weaver-api/src/app_bsky/feed/postgate.rs b/crates/weaver-api/src/app_bsky/feed/postgate.rs index dfdeb3b..f10b387 100644 --- a/crates/weaver-api/src/app_bsky/feed/postgate.rs +++ b/crates/weaver-api/src/app_bsky/feed/postgate.rs @@ -200,11 +200,15 @@ pub struct Postgate<'a> { /// List of AT-URIs embedding this post that the author has detached from. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub detached_embedding_uris: Option>>, + pub detached_embedding_uris: std::option::Option< + Vec>, + >, /// List of rules defining who can embed this post. If value is an empty array or is undefined, no particular rules apply and anyone can embed. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub embedding_rules: Option>>, + pub embedding_rules: std::option::Option< + Vec>, + >, /// Reference (AT-URI) to the post record. #[serde(borrow)] pub post: jacquard_common::types::string::AtUri<'a>, diff --git a/crates/weaver-api/src/app_bsky/feed/repost.rs b/crates/weaver-api/src/app_bsky/feed/repost.rs index e1c9207..3c88b6b 100644 --- a/crates/weaver-api/src/app_bsky/feed/repost.rs +++ b/crates/weaver-api/src/app_bsky/feed/repost.rs @@ -23,7 +23,7 @@ pub struct Repost<'a> { pub subject: crate::com_atproto::repo::strong_ref::StrongRef<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub via: Option>, + pub via: std::option::Option>, } pub mod repost_state { diff --git a/crates/weaver-api/src/app_bsky/feed/threadgate.rs b/crates/weaver-api/src/app_bsky/feed/threadgate.rs index fb19c71..e68990f 100644 --- a/crates/weaver-api/src/app_bsky/feed/threadgate.rs +++ b/crates/weaver-api/src/app_bsky/feed/threadgate.rs @@ -440,12 +440,14 @@ pub struct Threadgate<'a> { /// List of rules defining who can reply to this post. If value is an empty array, no one can reply. If value is undefined, anyone can reply. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub allow: Option>>, + pub allow: std::option::Option>>, pub created_at: jacquard_common::types::string::Datetime, /// List of hidden reply URIs. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub hidden_replies: Option>>, + pub hidden_replies: std::option::Option< + Vec>, + >, /// Reference (AT-URI) to the post record. #[serde(borrow)] pub post: jacquard_common::types::string::AtUri<'a>, diff --git a/crates/weaver-api/src/app_bsky/graph.rs b/crates/weaver-api/src/app_bsky/graph.rs index d1a8297..f2e3fbd 100644 --- a/crates/weaver-api/src/app_bsky/graph.rs +++ b/crates/weaver-api/src/app_bsky/graph.rs @@ -1237,23 +1237,25 @@ impl jacquard_common::IntoStatic for ListPurpose<'_> { pub struct ListView<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub avatar: Option>, + pub avatar: std::option::Option>, #[serde(borrow)] pub cid: jacquard_common::types::string::Cid<'a>, #[serde(borrow)] pub creator: crate::app_bsky::actor::ProfileView<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub description: Option>, + pub description: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub description_facets: Option>>, + pub description_facets: std::option::Option< + Vec>, + >, pub indexed_at: jacquard_common::types::string::Datetime, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub labels: Option>>, + pub labels: std::option::Option>>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub list_item_count: Option, + pub list_item_count: std::option::Option, #[serde(borrow)] pub name: jacquard_common::CowStr<'a>, #[serde(borrow)] @@ -1262,7 +1264,7 @@ pub struct ListView<'a> { pub uri: jacquard_common::types::string::AtUri<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub viewer: Option>, + pub viewer: std::option::Option>, } pub mod list_view_state { @@ -1802,16 +1804,16 @@ impl<'a> ::jacquard_lexicon::schema::LexiconSchema for ListView<'a> { pub struct ListViewBasic<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub avatar: Option>, + pub avatar: std::option::Option>, #[serde(borrow)] pub cid: jacquard_common::types::string::Cid<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub indexed_at: Option, + pub indexed_at: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub labels: Option>>, + pub labels: std::option::Option>>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub list_item_count: Option, + pub list_item_count: std::option::Option, #[serde(borrow)] pub name: jacquard_common::CowStr<'a>, #[serde(borrow)] @@ -1820,7 +1822,7 @@ pub struct ListViewBasic<'a> { pub uri: jacquard_common::types::string::AtUri<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub viewer: Option>, + pub viewer: std::option::Option>, } pub mod list_view_basic_state { @@ -2473,11 +2475,11 @@ pub struct Relationship<'a> { /// if the actor is followed by this DID, contains the AT-URI of the follow record #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub followed_by: Option>, + pub followed_by: std::option::Option>, /// if the actor follows this DID, this is the AT-URI of the follow record #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub following: Option>, + pub following: std::option::Option>, } pub mod relationship_state { @@ -2664,21 +2666,23 @@ pub struct StarterPackView<'a> { pub creator: crate::app_bsky::actor::ProfileViewBasic<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub feeds: Option>>, + pub feeds: std::option::Option>>, pub indexed_at: jacquard_common::types::string::Datetime, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub joined_all_time_count: Option, + pub joined_all_time_count: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub joined_week_count: Option, + pub joined_week_count: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub labels: Option>>, + pub labels: std::option::Option>>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub list: Option>, + pub list: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub list_items_sample: Option>>, + pub list_items_sample: std::option::Option< + Vec>, + >, #[serde(borrow)] pub record: jacquard_common::types::value::Data<'a>, #[serde(borrow)] @@ -3155,14 +3159,14 @@ pub struct StarterPackViewBasic<'a> { pub creator: crate::app_bsky::actor::ProfileViewBasic<'a>, pub indexed_at: jacquard_common::types::string::Datetime, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub joined_all_time_count: Option, + pub joined_all_time_count: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub joined_week_count: Option, + pub joined_week_count: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub labels: Option>>, + pub labels: std::option::Option>>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub list_item_count: Option, + pub list_item_count: std::option::Option, #[serde(borrow)] pub record: jacquard_common::types::value::Data<'a>, #[serde(borrow)] diff --git a/crates/weaver-api/src/app_bsky/graph/follow.rs b/crates/weaver-api/src/app_bsky/graph/follow.rs index 318b51b..d21ea03 100644 --- a/crates/weaver-api/src/app_bsky/graph/follow.rs +++ b/crates/weaver-api/src/app_bsky/graph/follow.rs @@ -23,7 +23,7 @@ pub struct Follow<'a> { pub subject: jacquard_common::types::string::Did<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub via: Option>, + pub via: std::option::Option>, } pub mod follow_state { diff --git a/crates/weaver-api/src/app_bsky/graph/get_lists_with_membership.rs b/crates/weaver-api/src/app_bsky/graph/get_lists_with_membership.rs index 3c8b0f5..79eb564 100644 --- a/crates/weaver-api/src/app_bsky/graph/get_lists_with_membership.rs +++ b/crates/weaver-api/src/app_bsky/graph/get_lists_with_membership.rs @@ -22,7 +22,7 @@ pub struct ListWithMembership<'a> { pub list: crate::app_bsky::graph::ListView<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub list_item: Option>, + pub list_item: std::option::Option>, } pub mod list_with_membership_state { diff --git a/crates/weaver-api/src/app_bsky/graph/get_starter_packs_with_membership.rs b/crates/weaver-api/src/app_bsky/graph/get_starter_packs_with_membership.rs index fbc0e1d..e3a2ab7 100644 --- a/crates/weaver-api/src/app_bsky/graph/get_starter_packs_with_membership.rs +++ b/crates/weaver-api/src/app_bsky/graph/get_starter_packs_with_membership.rs @@ -234,7 +234,7 @@ impl jacquard_common::xrpc::XrpcEndpoint for GetStarterPacksWithMembershipReques pub struct StarterPackWithMembership<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub list_item: Option>, + pub list_item: std::option::Option>, #[serde(borrow)] pub starter_pack: crate::app_bsky::graph::StarterPackView<'a>, } diff --git a/crates/weaver-api/src/app_bsky/graph/list.rs b/crates/weaver-api/src/app_bsky/graph/list.rs index 30815ed..88b06f9 100644 --- a/crates/weaver-api/src/app_bsky/graph/list.rs +++ b/crates/weaver-api/src/app_bsky/graph/list.rs @@ -20,17 +20,19 @@ pub struct List<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub avatar: Option>, + pub avatar: std::option::Option>, pub created_at: jacquard_common::types::string::Datetime, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub description: Option>, + pub description: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub description_facets: Option>>, + pub description_facets: std::option::Option< + Vec>, + >, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub labels: Option>, + pub labels: std::option::Option>, /// Display name for list; can not be empty. #[serde(borrow)] pub name: jacquard_common::CowStr<'a>, diff --git a/crates/weaver-api/src/app_bsky/graph/starterpack.rs b/crates/weaver-api/src/app_bsky/graph/starterpack.rs index 14a4c04..da639ff 100644 --- a/crates/weaver-api/src/app_bsky/graph/starterpack.rs +++ b/crates/weaver-api/src/app_bsky/graph/starterpack.rs @@ -337,13 +337,17 @@ pub struct Starterpack<'a> { pub created_at: jacquard_common::types::string::Datetime, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub description: Option>, + pub description: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub description_facets: Option>>, + pub description_facets: std::option::Option< + Vec>, + >, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub feeds: Option>>, + pub feeds: std::option::Option< + Vec>, + >, /// Reference (AT-URI) to the list record. #[serde(borrow)] pub list: jacquard_common::types::string::AtUri<'a>, diff --git a/crates/weaver-api/src/app_bsky/labeler.rs b/crates/weaver-api/src/app_bsky/labeler.rs index 3f18949..311608e 100644 --- a/crates/weaver-api/src/app_bsky/labeler.rs +++ b/crates/weaver-api/src/app_bsky/labeler.rs @@ -23,7 +23,7 @@ pub struct LabelerPolicies<'a> { /// Label values created by this labeler and scoped exclusively to it. Labels defined here will override global label definitions for this labeler. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub label_value_definitions: Option< + pub label_value_definitions: std::option::Option< Vec>, >, /// The label values which this labeler publishes. May include global or custom labels. @@ -612,14 +612,14 @@ pub struct LabelerView<'a> { pub indexed_at: jacquard_common::types::string::Datetime, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub labels: Option>>, + pub labels: std::option::Option>>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub like_count: Option, + pub like_count: std::option::Option, #[serde(borrow)] pub uri: jacquard_common::types::string::AtUri<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub viewer: Option>, + pub viewer: std::option::Option>, } pub mod labeler_view_state { @@ -945,28 +945,34 @@ pub struct LabelerViewDetailed<'a> { pub indexed_at: jacquard_common::types::string::Datetime, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub labels: Option>>, + pub labels: std::option::Option>>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub like_count: Option, + pub like_count: std::option::Option, #[serde(borrow)] pub policies: crate::app_bsky::labeler::LabelerPolicies<'a>, /// The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub reason_types: Option>>, + pub reason_types: std::option::Option< + Vec>, + >, /// Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub subject_collections: Option>>, + pub subject_collections: std::option::Option< + Vec>, + >, /// The set of subject types (account, record, etc) this service accepts reports on. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub subject_types: Option>>, + pub subject_types: std::option::Option< + Vec>, + >, #[serde(borrow)] pub uri: jacquard_common::types::string::AtUri<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub viewer: Option>, + pub viewer: std::option::Option>, } pub mod labeler_view_detailed_state { diff --git a/crates/weaver-api/src/app_bsky/labeler/service.rs b/crates/weaver-api/src/app_bsky/labeler/service.rs index 66b897b..c9ccb61 100644 --- a/crates/weaver-api/src/app_bsky/labeler/service.rs +++ b/crates/weaver-api/src/app_bsky/labeler/service.rs @@ -21,21 +21,27 @@ pub struct Service<'a> { pub created_at: jacquard_common::types::string::Datetime, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub labels: Option>, + pub labels: std::option::Option>, #[serde(borrow)] pub policies: crate::app_bsky::labeler::LabelerPolicies<'a>, /// The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub reason_types: Option>>, + pub reason_types: std::option::Option< + Vec>, + >, /// Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub subject_collections: Option>>, + pub subject_collections: std::option::Option< + Vec>, + >, /// The set of subject types (account, record, etc) this service accepts reports on. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub subject_types: Option>>, + pub subject_types: std::option::Option< + Vec>, + >, } pub mod service_state { diff --git a/crates/weaver-api/src/app_bsky/notification/list_notifications.rs b/crates/weaver-api/src/app_bsky/notification/list_notifications.rs index 26d77de..cd00439 100644 --- a/crates/weaver-api/src/app_bsky/notification/list_notifications.rs +++ b/crates/weaver-api/src/app_bsky/notification/list_notifications.rs @@ -248,13 +248,13 @@ pub struct Notification<'a> { pub is_read: bool, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub labels: Option>>, + pub labels: std::option::Option>>, /// The reason why this notification was delivered - e.g. your post was liked, or you received a new follower. #[serde(borrow)] pub reason: jacquard_common::CowStr<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub reason_subject: Option>, + pub reason_subject: std::option::Option>, #[serde(borrow)] pub record: jacquard_common::types::value::Data<'a>, #[serde(borrow)] diff --git a/crates/weaver-api/src/app_bsky/notification/register_push.rs b/crates/weaver-api/src/app_bsky/notification/register_push.rs index c7c8ece..be75103 100644 --- a/crates/weaver-api/src/app_bsky/notification/register_push.rs +++ b/crates/weaver-api/src/app_bsky/notification/register_push.rs @@ -19,7 +19,7 @@ pub struct RegisterPush<'a> { /// Set to true when the actor is age restricted #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub age_restricted: Option, + pub age_restricted: std::option::Option, #[serde(borrow)] pub app_id: jacquard_common::CowStr<'a>, #[serde(borrow)] diff --git a/crates/weaver-api/src/app_bsky/unspecced.rs b/crates/weaver-api/src/app_bsky/unspecced.rs index 7b159d7..86542a1 100644 --- a/crates/weaver-api/src/app_bsky/unspecced.rs +++ b/crates/weaver-api/src/app_bsky/unspecced.rs @@ -47,25 +47,25 @@ pub struct AgeAssuranceEvent<'a> { /// The IP address used when completing the AA flow. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub complete_ip: Option>, + pub complete_ip: std::option::Option>, /// The user agent used when completing the AA flow. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub complete_ua: Option>, + pub complete_ua: std::option::Option>, /// The date and time of this write operation. pub created_at: jacquard_common::types::string::Datetime, /// The email used for AA. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub email: Option>, + pub email: std::option::Option>, /// The IP address used when initiating the AA flow. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub init_ip: Option>, + pub init_ip: std::option::Option>, /// The user agent used when initiating the AA flow. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub init_ua: Option>, + pub init_ua: std::option::Option>, /// The status of the age assurance process. #[serde(borrow)] pub status: jacquard_common::CowStr<'a>, @@ -1717,7 +1717,7 @@ impl<'a> ::jacquard_lexicon::schema::LexiconSchema for SkeletonSearchStarterPack pub struct SkeletonTrend<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub category: Option>, + pub category: std::option::Option>, #[serde(borrow)] pub dids: Vec>, #[serde(borrow)] @@ -1728,7 +1728,7 @@ pub struct SkeletonTrend<'a> { pub started_at: jacquard_common::types::string::Datetime, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub status: Option>, + pub status: std::option::Option>, #[serde(borrow)] pub topic: jacquard_common::CowStr<'a>, } @@ -2645,7 +2645,7 @@ pub struct TrendView<'a> { pub actors: Vec>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub category: Option>, + pub category: std::option::Option>, #[serde(borrow)] pub display_name: jacquard_common::CowStr<'a>, #[serde(borrow)] @@ -2654,7 +2654,7 @@ pub struct TrendView<'a> { pub started_at: jacquard_common::types::string::Datetime, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub status: Option>, + pub status: std::option::Option>, #[serde(borrow)] pub topic: jacquard_common::CowStr<'a>, } diff --git a/crates/weaver-api/src/app_bsky/video.rs b/crates/weaver-api/src/app_bsky/video.rs index 0227ef8..582ebf2 100644 --- a/crates/weaver-api/src/app_bsky/video.rs +++ b/crates/weaver-api/src/app_bsky/video.rs @@ -23,20 +23,20 @@ pub mod upload_video; pub struct JobStatus<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub blob: Option>, + pub blob: std::option::Option>, #[serde(borrow)] pub did: jacquard_common::types::string::Did<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub error: Option>, + pub error: std::option::Option>, #[serde(borrow)] pub job_id: jacquard_common::CowStr<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub message: Option>, + pub message: std::option::Option>, /// Progress within the current processing state. #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub progress: Option, + pub progress: std::option::Option, /// The state of the video processing job. All values not listed as a known value indicate that the job is in process. #[serde(borrow)] pub state: jacquard_common::CowStr<'a>, diff --git a/crates/weaver-api/src/chat_bsky/actor.rs b/crates/weaver-api/src/chat_bsky/actor.rs index c4207a0..ced6c00 100644 --- a/crates/weaver-api/src/chat_bsky/actor.rs +++ b/crates/weaver-api/src/chat_bsky/actor.rs @@ -23,29 +23,29 @@ pub mod export_account_data; pub struct ProfileViewBasic<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub associated: Option>, + pub associated: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub avatar: Option>, + pub avatar: std::option::Option>, /// Set to true when the actor cannot actively participate in conversations #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub chat_disabled: Option, + pub chat_disabled: std::option::Option, #[serde(borrow)] pub did: jacquard_common::types::string::Did<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub display_name: Option>, + pub display_name: std::option::Option>, #[serde(borrow)] pub handle: jacquard_common::types::string::Handle<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub labels: Option>>, + pub labels: std::option::Option>>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub verification: Option>, + pub verification: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub viewer: Option>, + pub viewer: std::option::Option>, } pub mod profile_view_basic_state { diff --git a/crates/weaver-api/src/chat_bsky/convo.rs b/crates/weaver-api/src/chat_bsky/convo.rs index 8193f9e..811a304 100644 --- a/crates/weaver-api/src/chat_bsky/convo.rs +++ b/crates/weaver-api/src/chat_bsky/convo.rs @@ -39,10 +39,12 @@ pub struct ConvoView<'a> { pub id: jacquard_common::CowStr<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub last_message: Option>, + pub last_message: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub last_reaction: Option>, + pub last_reaction: std::option::Option< + crate::chat_bsky::convo::MessageAndReactionView<'a>, + >, #[serde(borrow)] pub members: Vec>, pub muted: bool, @@ -50,7 +52,7 @@ pub struct ConvoView<'a> { pub rev: jacquard_common::CowStr<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub status: Option>, + pub status: std::option::Option>, pub unread_count: i64, } @@ -3781,17 +3783,17 @@ impl<'a> ::jacquard_lexicon::schema::LexiconSchema for MessageRef<'a> { pub struct MessageView<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub embed: Option>, + pub embed: std::option::Option>, /// Annotations of text (mentions, URLs, hashtags, etc) #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub facets: Option>>, + pub facets: std::option::Option>>, #[serde(borrow)] pub id: jacquard_common::CowStr<'a>, /// Reactions to this message, in ascending order of creation time. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub reactions: Option>>, + pub reactions: std::option::Option>>, #[serde(borrow)] pub rev: jacquard_common::CowStr<'a>, #[serde(borrow)] diff --git a/crates/weaver-api/src/chat_bsky/moderation/update_actor_access.rs b/crates/weaver-api/src/chat_bsky/moderation/update_actor_access.rs index ff019e8..ebdd92b 100644 --- a/crates/weaver-api/src/chat_bsky/moderation/update_actor_access.rs +++ b/crates/weaver-api/src/chat_bsky/moderation/update_actor_access.rs @@ -22,7 +22,7 @@ pub struct UpdateActorAccess<'a> { pub allow_access: bool, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub r#ref: Option>, + pub r#ref: std::option::Option>, } pub mod update_actor_access_state { diff --git a/crates/weaver-api/src/com_atproto/admin.rs b/crates/weaver-api/src/com_atproto/admin.rs index 1bceff0..722a361 100644 --- a/crates/weaver-api/src/com_atproto/admin.rs +++ b/crates/weaver-api/src/com_atproto/admin.rs @@ -34,34 +34,40 @@ pub mod update_subject_status; #[serde(rename_all = "camelCase")] pub struct AccountView<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub deactivated_at: Option, + pub deactivated_at: std::option::Option, #[serde(borrow)] pub did: jacquard_common::types::string::Did<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub email: Option>, + pub email: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub email_confirmed_at: Option, + pub email_confirmed_at: std::option::Option< + jacquard_common::types::string::Datetime, + >, #[serde(borrow)] pub handle: jacquard_common::types::string::Handle<'a>, pub indexed_at: jacquard_common::types::string::Datetime, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub invite_note: Option>, + pub invite_note: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub invited_by: Option>, + pub invited_by: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub invites: Option>>, + pub invites: std::option::Option>>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub invites_disabled: Option, + pub invites_disabled: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub related_records: Option>>, + pub related_records: std::option::Option< + Vec>, + >, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub threat_signatures: Option>>, + pub threat_signatures: std::option::Option< + Vec>, + >, } pub mod account_view_state { @@ -888,7 +894,7 @@ pub struct RepoBlobRef<'a> { pub did: jacquard_common::types::string::Did<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub record_uri: Option>, + pub record_uri: std::option::Option>, } pub mod repo_blob_ref_state { @@ -1223,7 +1229,7 @@ pub struct StatusAttr<'a> { pub applied: bool, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub r#ref: Option>, + pub r#ref: std::option::Option>, } pub mod status_attr_state { diff --git a/crates/weaver-api/src/com_atproto/admin/disable_account_invites.rs b/crates/weaver-api/src/com_atproto/admin/disable_account_invites.rs index ea92e41..56c9b4b 100644 --- a/crates/weaver-api/src/com_atproto/admin/disable_account_invites.rs +++ b/crates/weaver-api/src/com_atproto/admin/disable_account_invites.rs @@ -22,7 +22,7 @@ pub struct DisableAccountInvites<'a> { /// Optional reason for disabled invites. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub note: Option>, + pub note: std::option::Option>, } pub mod disable_account_invites_state { diff --git a/crates/weaver-api/src/com_atproto/admin/enable_account_invites.rs b/crates/weaver-api/src/com_atproto/admin/enable_account_invites.rs index f0c8a0f..fafce93 100644 --- a/crates/weaver-api/src/com_atproto/admin/enable_account_invites.rs +++ b/crates/weaver-api/src/com_atproto/admin/enable_account_invites.rs @@ -22,7 +22,7 @@ pub struct EnableAccountInvites<'a> { /// Optional reason for enabled invites. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub note: Option>, + pub note: std::option::Option>, } pub mod enable_account_invites_state { diff --git a/crates/weaver-api/src/com_atproto/admin/send_email.rs b/crates/weaver-api/src/com_atproto/admin/send_email.rs index 290ed23..47b3dfe 100644 --- a/crates/weaver-api/src/com_atproto/admin/send_email.rs +++ b/crates/weaver-api/src/com_atproto/admin/send_email.rs @@ -20,7 +20,7 @@ pub struct SendEmail<'a> { /// Additional comment by the sender that won't be used in the email itself but helpful to provide more context for moderators/reviewers #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub comment: Option>, + pub comment: std::option::Option>, #[serde(borrow)] pub content: jacquard_common::CowStr<'a>, #[serde(borrow)] @@ -29,7 +29,7 @@ pub struct SendEmail<'a> { pub sender_did: jacquard_common::types::string::Did<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub subject: Option>, + pub subject: std::option::Option>, } pub mod send_email_state { diff --git a/crates/weaver-api/src/com_atproto/admin/update_subject_status.rs b/crates/weaver-api/src/com_atproto/admin/update_subject_status.rs index 141a2b0..9e83fd1 100644 --- a/crates/weaver-api/src/com_atproto/admin/update_subject_status.rs +++ b/crates/weaver-api/src/com_atproto/admin/update_subject_status.rs @@ -19,12 +19,12 @@ pub struct UpdateSubjectStatus<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub deactivated: Option>, + pub deactivated: std::option::Option>, #[serde(borrow)] pub subject: UpdateSubjectStatusSubject<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub takedown: Option>, + pub takedown: std::option::Option>, } pub mod update_subject_status_state { diff --git a/crates/weaver-api/src/com_atproto/label.rs b/crates/weaver-api/src/com_atproto/label.rs index 4ba463a..e2665e4 100644 --- a/crates/weaver-api/src/com_atproto/label.rs +++ b/crates/weaver-api/src/com_atproto/label.rs @@ -26,18 +26,18 @@ pub struct Label<'a> { /// Optionally, CID specifying the specific version of 'uri' resource this label applies to. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub cid: Option>, + pub cid: std::option::Option>, /// Timestamp when this label was created. pub cts: jacquard_common::types::string::Datetime, /// Timestamp at which this label expires (no longer applies). #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub exp: Option, + pub exp: std::option::Option, /// If true, this is a negation label, overwriting a previous label. #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub neg: Option, + pub neg: std::option::Option, /// Signature of dag-cbor encoded label. #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub sig: Option, + pub sig: std::option::Option, /// DID of the actor who created this label. #[serde(borrow)] pub src: jacquard_common::types::string::Did<'a>, @@ -49,7 +49,7 @@ pub struct Label<'a> { pub val: jacquard_common::CowStr<'a>, /// The AT Protocol version of the label object. #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub ver: Option, + pub ver: std::option::Option, } pub mod label_state { @@ -1024,14 +1024,14 @@ impl jacquard_common::IntoStatic for LabelValue<'_> { pub struct LabelValueDefinition<'a> { /// Does the user need to have adult content enabled in order to configure this label? #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub adult_only: Option, + pub adult_only: std::option::Option, /// What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing. #[serde(borrow)] pub blurs: jacquard_common::CowStr<'a>, /// The default setting for this label. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub default_setting: Option>, + pub default_setting: std::option::Option>, /// The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+). #[serde(borrow)] pub identifier: jacquard_common::CowStr<'a>, diff --git a/crates/weaver-api/src/com_atproto/moderation/create_report.rs b/crates/weaver-api/src/com_atproto/moderation/create_report.rs index 2c31ff8..bdf6181 100644 --- a/crates/weaver-api/src/com_atproto/moderation/create_report.rs +++ b/crates/weaver-api/src/com_atproto/moderation/create_report.rs @@ -19,11 +19,13 @@ pub struct CreateReport<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub mod_tool: Option>, + pub mod_tool: std::option::Option< + crate::com_atproto::moderation::create_report::ModTool<'a>, + >, /// Additional context about the content and violation. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub reason: Option>, + pub reason: std::option::Option>, /// Indicates the broad category of violation the report is for. #[serde(borrow)] pub reason_type: crate::com_atproto::moderation::ReasonType<'a>, diff --git a/crates/weaver-api/src/com_atproto/repo/apply_writes.rs b/crates/weaver-api/src/com_atproto/repo/apply_writes.rs index 646303c..16632c8 100644 --- a/crates/weaver-api/src/com_atproto/repo/apply_writes.rs +++ b/crates/weaver-api/src/com_atproto/repo/apply_writes.rs @@ -23,7 +23,7 @@ pub struct Create<'a> { /// NOTE: maxLength is redundant with record-key format. Keeping it temporarily to ensure backwards compatibility. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub rkey: Option< + pub rkey: std::option::Option< jacquard_common::types::string::RecordKey< jacquard_common::types::string::Rkey<'a>, >, @@ -715,7 +715,7 @@ pub struct CreateResult<'a> { pub uri: jacquard_common::types::string::AtUri<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub validation_status: Option>, + pub validation_status: std::option::Option>, } pub mod create_result_state { @@ -1131,10 +1131,10 @@ pub struct ApplyWrites<'a> { /// If provided, the entire operation will fail if the current repo commit CID does not match this value. Used to prevent conflicting repo mutations. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub swap_commit: Option>, + pub swap_commit: std::option::Option>, /// Can be set to 'false' to skip Lexicon schema validation of record data across all operations, 'true' to require it, or leave unset to validate only for known Lexicons. #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub validate: Option, + pub validate: std::option::Option, #[serde(borrow)] pub writes: Vec>, } @@ -1686,7 +1686,7 @@ pub struct UpdateResult<'a> { pub uri: jacquard_common::types::string::AtUri<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub validation_status: Option>, + pub validation_status: std::option::Option>, } pub mod update_result_state { diff --git a/crates/weaver-api/src/com_atproto/repo/create_record.rs b/crates/weaver-api/src/com_atproto/repo/create_record.rs index 830dfef..6e91b26 100644 --- a/crates/weaver-api/src/com_atproto/repo/create_record.rs +++ b/crates/weaver-api/src/com_atproto/repo/create_record.rs @@ -29,7 +29,7 @@ pub struct CreateRecord<'a> { /// The Record Key. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub rkey: Option< + pub rkey: std::option::Option< jacquard_common::types::string::RecordKey< jacquard_common::types::string::Rkey<'a>, >, @@ -37,10 +37,10 @@ pub struct CreateRecord<'a> { /// Compare and swap with the previous commit by CID. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub swap_commit: Option>, + pub swap_commit: std::option::Option>, /// Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons. #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub validate: Option, + pub validate: std::option::Option, } pub mod create_record_state { diff --git a/crates/weaver-api/src/com_atproto/repo/delete_record.rs b/crates/weaver-api/src/com_atproto/repo/delete_record.rs index 817fa9e..b546dff 100644 --- a/crates/weaver-api/src/com_atproto/repo/delete_record.rs +++ b/crates/weaver-api/src/com_atproto/repo/delete_record.rs @@ -31,11 +31,11 @@ pub struct DeleteRecord<'a> { /// Compare and swap with the previous commit by CID. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub swap_commit: Option>, + pub swap_commit: std::option::Option>, /// Compare and swap with the previous record by CID. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub swap_record: Option>, + pub swap_record: std::option::Option>, } pub mod delete_record_state { diff --git a/crates/weaver-api/src/com_atproto/repo/put_record.rs b/crates/weaver-api/src/com_atproto/repo/put_record.rs index 8e4ae48..318e40e 100644 --- a/crates/weaver-api/src/com_atproto/repo/put_record.rs +++ b/crates/weaver-api/src/com_atproto/repo/put_record.rs @@ -34,14 +34,14 @@ pub struct PutRecord<'a> { /// Compare and swap with the previous commit by CID. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub swap_commit: Option>, + pub swap_commit: std::option::Option>, /// Compare and swap with the previous record by CID. WARNING: nullable and optional field; may cause problems with golang implementation #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub swap_record: Option>, + pub swap_record: std::option::Option>, /// Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons. #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub validate: Option, + pub validate: std::option::Option, } pub mod put_record_state { diff --git a/crates/weaver-api/src/com_atproto/server/create_account.rs b/crates/weaver-api/src/com_atproto/server/create_account.rs index 1dfcaba..19ff6a2 100644 --- a/crates/weaver-api/src/com_atproto/server/create_account.rs +++ b/crates/weaver-api/src/com_atproto/server/create_account.rs @@ -20,34 +20,34 @@ pub struct CreateAccount<'a> { /// Pre-existing atproto DID, being imported to a new account. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub did: Option>, + pub did: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub email: Option>, + pub email: std::option::Option>, /// Requested handle for the account. #[serde(borrow)] pub handle: jacquard_common::types::string::Handle<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub invite_code: Option>, + pub invite_code: std::option::Option>, /// Initial account password. May need to meet instance-specific password strength requirements. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub password: Option>, + pub password: std::option::Option>, /// A signed DID PLC operation to be submitted as part of importing an existing account to this instance. NOTE: this optional field may be updated when full account migration is implemented. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub plc_op: Option>, + pub plc_op: std::option::Option>, /// DID PLC rotation key (aka, recovery key) to be included in PLC creation operation. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub recovery_key: Option>, + pub recovery_key: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub verification_code: Option>, + pub verification_code: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub verification_phone: Option>, + pub verification_phone: std::option::Option>, } pub mod create_account_state { diff --git a/crates/weaver-api/src/com_atproto/server/create_app_password.rs b/crates/weaver-api/src/com_atproto/server/create_app_password.rs index bb1450c..5c2d7aa 100644 --- a/crates/weaver-api/src/com_atproto/server/create_app_password.rs +++ b/crates/weaver-api/src/com_atproto/server/create_app_password.rs @@ -23,7 +23,7 @@ pub struct AppPassword<'a> { #[serde(borrow)] pub password: jacquard_common::CowStr<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub privileged: Option, + pub privileged: std::option::Option, } pub mod app_password_state { diff --git a/crates/weaver-api/src/com_atproto/server/create_invite_code.rs b/crates/weaver-api/src/com_atproto/server/create_invite_code.rs index ca8b14e..291a78f 100644 --- a/crates/weaver-api/src/com_atproto/server/create_invite_code.rs +++ b/crates/weaver-api/src/com_atproto/server/create_invite_code.rs @@ -19,7 +19,7 @@ pub struct CreateInviteCode<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub for_account: Option>, + pub for_account: std::option::Option>, pub use_count: i64, } diff --git a/crates/weaver-api/src/com_atproto/server/create_invite_codes.rs b/crates/weaver-api/src/com_atproto/server/create_invite_codes.rs index cf2512d..4eb9651 100644 --- a/crates/weaver-api/src/com_atproto/server/create_invite_codes.rs +++ b/crates/weaver-api/src/com_atproto/server/create_invite_codes.rs @@ -347,7 +347,7 @@ pub struct CreateInviteCodes<'a> { pub code_count: i64, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub for_accounts: Option>>, + pub for_accounts: std::option::Option>>, pub use_count: i64, } diff --git a/crates/weaver-api/src/com_atproto/server/list_app_passwords.rs b/crates/weaver-api/src/com_atproto/server/list_app_passwords.rs index d206079..0500c91 100644 --- a/crates/weaver-api/src/com_atproto/server/list_app_passwords.rs +++ b/crates/weaver-api/src/com_atproto/server/list_app_passwords.rs @@ -21,7 +21,7 @@ pub struct AppPassword<'a> { #[serde(borrow)] pub name: jacquard_common::CowStr<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub privileged: Option, + pub privileged: std::option::Option, } pub mod app_password_state { diff --git a/crates/weaver-api/src/com_atproto/sync/list_repos.rs b/crates/weaver-api/src/com_atproto/sync/list_repos.rs index 128f33f..a6ef385 100644 --- a/crates/weaver-api/src/com_atproto/sync/list_repos.rs +++ b/crates/weaver-api/src/com_atproto/sync/list_repos.rs @@ -171,7 +171,7 @@ impl jacquard_common::xrpc::XrpcEndpoint for ListReposRequest { #[serde(rename_all = "camelCase")] pub struct Repo<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub active: Option, + pub active: std::option::Option, #[serde(borrow)] pub did: jacquard_common::types::string::Did<'a>, /// Current repo commit CID @@ -181,7 +181,7 @@ pub struct Repo<'a> { /// If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub status: Option>, + pub status: std::option::Option>, } pub mod repo_state { diff --git a/crates/weaver-api/src/com_atproto/sync/subscribe_repos.rs b/crates/weaver-api/src/com_atproto/sync/subscribe_repos.rs index 642b49f..5c811d4 100644 --- a/crates/weaver-api/src/com_atproto/sync/subscribe_repos.rs +++ b/crates/weaver-api/src/com_atproto/sync/subscribe_repos.rs @@ -26,7 +26,7 @@ pub struct Account<'a> { /// If active=false, this optional field indicates a reason for why the account is not active. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub status: Option>, + pub status: std::option::Option>, pub time: jacquard_common::types::string::Datetime, } @@ -938,7 +938,7 @@ pub struct Commit<'a> { /// The root CID of the MST tree for the previous commit from this repo (indicated by the 'since' revision field in this message). Corresponds to the 'data' field in the repo commit object. NOTE: this field is effectively required for the 'inductive' version of firehose. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub prev_data: Option>, + pub prev_data: std::option::Option>, /// DEPRECATED -- unused pub rebase: bool, /// The repo this event comes from. Note that all other message types name this field 'did'. @@ -1586,7 +1586,7 @@ pub struct Identity<'a> { /// The current handle for the account, or 'handle.invalid' if validation fails. This field is optional, might have been validated or passed-through from an upstream source. Semantics and behaviors for PDS vs Relay may evolve in the future; see atproto specs for more details. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub handle: Option>, + pub handle: std::option::Option>, pub seq: i64, pub time: jacquard_common::types::string::Datetime, } @@ -2085,7 +2085,7 @@ pub struct RepoOp<'a> { /// For updates and deletes, the previous record CID (required for inductive firehose). For creations, field should not be defined. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub prev: Option>, + pub prev: std::option::Option>, } pub mod repo_op_state { diff --git a/crates/weaver-api/src/sh_weaver/actor.rs b/crates/weaver-api/src/sh_weaver/actor.rs index f231dea..97b6cc2 100644 --- a/crates/weaver-api/src/sh_weaver/actor.rs +++ b/crates/weaver-api/src/sh_weaver/actor.rs @@ -25,7 +25,7 @@ pub struct Author<'a> { pub did: jacquard_common::types::string::Did<'a>, /// signed bytes of the corresponding notebook record in the author's repo #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub signature: Option, + pub signature: std::option::Option, } pub mod author_state { @@ -1256,55 +1256,55 @@ impl<'a> ::jacquard_lexicon::schema::LexiconSchema for ProfileDataViewBasic<'a> pub struct ProfileView<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub avatar: Option>, + pub avatar: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub banner: Option>, + pub banner: std::option::Option>, /// Include link to this account on Bluesky. #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub bluesky: Option, + pub bluesky: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub created_at: Option, + pub created_at: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub description: Option>, + pub description: std::option::Option>, #[serde(borrow)] pub did: jacquard_common::types::string::Did<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub display_name: Option>, + pub display_name: std::option::Option>, #[serde(borrow)] pub handle: jacquard_common::types::string::Handle<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub indexed_at: Option, + pub indexed_at: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub labels: Option>>, + pub labels: std::option::Option>>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub links: Option>>, + pub links: std::option::Option>>, /// Free-form location text. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub location: Option>, + pub location: std::option::Option>, /// Notebooks or other records pinned for display. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub pinned: Option>, + pub pinned: std::option::Option>, /// Pronouns to use in user-generated content. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub pronouns: Option>, + pub pronouns: std::option::Option>, /// Include link to this account on stream.place. #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub streamplace: Option, + pub streamplace: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub subscribed_count: Option, + pub subscribed_count: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub subscriber_count: Option, + pub subscriber_count: std::option::Option, /// Include link to this account on Tangled. #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub tangled: Option, + pub tangled: std::option::Option, } pub mod profile_view_state { @@ -1930,25 +1930,25 @@ impl<'a> ::jacquard_lexicon::schema::LexiconSchema for ProfileView<'a> { pub struct ProfileViewBasic<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub avatar: Option>, + pub avatar: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub created_at: Option, + pub created_at: std::option::Option, #[serde(borrow)] pub did: jacquard_common::types::string::Did<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub display_name: Option>, + pub display_name: std::option::Option>, #[serde(borrow)] pub handle: jacquard_common::types::string::Handle<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub indexed_at: Option, + pub indexed_at: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub labels: Option>>, + pub labels: std::option::Option>>, /// Pronouns to use in user-generated content. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub pronouns: Option>, + pub pronouns: std::option::Option>, } pub mod profile_view_basic_state { @@ -2288,25 +2288,27 @@ pub struct TangledProfileView<'a> { /// Free-form profile description text. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub description: Option>, + pub description: std::option::Option>, #[serde(borrow)] pub did: jacquard_common::types::string::Did<'a>, #[serde(borrow)] pub handle: jacquard_common::types::string::Handle<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub links: Option>>, + pub links: std::option::Option>>, /// Free-form location text. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub location: Option>, + pub location: std::option::Option>, /// Any ATURI, it is up to appviews to validate these fields. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub pinned_repositories: Option>>, + pub pinned_repositories: std::option::Option< + Vec>, + >, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub stats: Option>>, + pub stats: std::option::Option>>, } pub mod tangled_profile_view_state { diff --git a/crates/weaver-api/src/sh_weaver/actor/profile.rs b/crates/weaver-api/src/sh_weaver/actor/profile.rs index f0143ef..0d73cb7 100644 --- a/crates/weaver-api/src/sh_weaver/actor/profile.rs +++ b/crates/weaver-api/src/sh_weaver/actor/profile.rs @@ -21,48 +21,48 @@ pub struct Profile<'a> { /// Small image to be displayed next to posts from account. AKA, 'profile picture' #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub avatar: Option>, + pub avatar: std::option::Option>, /// Larger horizontal image to display behind profile view. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub banner: Option>, + pub banner: std::option::Option>, /// Include link to this account on Bluesky. #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub bluesky: Option, + pub bluesky: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub created_at: Option, + pub created_at: std::option::Option, /// Free-form profile description text. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub description: Option>, + pub description: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub display_name: Option>, + pub display_name: std::option::Option>, /// Self-label values, specific to the application, on the overall account. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub labels: Option>, + pub labels: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub links: Option>>, + pub links: std::option::Option>>, /// Free-form location text. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub location: Option>, + pub location: std::option::Option>, /// Notebooks or other records pinned for display. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub pinned: Option>, + pub pinned: std::option::Option>, /// Pronouns to use in user-generated content. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub pronouns: Option>, + pub pronouns: std::option::Option>, /// Include link to this account on stream.place. #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub streamplace: Option, + pub streamplace: std::option::Option, /// Include link to this account on Tangled. #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub tangled: Option, + pub tangled: std::option::Option, } pub mod profile_state { diff --git a/crates/weaver-api/src/sh_weaver/edit/cursor.rs b/crates/weaver-api/src/sh_weaver/edit/cursor.rs index 7b2db06..676a9dd 100644 --- a/crates/weaver-api/src/sh_weaver/edit/cursor.rs +++ b/crates/weaver-api/src/sh_weaver/edit/cursor.rs @@ -746,7 +746,7 @@ pub struct Cursor<'a> { pub id: crate::sh_weaver::edit::cursor::Id<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub side: Option>, + pub side: std::option::Option>, } pub mod cursor_state { diff --git a/crates/weaver-api/src/sh_weaver/embed/external.rs b/crates/weaver-api/src/sh_weaver/embed/external.rs index 25a82d3..b8ec638 100644 --- a/crates/weaver-api/src/sh_weaver/embed/external.rs +++ b/crates/weaver-api/src/sh_weaver/embed/external.rs @@ -21,7 +21,7 @@ pub struct ExternalEmbed<'a> { pub description: jacquard_common::CowStr<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub thumb: Option>, + pub thumb: std::option::Option>, #[serde(borrow)] pub title: jacquard_common::CowStr<'a>, #[serde(borrow)] @@ -800,7 +800,7 @@ pub struct ViewExternal<'a> { pub description: jacquard_common::CowStr<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub thumb: Option>, + pub thumb: std::option::Option>, #[serde(borrow)] pub title: jacquard_common::CowStr<'a>, #[serde(borrow)] diff --git a/crates/weaver-api/src/sh_weaver/embed/images.rs b/crates/weaver-api/src/sh_weaver/embed/images.rs index 0caf799..aa61830 100644 --- a/crates/weaver-api/src/sh_weaver/embed/images.rs +++ b/crates/weaver-api/src/sh_weaver/embed/images.rs @@ -23,15 +23,15 @@ pub struct Image<'a> { /// Blurhash string for the image, used for low-resolution placeholders. This must be a valid Blurhash string. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub blurhash: Option>, + pub blurhash: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub dimensions: Option>, + pub dimensions: std::option::Option>, #[serde(borrow)] pub image: jacquard_common::types::blob::BlobRef<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub name: Option>, + pub name: std::option::Option>, } pub mod image_state { @@ -880,13 +880,13 @@ pub struct ViewImage<'a> { pub alt: jacquard_common::CowStr<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub dimensions: Option>, + pub dimensions: std::option::Option>, /// Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View. #[serde(borrow)] pub fullsize: jacquard_common::types::string::Uri<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub name: Option>, + pub name: std::option::Option>, /// Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View. #[serde(borrow)] pub thumb: jacquard_common::types::string::Uri<'a>, diff --git a/crates/weaver-api/src/sh_weaver/embed/record_with_media.rs b/crates/weaver-api/src/sh_weaver/embed/record_with_media.rs index a0d41b6..710b339 100644 --- a/crates/weaver-api/src/sh_weaver/embed/record_with_media.rs +++ b/crates/weaver-api/src/sh_weaver/embed/record_with_media.rs @@ -310,7 +310,7 @@ pub struct View<'a> { pub media: ViewMedia<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub records: Option>, + pub records: std::option::Option>, } pub mod view_state { diff --git a/crates/weaver-api/src/sh_weaver/embed/records.rs b/crates/weaver-api/src/sh_weaver/embed/records.rs index 013a4e9..209ca2b 100644 --- a/crates/weaver-api/src/sh_weaver/embed/records.rs +++ b/crates/weaver-api/src/sh_weaver/embed/records.rs @@ -638,7 +638,7 @@ impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Records<'a> { pub struct RecordEmbed<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub name: Option>, + pub name: std::option::Option>, #[serde(borrow)] pub record: crate::com_atproto::repo::strong_ref::StrongRef<'a>, } @@ -810,7 +810,7 @@ impl<'a> ::jacquard_lexicon::schema::LexiconSchema for RecordEmbed<'a> { pub struct RecordEmbedView<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub name: Option>, + pub name: std::option::Option>, #[serde(borrow)] pub record_view: RecordEmbedViewRecordView<'a>, } @@ -1737,19 +1737,21 @@ pub struct ViewRecord<'a> { pub cid: jacquard_common::types::string::Cid<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub embeds: Option>>, + pub embeds: std::option::Option< + Vec>, + >, pub indexed_at: jacquard_common::types::string::Datetime, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub labels: Option>>, + pub labels: std::option::Option>>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub like_count: Option, + pub like_count: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub quote_count: Option, + pub quote_count: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub reply_count: Option, + pub reply_count: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub repost_count: Option, + pub repost_count: std::option::Option, #[serde(borrow)] pub uri: jacquard_common::types::string::AtUri<'a>, /// The record data itself. diff --git a/crates/weaver-api/src/sh_weaver/embed/video.rs b/crates/weaver-api/src/sh_weaver/embed/video.rs index 0012428..c81e035 100644 --- a/crates/weaver-api/src/sh_weaver/embed/video.rs +++ b/crates/weaver-api/src/sh_weaver/embed/video.rs @@ -615,16 +615,16 @@ pub struct Video<'a> { /// Alt text description of the video, for accessibility. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub alt: Option>, + pub alt: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub captions: Option>>, + pub captions: std::option::Option>>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub dimensions: Option>, + pub dimensions: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub name: Option>, + pub name: std::option::Option>, /// The mp4 video file. May be up to 100mb, formerly limited to 50mb. #[serde(borrow)] pub video: jacquard_common::types::blob::BlobRef<'a>, @@ -914,20 +914,20 @@ impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Video<'a> { pub struct View<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub alt: Option>, + pub alt: std::option::Option>, #[serde(borrow)] pub cid: jacquard_common::types::string::Cid<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub dimensions: Option>, + pub dimensions: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub name: Option>, + pub name: std::option::Option>, #[serde(borrow)] pub playlist: jacquard_common::types::string::Uri<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub thumbnail: Option>, + pub thumbnail: std::option::Option>, } pub mod view_state { diff --git a/crates/weaver-api/src/sh_weaver/notebook.rs b/crates/weaver-api/src/sh_weaver/notebook.rs index cd51fe3..ee8a414 100644 --- a/crates/weaver-api/src/sh_weaver/notebook.rs +++ b/crates/weaver-api/src/sh_weaver/notebook.rs @@ -30,7 +30,7 @@ pub struct AuthorListView<'a> { pub record: crate::sh_weaver::actor::ProfileDataView<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub uri: Option>, + pub uri: std::option::Option>, } pub mod author_list_view_state { @@ -874,10 +874,10 @@ pub struct BookEntryView<'a> { pub index: i64, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub next: Option>, + pub next: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub prev: Option>, + pub prev: std::option::Option>, } pub mod book_entry_view_state { @@ -1139,13 +1139,13 @@ pub struct EntryView<'a> { pub record: jacquard_common::types::value::Data<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub rendered_view: Option>, + pub rendered_view: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub tags: Option>, + pub tags: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub title: Option>, + pub title: std::option::Option>, #[serde(borrow)] pub uri: jacquard_common::types::string::AtUri<'a>, } @@ -1511,10 +1511,10 @@ pub struct NotebookView<'a> { pub record: jacquard_common::types::value::Data<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub tags: Option>, + pub tags: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub title: Option>, + pub title: std::option::Option>, #[serde(borrow)] pub uri: jacquard_common::types::string::AtUri<'a>, } @@ -1854,7 +1854,7 @@ pub type Path<'a> = jacquard_common::CowStr<'a>; pub struct RenderedView<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub css: Option>, + pub css: std::option::Option>, #[serde(borrow)] pub html: jacquard_common::types::blob::BlobRef<'a>, } diff --git a/crates/weaver-api/src/sh_weaver/notebook/authors.rs b/crates/weaver-api/src/sh_weaver/notebook/authors.rs index 5b67589..049ee65 100644 --- a/crates/weaver-api/src/sh_weaver/notebook/authors.rs +++ b/crates/weaver-api/src/sh_weaver/notebook/authors.rs @@ -19,10 +19,10 @@ #[serde(rename_all = "camelCase")] pub struct AuthorListItem<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub index: Option, + pub index: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub profile: Option>, + pub profile: std::option::Option>, } pub mod author_list_item_state { @@ -313,7 +313,7 @@ pub struct Authors<'a> { #[serde(borrow)] pub author_list: Vec>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub created_at: Option, + pub created_at: std::option::Option, } pub mod authors_state { diff --git a/crates/weaver-api/src/sh_weaver/notebook/book.rs b/crates/weaver-api/src/sh_weaver/notebook/book.rs index 2203cbf..3c836da 100644 --- a/crates/weaver-api/src/sh_weaver/notebook/book.rs +++ b/crates/weaver-api/src/sh_weaver/notebook/book.rs @@ -22,21 +22,21 @@ pub struct Book<'a> { pub authors: Vec>, /// Client-declared timestamp when this was originally created. #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub created_at: Option, + pub created_at: std::option::Option, #[serde(borrow)] pub entry_list: Vec>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub path: Option>, + pub path: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub tags: Option>, + pub tags: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub theme: Option>, + pub theme: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub title: Option>, + pub title: std::option::Option>, } pub mod book_state { diff --git a/crates/weaver-api/src/sh_weaver/notebook/chapter.rs b/crates/weaver-api/src/sh_weaver/notebook/chapter.rs index 52030b6..c2c69df 100644 --- a/crates/weaver-api/src/sh_weaver/notebook/chapter.rs +++ b/crates/weaver-api/src/sh_weaver/notebook/chapter.rs @@ -22,7 +22,7 @@ pub struct Chapter<'a> { pub authors: Vec>, /// Client-declared timestamp when this was originally created. #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub created_at: Option, + pub created_at: std::option::Option, #[serde(borrow)] pub entry_list: Vec>, /// The notebook this chapter belongs to. @@ -30,10 +30,10 @@ pub struct Chapter<'a> { pub notebook: crate::com_atproto::repo::strong_ref::StrongRef<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub tags: Option>, + pub tags: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub title: Option>, + pub title: std::option::Option>, } pub mod chapter_state { diff --git a/crates/weaver-api/src/sh_weaver/notebook/entry.rs b/crates/weaver-api/src/sh_weaver/notebook/entry.rs index 533465c..a286d87 100644 --- a/crates/weaver-api/src/sh_weaver/notebook/entry.rs +++ b/crates/weaver-api/src/sh_weaver/notebook/entry.rs @@ -26,12 +26,12 @@ pub struct Entry<'a> { /// The set of images and records, if any, embedded in the notebook entry. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub embeds: Option>, + pub embeds: std::option::Option>, #[serde(borrow)] pub path: crate::sh_weaver::notebook::Path<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub tags: Option>, + pub tags: std::option::Option>, #[serde(borrow)] pub title: crate::sh_weaver::notebook::Title<'a>, } diff --git a/crates/weaver-api/src/sh_weaver/notebook/page.rs b/crates/weaver-api/src/sh_weaver/notebook/page.rs index 3d753ab..3b4752c 100644 --- a/crates/weaver-api/src/sh_weaver/notebook/page.rs +++ b/crates/weaver-api/src/sh_weaver/notebook/page.rs @@ -20,7 +20,7 @@ pub struct Page<'a> { /// Client-declared timestamp when this was originally created. #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub created_at: Option, + pub created_at: std::option::Option, #[serde(borrow)] pub entry_list: Vec>, /// The notebook this page belongs to. @@ -28,10 +28,10 @@ pub struct Page<'a> { pub notebook: crate::com_atproto::repo::strong_ref::StrongRef<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub tags: Option>, + pub tags: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub title: Option>, + pub title: std::option::Option>, } pub mod page_state { diff --git a/crates/weaver-api/src/sh_weaver/notebook/theme.rs b/crates/weaver-api/src/sh_weaver/notebook/theme.rs index b6dd09d..eb62451 100644 --- a/crates/weaver-api/src/sh_weaver/notebook/theme.rs +++ b/crates/weaver-api/src/sh_weaver/notebook/theme.rs @@ -565,7 +565,7 @@ pub struct Theme<'a> { pub dark_scheme: crate::com_atproto::repo::strong_ref::StrongRef<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub default_theme: Option>, + pub default_theme: std::option::Option>, #[serde(borrow)] pub fonts: ThemeFonts<'a>, /// Syntax highlighting theme for light mode diff --git a/crates/weaver-api/src/tools_ozone/communication.rs b/crates/weaver-api/src/tools_ozone/communication.rs index 1c807c4..149bb7f 100644 --- a/crates/weaver-api/src/tools_ozone/communication.rs +++ b/crates/weaver-api/src/tools_ozone/communication.rs @@ -31,7 +31,7 @@ pub struct TemplateView<'a> { pub id: jacquard_common::CowStr<'a>, /// Message language. #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub lang: Option, + pub lang: std::option::Option, /// DID of the user who last updated the template. #[serde(borrow)] pub last_updated_by: jacquard_common::types::string::Did<'a>, @@ -41,7 +41,7 @@ pub struct TemplateView<'a> { /// Content of the template, can contain markdown and variable placeholders. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub subject: Option>, + pub subject: std::option::Option>, pub updated_at: jacquard_common::types::string::Datetime, } diff --git a/crates/weaver-api/src/tools_ozone/moderation.rs b/crates/weaver-api/src/tools_ozone/moderation.rs index 2b972c7..4edbf61 100644 --- a/crates/weaver-api/src/tools_ozone/moderation.rs +++ b/crates/weaver-api/src/tools_ozone/moderation.rs @@ -38,10 +38,10 @@ pub struct AccountEvent<'a> { pub active: bool, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub comment: Option>, + pub comment: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub status: Option>, + pub status: std::option::Option>, pub timestamp: jacquard_common::types::string::Datetime, } @@ -1276,6 +1276,16 @@ fn lexicon_doc_tools_ozone_moderation_defs() -> ::jacquard_lexicon::lexicon::Lex known_values: None, }), ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "isDelivered", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::Boolean(::jacquard_lexicon::lexicon::LexBoolean { + description: None, + default: None, + r#const: None, + }), + ); map.insert( ::jacquard_common::smol_str::SmolStr::new_static("policies"), ::jacquard_lexicon::lexicon::LexObjectProperty::Array(::jacquard_lexicon::lexicon::LexArray { @@ -2055,6 +2065,32 @@ fn lexicon_doc_tools_ozone_moderation_defs() -> ::jacquard_lexicon::lexicon::Lex known_values: None, }), ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "targetServices", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::Array(::jacquard_lexicon::lexicon::LexArray { + description: Some( + ::jacquard_common::CowStr::new_static( + "List of services where the takedown should be applied. If empty or not provided, takedown is applied on all configured services.", + ), + ), + items: ::jacquard_lexicon::lexicon::LexArrayItem::String(::jacquard_lexicon::lexicon::LexString { + description: None, + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + min_length: None, + max_length: None, + }), + ); map }, }), @@ -4960,21 +4996,21 @@ pub struct AgeAssuranceEvent<'a> { /// The IP address used when completing the AA flow. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub complete_ip: Option>, + pub complete_ip: std::option::Option>, /// The user agent used when completing the AA flow. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub complete_ua: Option>, + pub complete_ua: std::option::Option>, /// The date and time of this write operation. pub created_at: jacquard_common::types::string::Datetime, /// The IP address used when initiating the AA flow. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub init_ip: Option>, + pub init_ip: std::option::Option>, /// The user agent used when initiating the AA flow. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub init_ua: Option>, + pub init_ua: std::option::Option>, /// The status of the age assurance process. #[serde(borrow)] pub status: jacquard_common::CowStr<'a>, @@ -5312,12 +5348,12 @@ pub struct BlobView<'a> { pub created_at: jacquard_common::types::string::Datetime, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub details: Option>, + pub details: std::option::Option>, #[serde(borrow)] pub mime_type: jacquard_common::CowStr<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub moderation: Option>, + pub moderation: std::option::Option>, pub size: i64, } @@ -5662,16 +5698,16 @@ impl<'a> ::jacquard_lexicon::schema::LexiconSchema for CancelScheduledTakedownEv pub struct IdentityEvent<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub comment: Option>, + pub comment: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub handle: Option>, + pub handle: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub pds_host: Option>, + pub pds_host: std::option::Option>, pub timestamp: jacquard_common::types::string::Datetime, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub tombstone: Option, + pub tombstone: std::option::Option, } pub mod identity_event_state { @@ -6180,6 +6216,9 @@ pub struct ModEventEmail<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] pub content: std::option::Option>, + /// Indicates whether the email was successfully delivered to the user's inbox. + #[serde(skip_serializing_if = "std::option::Option::is_none")] + pub is_delivered: std::option::Option, /// Names/Keywords of the policies that necessitated the email. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] @@ -6278,12 +6317,12 @@ impl<'a> ::jacquard_lexicon::schema::LexiconSchema for ModEventEscalate<'a> { pub struct ModEventLabel<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub comment: Option>, + pub comment: std::option::Option>, #[serde(borrow)] pub create_label_vals: Vec>, /// Indicates how long the label will remain on the subject. Only applies on labels that are being added. #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub duration_in_hours: Option, + pub duration_in_hours: std::option::Option, #[serde(borrow)] pub negate_label_vals: Vec>, } @@ -6495,7 +6534,7 @@ impl<'a> ::jacquard_lexicon::schema::LexiconSchema for ModEventLabel<'a> { pub struct ModEventMute<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub comment: Option>, + pub comment: std::option::Option>, /// Indicates how long the subject should remain muted. pub duration_in_hours: i64, } @@ -6695,7 +6734,7 @@ impl<'a> ::jacquard_lexicon::schema::LexiconSchema for ModEventMuteReporter<'a> pub struct ModEventPriorityScore<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub comment: Option>, + pub comment: std::option::Option>, pub score: i64, } @@ -6882,10 +6921,10 @@ impl<'a> ::jacquard_lexicon::schema::LexiconSchema for ModEventPriorityScore<'a> pub struct ModEventReport<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub comment: Option>, + pub comment: std::option::Option>, /// Set to true if the reporter was muted from reporting at the time of the event. These reports won't impact the reviewState of the subject. #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub is_reporter_muted: Option, + pub is_reporter_muted: std::option::Option, #[serde(borrow)] pub report_type: crate::com_atproto::moderation::ReasonType<'a>, } @@ -7163,7 +7202,7 @@ pub struct ModEventTag<'a> { /// Additional comment about added/removed tags. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub comment: Option>, + pub comment: std::option::Option>, /// Tags to be removed to the subject. Ignores a tag If it doesn't exist, won't be duplicated. #[serde(borrow)] pub remove: Vec>, @@ -7382,6 +7421,10 @@ pub struct ModEventTakedown<'a> { /// When the strike should expire. If not provided, the strike never expires. #[serde(skip_serializing_if = "std::option::Option::is_none")] pub strike_expires_at: std::option::Option, + /// List of services where the takedown should be applied. If empty or not provided, takedown is applied on all configured services. + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub target_services: std::option::Option>>, } impl<'a> ::jacquard_lexicon::schema::LexiconSchema for ModEventTakedown<'a> { @@ -7504,20 +7547,20 @@ pub struct ModEventView<'a> { pub created_by: jacquard_common::types::string::Did<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub creator_handle: Option>, + pub creator_handle: std::option::Option>, #[serde(borrow)] pub event: ModEventViewEvent<'a>, pub id: i64, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub mod_tool: Option>, + pub mod_tool: std::option::Option>, #[serde(borrow)] pub subject: ModEventViewSubject<'a>, #[serde(borrow)] pub subject_blob_cids: Vec>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub subject_handle: Option>, + pub subject_handle: std::option::Option>, } pub mod mod_event_view_state { @@ -8033,7 +8076,7 @@ pub struct ModEventViewDetail<'a> { pub id: i64, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub mod_tool: Option>, + pub mod_tool: std::option::Option>, #[serde(borrow)] pub subject: ModEventViewDetailSubject<'a>, #[serde(borrow)] @@ -8610,10 +8653,10 @@ impl<'a> ::jacquard_lexicon::schema::LexiconSchema for ModerationDetail<'a> { pub struct RecordEvent<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub cid: Option>, + pub cid: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub comment: Option>, + pub comment: std::option::Option>, #[serde(borrow)] pub op: jacquard_common::CowStr<'a>, pub timestamp: jacquard_common::types::string::Datetime, @@ -9265,7 +9308,7 @@ pub struct RecordViewDetail<'a> { pub indexed_at: jacquard_common::types::string::Datetime, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub labels: Option>>, + pub labels: std::option::Option>>, #[serde(borrow)] pub moderation: crate::tools_ozone::moderation::ModerationDetail<'a>, #[serde(borrow)] @@ -9868,30 +9911,32 @@ impl<'a> ::jacquard_lexicon::schema::LexiconSchema for RecordsStats<'a> { #[serde(rename_all = "camelCase")] pub struct RepoView<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub deactivated_at: Option, + pub deactivated_at: std::option::Option, #[serde(borrow)] pub did: jacquard_common::types::string::Did<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub email: Option>, + pub email: std::option::Option>, #[serde(borrow)] pub handle: jacquard_common::types::string::Handle<'a>, pub indexed_at: jacquard_common::types::string::Datetime, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub invite_note: Option>, + pub invite_note: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub invited_by: Option>, + pub invited_by: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub invites_disabled: Option, + pub invites_disabled: std::option::Option, #[serde(borrow)] pub moderation: crate::tools_ozone::moderation::Moderation<'a>, #[serde(borrow)] pub related_records: Vec>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub threat_signatures: Option>>, + pub threat_signatures: std::option::Option< + Vec>, + >, } pub mod repo_view_state { @@ -10316,38 +10361,42 @@ impl<'a> ::jacquard_lexicon::schema::LexiconSchema for RepoView<'a> { #[serde(rename_all = "camelCase")] pub struct RepoViewDetail<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub deactivated_at: Option, + pub deactivated_at: std::option::Option, #[serde(borrow)] pub did: jacquard_common::types::string::Did<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub email: Option>, + pub email: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub email_confirmed_at: Option, + pub email_confirmed_at: std::option::Option< + jacquard_common::types::string::Datetime, + >, #[serde(borrow)] pub handle: jacquard_common::types::string::Handle<'a>, pub indexed_at: jacquard_common::types::string::Datetime, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub invite_note: Option>, + pub invite_note: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub invited_by: Option>, + pub invited_by: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub invites: Option>>, + pub invites: std::option::Option>>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub invites_disabled: Option, + pub invites_disabled: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub labels: Option>>, + pub labels: std::option::Option>>, #[serde(borrow)] pub moderation: crate::tools_ozone::moderation::ModerationDetail<'a>, #[serde(borrow)] pub related_records: Vec>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub threat_signatures: Option>>, + pub threat_signatures: std::option::Option< + Vec>, + >, } pub mod repo_view_detail_state { @@ -11644,37 +11693,37 @@ pub struct ScheduledActionView<'a> { /// Serialized event object that will be propagated to the event when performed #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub event_data: Option>, + pub event_data: std::option::Option>, /// Earliest time to execute the action (for randomized scheduling) #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub execute_after: Option, + pub execute_after: std::option::Option, /// Exact time to execute the action #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub execute_at: Option, + pub execute_at: std::option::Option, /// Latest time to execute the action (for randomized scheduling) #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub execute_until: Option, + pub execute_until: std::option::Option, /// ID of the moderation event created when action was successfully executed #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub execution_event_id: Option, + pub execution_event_id: std::option::Option, /// Auto-incrementing row ID pub id: i64, /// When the action was last attempted to be executed #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub last_executed_at: Option, + pub last_executed_at: std::option::Option, /// Reason for the last execution failure #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub last_failure_reason: Option>, + pub last_failure_reason: std::option::Option>, /// Whether execution time should be randomized within the specified range #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub randomize_execution: Option, + pub randomize_execution: std::option::Option, /// Current status of the scheduled action #[serde(borrow)] pub status: jacquard_common::CowStr<'a>, /// When the scheduled action was last updated #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub updated_at: Option, + pub updated_at: std::option::Option, } pub mod scheduled_action_view_state { @@ -12297,70 +12346,80 @@ pub struct SubjectStatusView<'a> { /// Statistics related to the account subject #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub account_stats: Option>, + pub account_stats: std::option::Option< + crate::tools_ozone::moderation::AccountStats<'a>, + >, /// Strike information for the account (account-level only) #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub account_strike: Option>, + pub account_strike: std::option::Option< + crate::tools_ozone::moderation::AccountStrike<'a>, + >, /// Current age assurance state of the subject. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub age_assurance_state: Option>, + pub age_assurance_state: std::option::Option>, /// Whether or not the last successful update to age assurance was made by the user or admin. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub age_assurance_updated_by: Option>, + pub age_assurance_updated_by: std::option::Option>, /// True indicates that the a previously taken moderator action was appealed against, by the author of the content. False indicates last appeal was resolved by moderators. #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub appealed: Option, + pub appealed: std::option::Option, /// Sticky comment on the subject. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub comment: Option>, + pub comment: std::option::Option>, /// Timestamp referencing the first moderation status impacting event was emitted on the subject pub created_at: jacquard_common::types::string::Datetime, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub hosting: Option>, + pub hosting: std::option::Option>, pub id: i64, /// Timestamp referencing when the author of the subject appealed a moderation action #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub last_appealed_at: Option, + pub last_appealed_at: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub last_reported_at: Option, + pub last_reported_at: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub last_reviewed_at: Option, + pub last_reviewed_at: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub last_reviewed_by: Option>, + pub last_reviewed_by: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub mute_reporting_until: Option, + pub mute_reporting_until: std::option::Option< + jacquard_common::types::string::Datetime, + >, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub mute_until: Option, + pub mute_until: std::option::Option, /// Numeric value representing the level of priority. Higher score means higher priority. #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub priority_score: Option, + pub priority_score: std::option::Option, /// Statistics related to the record subjects authored by the subject's account #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub records_stats: Option>, + pub records_stats: std::option::Option< + crate::tools_ozone::moderation::RecordsStats<'a>, + >, #[serde(borrow)] pub review_state: crate::tools_ozone::moderation::SubjectReviewState<'a>, #[serde(borrow)] pub subject: SubjectStatusViewSubject<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub subject_blob_cids: Option>>, + pub subject_blob_cids: std::option::Option< + Vec>, + >, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub subject_repo_handle: Option>, + pub subject_repo_handle: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub suspend_until: Option, + pub suspend_until: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub tags: Option>>, + pub tags: std::option::Option>>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub takendown: Option, + pub takendown: std::option::Option, /// Timestamp referencing when the last update was made to the moderation status of the subject pub updated_at: jacquard_common::types::string::Datetime, } @@ -13158,16 +13217,20 @@ impl<'a> ::jacquard_lexicon::schema::LexiconSchema for SubjectStatusView<'a> { pub struct SubjectView<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub profile: Option>, + pub profile: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub record: Option>, + pub record: std::option::Option< + crate::tools_ozone::moderation::RecordViewDetail<'a>, + >, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub repo: Option>, + pub repo: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub status: Option>, + pub status: std::option::Option< + crate::tools_ozone::moderation::SubjectStatusView<'a>, + >, #[serde(borrow)] pub subject: jacquard_common::CowStr<'a>, #[serde(borrow)] diff --git a/crates/weaver-api/src/tools_ozone/moderation/cancel_scheduled_actions.rs b/crates/weaver-api/src/tools_ozone/moderation/cancel_scheduled_actions.rs index e391e88..43a4388 100644 --- a/crates/weaver-api/src/tools_ozone/moderation/cancel_scheduled_actions.rs +++ b/crates/weaver-api/src/tools_ozone/moderation/cancel_scheduled_actions.rs @@ -440,7 +440,7 @@ pub struct FailedCancellation<'a> { pub error: jacquard_common::CowStr<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub error_code: Option>, + pub error_code: std::option::Option>, } pub mod failed_cancellation_state { @@ -637,7 +637,7 @@ pub struct CancelScheduledActions<'a> { /// Optional comment describing the reason for cancellation #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub comment: Option>, + pub comment: std::option::Option>, /// Array of DID subjects to cancel scheduled actions for #[serde(borrow)] pub subjects: Vec>, diff --git a/crates/weaver-api/src/tools_ozone/moderation/emit_event.rs b/crates/weaver-api/src/tools_ozone/moderation/emit_event.rs index 627c075..4e31137 100644 --- a/crates/weaver-api/src/tools_ozone/moderation/emit_event.rs +++ b/crates/weaver-api/src/tools_ozone/moderation/emit_event.rs @@ -24,15 +24,17 @@ pub struct EmitEvent<'a> { /// An optional external ID for the event, used to deduplicate events from external systems. Fails when an event of same type with the same external ID exists for the same subject. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub external_id: Option>, + pub external_id: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub mod_tool: Option>, + pub mod_tool: std::option::Option>, #[serde(borrow)] pub subject: EmitEventSubject<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub subject_blob_cids: Option>>, + pub subject_blob_cids: std::option::Option< + Vec>, + >, } pub mod emit_event_state { diff --git a/crates/weaver-api/src/tools_ozone/moderation/list_scheduled_actions.rs b/crates/weaver-api/src/tools_ozone/moderation/list_scheduled_actions.rs index b0432e9..95fcebe 100644 --- a/crates/weaver-api/src/tools_ozone/moderation/list_scheduled_actions.rs +++ b/crates/weaver-api/src/tools_ozone/moderation/list_scheduled_actions.rs @@ -20,23 +20,23 @@ pub struct ListScheduledActions<'a> { /// Cursor for pagination #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub cursor: Option>, + pub cursor: std::option::Option>, /// Filter actions scheduled to execute before this time #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub ends_before: Option, + pub ends_before: std::option::Option, /// Maximum number of results to return #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub limit: Option, + pub limit: std::option::Option, /// Filter actions scheduled to execute after this time #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub starts_after: Option, + pub starts_after: std::option::Option, /// Filter actions by status #[serde(borrow)] pub statuses: Vec>, /// Filter actions for specific DID subjects #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub subjects: Option>>, + pub subjects: std::option::Option>>, } pub mod list_scheduled_actions_state { diff --git a/crates/weaver-api/src/tools_ozone/moderation/schedule_action.rs b/crates/weaver-api/src/tools_ozone/moderation/schedule_action.rs index 3f7cefc..4370e3b 100644 --- a/crates/weaver-api/src/tools_ozone/moderation/schedule_action.rs +++ b/crates/weaver-api/src/tools_ozone/moderation/schedule_action.rs @@ -21,7 +21,7 @@ pub struct FailedScheduling<'a> { pub error: jacquard_common::CowStr<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub error_code: Option>, + pub error_code: std::option::Option>, #[serde(borrow)] pub subject: jacquard_common::types::string::Did<'a>, } @@ -573,6 +573,48 @@ fn lexicon_doc_tools_ozone_moderation_scheduleAction() -> ::jacquard_lexicon::le r#const: None, }), ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "emailContent", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "Email content to be sent to the user upon takedown.", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "emailSubject", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "Subject of the email to be sent to the user upon takedown.", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); map.insert( ::jacquard_common::smol_str::SmolStr::new_static("policies"), ::jacquard_lexicon::lexicon::LexObjectProperty::Array(::jacquard_lexicon::lexicon::LexArray { @@ -597,6 +639,63 @@ fn lexicon_doc_tools_ozone_moderation_scheduleAction() -> ::jacquard_lexicon::le max_length: Some(5usize), }), ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "severityLevel", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "Severity level of the violation (e.g., 'sev-0', 'sev-1', 'sev-2', etc.).", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "strikeCount", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::Integer(::jacquard_lexicon::lexicon::LexInteger { + description: None, + default: None, + minimum: None, + maximum: None, + r#enum: None, + r#const: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "strikeExpiresAt", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "When the strike should expire. If not provided, the strike never expires.", + ), + ), + format: Some( + ::jacquard_lexicon::lexicon::LexStringFormat::Datetime, + ), + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); map }, }), @@ -642,7 +741,7 @@ pub struct ScheduleAction<'a> { /// This will be propagated to the moderation event when it is applied #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub mod_tool: Option>, + pub mod_tool: std::option::Option>, #[serde(borrow)] pub scheduling: crate::tools_ozone::moderation::schedule_action::SchedulingConfig< 'a, @@ -1198,10 +1297,28 @@ pub struct Takedown<'a> { /// Indicates how long the takedown should be in effect before automatically expiring. #[serde(skip_serializing_if = "std::option::Option::is_none")] pub duration_in_hours: std::option::Option, + /// Email content to be sent to the user upon takedown. + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub email_content: std::option::Option>, + /// Subject of the email to be sent to the user upon takedown. + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub email_subject: std::option::Option>, /// Names/Keywords of the policies that drove the decision. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] pub policies: std::option::Option>>, + /// Severity level of the violation (e.g., 'sev-0', 'sev-1', 'sev-2', etc.). + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub severity_level: std::option::Option>, + /// Number of strikes to assign to the user when takedown is applied. + #[serde(skip_serializing_if = "std::option::Option::is_none")] + pub strike_count: std::option::Option, + /// When the strike should expire. If not provided, the strike never expires. + #[serde(skip_serializing_if = "std::option::Option::is_none")] + pub strike_expires_at: std::option::Option, } impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Takedown<'a> { diff --git a/crates/weaver-api/src/tools_ozone/safelink.rs b/crates/weaver-api/src/tools_ozone/safelink.rs index fcfc4e8..eeae0ca 100644 --- a/crates/weaver-api/src/tools_ozone/safelink.rs +++ b/crates/weaver-api/src/tools_ozone/safelink.rs @@ -110,7 +110,7 @@ pub struct Event<'a> { /// Optional comment about the decision #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub comment: Option>, + pub comment: std::option::Option>, pub created_at: jacquard_common::types::string::Datetime, /// DID of the user who created this rule #[serde(borrow)] @@ -1206,7 +1206,7 @@ pub struct UrlRule<'a> { /// Optional comment about the decision #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub comment: Option>, + pub comment: std::option::Option>, /// Timestamp when the rule was created pub created_at: jacquard_common::types::string::Datetime, /// DID of the user added the rule. diff --git a/crates/weaver-api/src/tools_ozone/safelink/add_rule.rs b/crates/weaver-api/src/tools_ozone/safelink/add_rule.rs index 9f2397b..168971d 100644 --- a/crates/weaver-api/src/tools_ozone/safelink/add_rule.rs +++ b/crates/weaver-api/src/tools_ozone/safelink/add_rule.rs @@ -22,11 +22,11 @@ pub struct AddRule<'a> { /// Optional comment about the decision #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub comment: Option>, + pub comment: std::option::Option>, /// Author DID. Only respected when using admin auth #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub created_by: Option>, + pub created_by: std::option::Option>, #[serde(borrow)] pub pattern: crate::tools_ozone::safelink::PatternType<'a>, #[serde(borrow)] diff --git a/crates/weaver-api/src/tools_ozone/safelink/remove_rule.rs b/crates/weaver-api/src/tools_ozone/safelink/remove_rule.rs index 85a6a1d..2fc4176 100644 --- a/crates/weaver-api/src/tools_ozone/safelink/remove_rule.rs +++ b/crates/weaver-api/src/tools_ozone/safelink/remove_rule.rs @@ -20,11 +20,11 @@ pub struct RemoveRule<'a> { /// Optional comment about why the rule is being removed #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub comment: Option>, + pub comment: std::option::Option>, /// Optional DID of the user. Only respected when using admin auth. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub created_by: Option>, + pub created_by: std::option::Option>, #[serde(borrow)] pub pattern: crate::tools_ozone::safelink::PatternType<'a>, /// The URL or domain to remove the rule for diff --git a/crates/weaver-api/src/tools_ozone/safelink/update_rule.rs b/crates/weaver-api/src/tools_ozone/safelink/update_rule.rs index 6829a0f..fd668e2 100644 --- a/crates/weaver-api/src/tools_ozone/safelink/update_rule.rs +++ b/crates/weaver-api/src/tools_ozone/safelink/update_rule.rs @@ -22,11 +22,11 @@ pub struct UpdateRule<'a> { /// Optional comment about the update #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub comment: Option>, + pub comment: std::option::Option>, /// Optional DID to credit as the creator. Only respected for admin_token authentication. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub created_by: Option>, + pub created_by: std::option::Option>, #[serde(borrow)] pub pattern: crate::tools_ozone::safelink::PatternType<'a>, #[serde(borrow)] diff --git a/crates/weaver-api/src/tools_ozone/set.rs b/crates/weaver-api/src/tools_ozone/set.rs index 695b890..d282a64 100644 --- a/crates/weaver-api/src/tools_ozone/set.rs +++ b/crates/weaver-api/src/tools_ozone/set.rs @@ -283,7 +283,7 @@ pub struct SetView<'a> { pub created_at: jacquard_common::types::string::Datetime, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub description: Option>, + pub description: std::option::Option>, #[serde(borrow)] pub name: jacquard_common::CowStr<'a>, pub set_size: i64, diff --git a/crates/weaver-api/src/tools_ozone/setting.rs b/crates/weaver-api/src/tools_ozone/setting.rs index 22849d1..87b1eed 100644 --- a/crates/weaver-api/src/tools_ozone/setting.rs +++ b/crates/weaver-api/src/tools_ozone/setting.rs @@ -22,12 +22,12 @@ pub mod upsert_option; #[serde(rename_all = "camelCase")] pub struct DefsOption<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub created_at: Option, + pub created_at: std::option::Option, #[serde(borrow)] pub created_by: jacquard_common::types::string::Did<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub description: Option>, + pub description: std::option::Option>, #[serde(borrow)] pub did: jacquard_common::types::string::Did<'a>, #[serde(borrow)] @@ -36,11 +36,11 @@ pub struct DefsOption<'a> { pub last_updated_by: jacquard_common::types::string::Did<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub manager_role: Option>, + pub manager_role: std::option::Option>, #[serde(borrow)] pub scope: jacquard_common::CowStr<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub updated_at: Option, + pub updated_at: std::option::Option, #[serde(borrow)] pub value: jacquard_common::types::value::Data<'a>, } diff --git a/crates/weaver-api/src/tools_ozone/setting/upsert_option.rs b/crates/weaver-api/src/tools_ozone/setting/upsert_option.rs index 6522603..131b34c 100644 --- a/crates/weaver-api/src/tools_ozone/setting/upsert_option.rs +++ b/crates/weaver-api/src/tools_ozone/setting/upsert_option.rs @@ -19,12 +19,12 @@ pub struct UpsertOption<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub description: Option>, + pub description: std::option::Option>, #[serde(borrow)] pub key: jacquard_common::types::string::Nsid<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub manager_role: Option>, + pub manager_role: std::option::Option>, #[serde(borrow)] pub scope: jacquard_common::CowStr<'a>, #[serde(borrow)] diff --git a/crates/weaver-api/src/tools_ozone/signature/find_related_accounts.rs b/crates/weaver-api/src/tools_ozone/signature/find_related_accounts.rs index 70169a6..002d271 100644 --- a/crates/weaver-api/src/tools_ozone/signature/find_related_accounts.rs +++ b/crates/weaver-api/src/tools_ozone/signature/find_related_accounts.rs @@ -213,7 +213,9 @@ pub struct RelatedAccount<'a> { pub account: crate::com_atproto::admin::AccountView<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub similarities: Option>>, + pub similarities: std::option::Option< + Vec>, + >, } pub mod related_account_state { diff --git a/crates/weaver-api/src/tools_ozone/team.rs b/crates/weaver-api/src/tools_ozone/team.rs index 7329f05..51545dc 100644 --- a/crates/weaver-api/src/tools_ozone/team.rs +++ b/crates/weaver-api/src/tools_ozone/team.rs @@ -23,21 +23,21 @@ pub mod update_member; #[serde(rename_all = "camelCase")] pub struct Member<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub created_at: Option, + pub created_at: std::option::Option, #[serde(borrow)] pub did: jacquard_common::types::string::Did<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub disabled: Option, + pub disabled: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub last_updated_by: Option>, + pub last_updated_by: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub profile: Option>, + pub profile: std::option::Option>, #[serde(borrow)] pub role: jacquard_common::CowStr<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub updated_at: Option, + pub updated_at: std::option::Option, } pub mod member_state { diff --git a/crates/weaver-api/src/tools_ozone/team/update_member.rs b/crates/weaver-api/src/tools_ozone/team/update_member.rs index f587669..7d01eb3 100644 --- a/crates/weaver-api/src/tools_ozone/team/update_member.rs +++ b/crates/weaver-api/src/tools_ozone/team/update_member.rs @@ -20,10 +20,10 @@ pub struct UpdateMember<'a> { #[serde(borrow)] pub did: jacquard_common::types::string::Did<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub disabled: Option, + pub disabled: std::option::Option, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub role: Option>, + pub role: std::option::Option>, } pub mod update_member_state { diff --git a/crates/weaver-api/src/tools_ozone/verification.rs b/crates/weaver-api/src/tools_ozone/verification.rs index e8d524b..c468a41 100644 --- a/crates/weaver-api/src/tools_ozone/verification.rs +++ b/crates/weaver-api/src/tools_ozone/verification.rs @@ -35,30 +35,30 @@ pub struct VerificationView<'a> { pub issuer: jacquard_common::types::string::Did<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub issuer_profile: Option>, + pub issuer_profile: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub issuer_repo: Option>, + pub issuer_repo: std::option::Option>, /// Describes the reason for revocation, also indicating that the verification is no longer valid. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub revoke_reason: Option>, + pub revoke_reason: std::option::Option>, /// Timestamp when the verification was revoked. #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub revoked_at: Option, + pub revoked_at: std::option::Option, /// The user who revoked this verification. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub revoked_by: Option>, + pub revoked_by: std::option::Option>, /// The subject of the verification. #[serde(borrow)] pub subject: jacquard_common::types::string::Did<'a>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub subject_profile: Option>, + pub subject_profile: std::option::Option>, #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub subject_repo: Option>, + pub subject_repo: std::option::Option>, /// The AT-URI of the verification record. #[serde(borrow)] pub uri: jacquard_common::types::string::AtUri<'a>, diff --git a/crates/weaver-api/src/tools_ozone/verification/grant_verifications.rs b/crates/weaver-api/src/tools_ozone/verification/grant_verifications.rs index 9c63d32..f1ea00d 100644 --- a/crates/weaver-api/src/tools_ozone/verification/grant_verifications.rs +++ b/crates/weaver-api/src/tools_ozone/verification/grant_verifications.rs @@ -618,7 +618,7 @@ impl jacquard_common::xrpc::XrpcEndpoint for GrantVerificationsRequest { pub struct VerificationInput<'a> { /// Timestamp for verification record. Defaults to current time when not specified. #[serde(skip_serializing_if = "std::option::Option::is_none")] - pub created_at: Option, + pub created_at: std::option::Option, /// Display name of the subject the verification applies to at the moment of verifying. #[serde(borrow)] pub display_name: jacquard_common::CowStr<'a>, diff --git a/crates/weaver-api/src/tools_ozone/verification/revoke_verifications.rs b/crates/weaver-api/src/tools_ozone/verification/revoke_verifications.rs index e7841d4..107233c 100644 --- a/crates/weaver-api/src/tools_ozone/verification/revoke_verifications.rs +++ b/crates/weaver-api/src/tools_ozone/verification/revoke_verifications.rs @@ -20,7 +20,7 @@ pub struct RevokeVerifications<'a> { /// Reason for revoking the verification. This is optional and can be omitted if not needed. #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] - pub revoke_reason: Option>, + pub revoke_reason: std::option::Option>, /// Array of verification record uris to revoke #[serde(borrow)] pub uris: Vec>, diff --git a/crates/weaver-app/.env-prod b/crates/weaver-app/.env-dev similarity index 59% rename from crates/weaver-app/.env-prod rename to crates/weaver-app/.env-dev index dc4c3dd..5d82bc4 100644 --- a/crates/weaver-app/.env-prod +++ b/crates/weaver-app/.env-dev @@ -1,6 +1,6 @@ -WEAVER_APP_ENV="prod" -WEAVER_APP_HOST="https://alpha.weaver.sh" -WEAVER_APP_DOMAIN="https://alpha.weaver.sh" +WEAVER_APP_ENV="dev" +WEAVER_APP_HOST="http://localhost" +WEAVER_APP_DOMAIN="" WEAVER_PORT=8080 WEAVER_APP_SCOPES="atproto transition:generic" WEAVER_CLIENT_NAME="Weaver" diff --git a/crates/weaver-app/Cargo.toml b/crates/weaver-app/Cargo.toml index a866830..b04f998 100644 --- a/crates/weaver-app/Cargo.toml +++ b/crates/weaver-app/Cargo.toml @@ -24,6 +24,7 @@ dioxus = { version = "0.7.1", features = ["router"] } weaver-common = { path = "../weaver-common" } jacquard = { workspace = true}#, features = ["streaming"] } jacquard-lexicon = { workspace = true } +jacquard-identity = { workspace = true } jacquard-axum = { workspace = true, optional = true } weaver-api = { path = "../weaver-api"}#, features = ["streaming"] } markdown-weaver = { workspace = true } @@ -41,12 +42,12 @@ base64 = "0.22" http = "1.3" reqwest = { version = "0.12", default-features = false, features = ["json"] } -dioxus-free-icons = { version = "0.10.0" } # diesel = { version = "2.3", features = ["sqlite", "returning_clauses_for_sqlite_3_35", "chrono", "serde_json"] } # diesel_migrations = { version = "2.3", features = ["sqlite"] } tokio = { version = "1.28", features = ["sync"] } serde_html_form = "0.2.8" tracing.workspace = true +serde_ipld_dagcbor = { version = "0.6" } [target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies] webbrowser = "1.0.6" diff --git a/crates/weaver-app/src/auth/mod.rs b/crates/weaver-app/src/auth/mod.rs index 67ba1b7..6d1538e 100644 --- a/crates/weaver-app/src/auth/mod.rs +++ b/crates/weaver-app/src/auth/mod.rs @@ -5,6 +5,7 @@ pub use storage::AuthStore; mod state; pub use state::AuthState; +use crate::fetch::Fetcher; #[cfg(all(feature = "fullstack-server", feature = "server"))] use dioxus::prelude::*; #[cfg(all(feature = "fullstack-server", feature = "server"))] @@ -25,13 +26,12 @@ pub async fn client_metadata() -> Result> { } #[cfg(not(target_arch = "wasm32"))] -pub async fn restore_session() -> Result<(), String> { +pub async fn restore_session(_fetcher: Fetcher) -> Result<(), String> { Ok(()) } #[cfg(target_arch = "wasm32")] -pub async fn restore_session() -> Result<(), CapturedError> { - use crate::fetch::CachedFetcher; +pub async fn restore_session(fetcher: Fetcher) -> Result<(), CapturedError> { use dioxus::prelude::*; use gloo_storage::{LocalStorage, Storage}; use jacquard::types::string::Did; @@ -59,7 +59,6 @@ pub async fn restore_session() -> Result<(), CapturedError> { let (did_str, session_id) = found_session.ok_or(CapturedError::from_display("No saved session found"))?; let did = Did::new_owned(did_str)?; - let fetcher = use_context::(); let session = fetcher .client diff --git a/crates/weaver-app/src/blobcache.rs b/crates/weaver-app/src/blobcache.rs index 1ad73a8..828bbcc 100644 --- a/crates/weaver-app/src/blobcache.rs +++ b/crates/weaver-app/src/blobcache.rs @@ -42,13 +42,37 @@ impl BlobCache { } AtIdentifier::Handle(handle) => self.client.pds_for_handle(&handle).await?, }; - let blob = self - .client - .xrpc(pds_url) - .send(&GetBlob::new().cid(cid.clone()).did(repo_did).build()) - .await? - .buffer() - .clone(); + // let blob = if let Ok(blob_stream) = self + // .client + // .xrpc(pds_url) + // .send( + // &GetBlob::new() + // .cid(cid.clone()) + // .did(repo_did.clone()) + // .build(), + // ) + // .await + // { + // blob_stream.buffer().clone() + // } else { + // reqwest::get(format!( + // "https://cdn.bsky.app/img/feed_fullsize/plain/{}/{}@jpeg", + // repo_did, cid + // )) + // .await? + // .bytes() + // .await? + // .clone() + // }; + // + let blob = reqwest::get(format!( + "https://cdn.bsky.app/img/feed_fullsize/plain/{}/{}@jpeg", + repo_did, cid + )) + .await? + .bytes() + .await? + .clone(); self.cache.insert(cid.clone(), blob); if let Some(name) = name { diff --git a/crates/weaver-app/src/components/css.rs b/crates/weaver-app/src/components/css.rs index 93d6de7..4533f71 100644 --- a/crates/weaver-app/src/components/css.rs +++ b/crates/weaver-app/src/components/css.rs @@ -35,7 +35,7 @@ pub fn NotebookCss(ident: SmolStr, notebook: SmolStr) -> Element { use weaver_renderer::css::{generate_base_css, generate_syntax_css}; use weaver_renderer::theme::{default_resolved_theme, resolve_theme}; - let fetcher = use_context::(); + let fetcher = use_context::(); let css_content = use_resource(move || { let ident = ident.clone(); @@ -87,7 +87,7 @@ pub fn NotebookCss(ident: SmolStr, notebook: SmolStr) -> Element { } #[cfg(feature = "fullstack-server")] -#[get("/css/{ident}/{notebook}", fetcher: Extension>)] +#[get("/css/{ident}/{notebook}", fetcher: Extension>)] pub async fn css(ident: SmolStr, notebook: SmolStr) -> Result { use dioxus::fullstack::http::header::CONTENT_TYPE; use jacquard::client::AgentSessionExt; diff --git a/crates/weaver-app/src/components/entry.rs b/crates/weaver-app/src/components/entry.rs index 3938585..d3f8b17 100644 --- a/crates/weaver-app/src/components/entry.rs +++ b/crates/weaver-app/src/components/entry.rs @@ -4,7 +4,7 @@ use crate::blobcache::BlobCache; use crate::{ components::avatar::{Avatar, AvatarImage}, - data::use_handle, + data::{NotebookHandle, use_handle}, }; use crate::Route; @@ -22,7 +22,18 @@ use jacquard::{ }; #[allow(unused_imports)] use std::sync::Arc; -use weaver_api::sh_weaver::notebook::{BookEntryView, entry}; +use weaver_api::sh_weaver::notebook::{BookEntryView, EntryView, entry}; + +#[component] +pub fn EntryPage( + ident: ReadSignal>, + book_title: ReadSignal, + title: ReadSignal, +) -> Element { + rsx! { + {std::iter::once(rsx! {Entry {ident, book_title, title}})} + } +} #[component] pub fn Entry( @@ -31,72 +42,54 @@ pub fn Entry( title: ReadSignal, ) -> Element { // Use feature-gated hook for SSR support - let entry = crate::data::use_entry_data(ident(), book_title(), title())?; + let entry = crate::data::use_entry_data(ident(), book_title(), title()); + let handle = use_context::(); + let fetcher = use_context::(); // Handle blob caching when entry data is available - use_effect(move || { - if let Some((_book_entry_view, entry_record)) = &*entry.read() { - if let Some(embeds) = &entry_record.embeds { - if let Some(images) = &embeds.images { - // Register blob mappings with service worker (client-side only) - #[cfg(all( - target_family = "wasm", - target_os = "unknown", - not(feature = "fullstack-server") - ))] - { - let images = images.clone(); - spawn(async move { - let fetcher = use_context::(); - let _ = crate::service_worker::register_entry_blobs( - &ident(), - book_title().as_str(), - &images, - &fetcher, - ) - .await; - }); - } - #[cfg(feature = "fullstack-server")] - { - let ident = ident(); - let images = images.clone(); - spawn(async move { - for image in &images.images { - use crate::data::cache_blob; - - let cid = image.image.blob().cid(); - cache_blob( - ident.to_smolstr(), - cid.to_smolstr(), - image.name.as_ref().map(|n| n.to_smolstr()), + if let Ok(entry) = entry { + match &*entry.read() { + Some((book_entry_view, entry_record)) => { + if let Some(embeds) = &entry_record.embeds { + if let Some(images) = &embeds.images { + // Register blob mappings with service worker (client-side only) + #[cfg(all( + target_family = "wasm", + target_os = "unknown", + not(feature = "fullstack-server") + ))] + { + let fetcher = fetcher.clone(); + let images = images.clone(); + spawn(async move { + let _ = crate::service_worker::register_entry_blobs( + &ident(), + book_title().as_str(), + &images, + &fetcher, ) - .await - .ok(); - } - }); + .await; + }); + } } } + rsx! { EntryPageView { + book_entry_view: book_entry_view.clone(), + entry_record: entry_record.clone(), + ident: handle.as_ref().unwrap().clone(), + book_title: book_title() + } } } + _ => rsx! { p { "Loading..." } }, } - }); - - match &*entry.read_unchecked() { - Some((book_entry_view, entry_record)) => { - rsx! { EntryPage { - book_entry_view: book_entry_view.clone(), - entry_record: entry_record.clone(), - ident: use_handle(ident())?(), - book_title: book_title() - } } - } - _ => rsx! { p { "Loading..." } }, + } else { + rsx! { p { "Loading..." } } } } /// Full entry page with metadata, content, and navigation #[component] -fn EntryPage( +fn EntryPageView( book_entry_view: BookEntryView<'static>, entry_record: entry::Entry<'static>, ident: AtIdentifier<'static>, @@ -166,7 +159,7 @@ pub fn EntryCard( author_count: usize, ) -> Element { use crate::Route; - use jacquard::{IntoStatic, from_data}; + use jacquard::from_data; use weaver_api::sh_weaver::notebook::entry::Entry; let entry_view = &entry.entry; @@ -176,8 +169,7 @@ pub fn EntryCard( .map(|t| t.as_ref()) .unwrap_or("Untitled"); - let ident = use_handle(entry_view.uri.authority().clone().into_static())?; - + let ident = use_context::(); // Format date let formatted_date = entry_view .indexed_at @@ -204,8 +196,8 @@ pub fn EntryCard( rsx! { div { class: "entry-card", Link { - to: Route::Entry { - ident: ident(), + to: Route::EntryPage { + ident: ident.as_ref().unwrap().clone(), book_title: book_title.clone(), title: title.to_string().into() }, @@ -236,7 +228,7 @@ pub fn EntryCard( } } span { class: "author-name", "{display_name}" } - span { class: "meta-label", "@{ident}" } + span { class: "meta-label", "@{ident.as_ref().unwrap()}" } } } ProfileDataViewInner::ProfileViewDetailed(profile) => { @@ -248,7 +240,7 @@ pub fn EntryCard( } } span { class: "author-name", "{display_name}" } - span { class: "meta-label", "@{ident}" } + span { class: "meta-label", "@{ident.as_ref().unwrap()}" } } } ProfileDataViewInner::TangledProfileView(profile) => { @@ -291,7 +283,7 @@ pub fn EntryCard( /// Metadata header showing title, authors, date, tags #[component] fn EntryMetadata( - entry_view: weaver_api::sh_weaver::notebook::EntryView<'static>, + entry_view: EntryView<'static>, ident: AtIdentifier<'static>, created_at: Datetime, ) -> Element { @@ -402,7 +394,7 @@ fn EntryMetadata( #[component] fn NavButton( direction: &'static str, - entry: weaver_api::sh_weaver::notebook::EntryView<'static>, + entry: EntryView<'static>, ident: AtIdentifier<'static>, book_title: SmolStr, ) -> Element { @@ -415,7 +407,7 @@ fn NavButton( rsx! { Link { - to: Route::Entry { + to: Route::EntryPage { ident: ident.clone(), book_title: book_title.clone(), title: entry_title.to_string().into() diff --git a/crates/weaver-app/src/components/identity.rs b/crates/weaver-app/src/components/identity.rs index b624e94..4bf4c97 100644 --- a/crates/weaver-app/src/components/identity.rs +++ b/crates/weaver-app/src/components/identity.rs @@ -8,21 +8,25 @@ const NOTEBOOK_CARD_CSS: Asset = asset!("/assets/styling/notebook-card.css"); #[component] pub fn Repository(ident: ReadSignal>) -> Element { + // Fetch notebooks for this specific DID with SSR support + let notebooks = data::use_notebooks_for_did(ident())?; + use_context_provider(|| notebooks); rsx! { - // We can create elements inside the rsx macro with the element name followed by a block of attributes and children. div { Outlet:: {} } } } +pub fn use_repo_notebook_context() +-> Option, Vec>)>>>> { + try_use_context::, Vec>)>>>>() +} + #[component] pub fn RepositoryIndex(ident: ReadSignal>) -> Element { use crate::components::ProfileDisplay; - - // Fetch notebooks for this specific DID with SSR support - let notebooks = data::use_notebooks_for_did(ident()).ok(); - + let notebooks = use_repo_notebook_context(); rsx! { document::Stylesheet { href: NOTEBOOK_CARD_CSS } @@ -35,32 +39,30 @@ pub fn RepositoryIndex(ident: ReadSignal>) -> Element { // Main content area main { class: "repository-main", div { class: "notebooks-list", - if let Some(notebooks_memo) = ¬ebooks { - match &*notebooks_memo.read_unchecked() { - Some(notebook_list) => rsx! { - for notebook in notebook_list.iter() { - { - let view = ¬ebook.0; - let entries = ¬ebook.1; - rsx! { - div { - key: "{view.cid}", - NotebookCard { - notebook: view.clone(), - entry_refs: entries.clone() - } - } - } - } - } - }, - None => rsx! { - div { "Loading notebooks..." } - } - } - } else { - div { "Loading notebooks..." } - } + if let Some(notebooks) = notebooks { + match &*notebooks.read() { + Some(Some(notebook_list)) => rsx! { + for notebook in notebook_list.iter() { + { + let view = ¬ebook.0; + let entries = ¬ebook.1; + rsx! { + div { + key: "{view.cid}", + NotebookCard { + notebook: view.clone(), + entry_refs: entries.clone() + } + } + } + } + } + }, + _ => rsx! { + div { "Loading notebooks..." } + } + } + } } } } @@ -74,7 +76,7 @@ pub fn NotebookCard( ) -> Element { use jacquard::IntoStatic; - let fetcher = use_context::(); + let fetcher = use_context::(); let title = notebook .title @@ -109,7 +111,7 @@ pub fn NotebookCard( div { class: "notebook-card-container", Link { - to: Route::Entry { + to: Route::EntryPage { ident: ident.clone(), book_title: title.to_string().into(), title: "".into() // Will redirect to first entry @@ -192,7 +194,7 @@ pub fn NotebookCard( rsx! { Link { - to: Route::Entry { + to: Route::EntryPage { ident: ident.clone(), book_title: book_title.clone(), title: entry_title.to_string().into() @@ -236,7 +238,7 @@ pub fn NotebookCard( rsx! { Link { - to: Route::Entry { + to: Route::EntryPage { ident: ident.clone(), book_title: book_title.clone(), title: entry_title.to_string().into() @@ -289,7 +291,7 @@ pub fn NotebookCard( rsx! { Link { - to: Route::Entry { + to: Route::EntryPage { ident: ident.clone(), book_title: book_title.clone(), title: entry_title.to_string().into() diff --git a/crates/weaver-app/src/components/login.rs b/crates/weaver-app/src/components/login.rs index 39d49f9..3edf5a6 100644 --- a/crates/weaver-app/src/components/login.rs +++ b/crates/weaver-app/src/components/login.rs @@ -4,58 +4,85 @@ use jacquard::oauth::client::OAuthClient; use jacquard::oauth::session::ClientData; use jacquard::{oauth::types::AuthorizeOptions, smol_str::SmolStr}; -use crate::CONFIG; +use crate::{CONFIG, Route}; use crate::{ components::{ button::{Button, ButtonVariant}, dialog::{DialogContent, DialogRoot, DialogTitle}, input::Input, }, - fetch::CachedFetcher, + fetch::Fetcher, }; -#[component] -pub fn LoginModal(open: Signal) -> Element { - let mut handle_input = use_signal(|| String::new()); - let mut error = use_signal(|| Option::::None); - let mut is_loading = use_signal(|| false); - - let mut handle_submit = move || { - let handle = handle_input.read().clone(); - if handle.is_empty() { - error.set(Some("Please enter a handle".to_string())); - return; - } - - is_loading.set(true); - error.set(None); - - let fetcher = use_context::(); +fn handle_submit( + full_route: Route, + fetcher: Fetcher, + mut error: Signal>, + mut is_loading: Signal, + handle_input: Signal, + mut open: Signal, +) { + let handle = handle_input.read().clone(); + if handle.is_empty() { + error.set(Some("Please enter a handle".to_string())); + return; + } - #[cfg(target_arch = "wasm32")] - { - use crate::Route; - use gloo_storage::Storage; - let full_route = use_route::(); - gloo_storage::LocalStorage::set("cached_route", format!("{}", full_route)).ok(); - } + is_loading.set(true); + error.set(None); - use_effect(move || { - let handle = handle.clone(); - let fetcher = fetcher.clone(); - spawn(async move { - match start_oauth_flow(handle, fetcher).await { - Ok(_) => { - open.set(false); - } - Err(e) => { - error!("Authentication failed: {}", e); - error.set(Some(format!("Authentication failed: {}", e))); - is_loading.set(false); - } + #[cfg(target_arch = "wasm32")] + { + use gloo_storage::Storage; + gloo_storage::LocalStorage::set("cached_route", format!("{}", full_route)).ok(); + spawn(async move { + match start_oauth_flow(handle, fetcher).await { + Ok(_) => { + open.set(false); + } + Err(e) => { + error!("Authentication failed: {}", e); + error.set(Some(format!("Authentication failed: {}", e))); + is_loading.set(false); } - }); + } }); + } +} + +#[component] +pub fn LoginModal(open: Signal) -> Element { + let mut handle_input = use_signal(|| String::new()); + let error = use_signal(|| Option::::None); + let is_loading = use_signal(|| false); + let full_route = use_route::(); + let fetcher = use_context::(); + let submit_route = full_route.clone(); + let submit_fetcher = fetcher.clone(); + let submit_closure1 = move || { + let submit_route = submit_route.clone(); + let submit_fetcher = submit_fetcher.clone(); + handle_submit( + submit_route, + submit_fetcher, + error, + is_loading, + handle_input, + open, + ); + }; + + let submit_closure2 = move || { + let submit_route = full_route.clone(); + let submit_fetcher = fetcher.clone(); + handle_submit( + submit_route, + submit_fetcher, + error, + is_loading, + handle_input, + open, + ); }; rsx! { @@ -76,7 +103,7 @@ pub fn LoginModal(open: Signal) -> Element { oninput: move |e: FormEvent| handle_input.set(e.value()), onkeypress: move |k: KeyboardEvent| { if k.key() == Key::Enter { - handle_submit(); + submit_closure1(); } }, placeholder: "Enter your handle", @@ -98,7 +125,7 @@ pub fn LoginModal(open: Signal) -> Element { r#type: "submit", disabled: is_loading(), onclick: move |_| { - handle_submit(); + submit_closure2(); }, if is_loading() { "Authenticating..." } else { "Sign In" } } @@ -108,7 +135,7 @@ pub fn LoginModal(open: Signal) -> Element { } } -async fn start_oauth_flow(handle: String, fetcher: CachedFetcher) -> Result<(), SmolStr> { +async fn start_oauth_flow(handle: String, fetcher: Fetcher) -> Result<(), SmolStr> { info!("Starting OAuth flow for handle: {}", handle); let client_data = ClientData { diff --git a/crates/weaver-app/src/components/mod.rs b/crates/weaver-app/src/components/mod.rs index 8807a65..1a1434b 100644 --- a/crates/weaver-app/src/components/mod.rs +++ b/crates/weaver-app/src/components/mod.rs @@ -7,7 +7,7 @@ pub use css::NotebookCss; mod entry; #[allow(unused_imports)] -pub use entry::{Entry, EntryCard, EntryMarkdown}; +pub use entry::{Entry, EntryCard, EntryMarkdown, EntryPage}; pub mod identity; #[allow(unused_imports)] @@ -22,6 +22,9 @@ pub use notebook_cover::NotebookCover; pub mod login; +pub mod record_editor; +pub mod record_view; + use dioxus::prelude::*; #[derive(PartialEq, Props, Clone)] @@ -121,7 +124,7 @@ static _IOSK_ITAL_BOLD: Asset = asset!( .with_hash_suffix(false) .into_asset_options() ); -pub mod input; -pub mod dialog; -pub mod button; pub mod accordion; +pub mod button; +pub mod dialog; +pub mod input; diff --git a/crates/weaver-app/src/components/notebook_cover.rs b/crates/weaver-app/src/components/notebook_cover.rs index bcdb348..60991f7 100644 --- a/crates/weaver-app/src/components/notebook_cover.rs +++ b/crates/weaver-app/src/components/notebook_cover.rs @@ -1,6 +1,9 @@ #![allow(non_snake_case)] -use crate::components::avatar::{Avatar, AvatarImage}; +use crate::{ + components::avatar::{Avatar, AvatarImage}, + data::NotebookHandle, +}; use dioxus::prelude::*; use weaver_api::sh_weaver::notebook::NotebookView; @@ -21,7 +24,7 @@ pub fn NotebookCover(notebook: NotebookView<'static>, title: String) -> Element h1 { class: "notebook-cover-title", "{title}" } div { "Error loading notebook details" } } - } + }; } }; @@ -89,7 +92,6 @@ fn NotebookAuthors( #[component] fn NotebookAuthor(author: weaver_api::sh_weaver::notebook::AuthorListView<'static>) -> Element { - use crate::data::use_handle; use weaver_api::sh_weaver::actor::ProfileDataViewInner; // Author already has profile data hydrated @@ -100,7 +102,6 @@ fn NotebookAuthor(author: weaver_api::sh_weaver::notebook::AuthorListView<'stati .as_ref() .map(|n| n.as_ref()) .unwrap_or("Unknown"); - let handle = use_handle(p.did.clone().into())?; rsx! { div { class: "notebook-author", @@ -111,7 +112,7 @@ fn NotebookAuthor(author: weaver_api::sh_weaver::notebook::AuthorListView<'stati } div { class: "notebook-author-info", div { class: "notebook-author-name", "{display_name}" } - div { class: "notebook-author-handle", "@{handle()}" } + div { class: "notebook-author-handle", "@{p.handle}" } } } } @@ -122,7 +123,6 @@ fn NotebookAuthor(author: weaver_api::sh_weaver::notebook::AuthorListView<'stati .as_ref() .map(|n| n.as_ref()) .unwrap_or("Unknown"); - let handle = use_handle(p.did.clone().into())?; rsx! { div { class: "notebook-author", @@ -133,7 +133,7 @@ fn NotebookAuthor(author: weaver_api::sh_weaver::notebook::AuthorListView<'stati } div { class: "notebook-author-info", div { class: "notebook-author-name", "{display_name}" } - div { class: "notebook-author-handle", "@{handle()}" } + div { class: "notebook-author-handle", "@{p.handle}" } } } } diff --git a/crates/weaver-app/src/components/profile.rs b/crates/weaver-app/src/components/profile.rs index 617fc7f..faacb3d 100644 --- a/crates/weaver-app/src/components/profile.rs +++ b/crates/weaver-app/src/components/profile.rs @@ -1,11 +1,9 @@ #![allow(non_snake_case)] -use crate::{ - components::{ - avatar::{Avatar, AvatarImage}, - BskyIcon, TangledIcon, - }, - data::use_handle, +use crate::components::{ + BskyIcon, TangledIcon, + avatar::{Avatar, AvatarImage}, + identity::use_repo_notebook_context, }; use dioxus::prelude::*; use jacquard::types::ident::AtIdentifier; @@ -14,59 +12,62 @@ use weaver_api::sh_weaver::actor::ProfileDataViewInner; const PROFILE_CSS: Asset = asset!("/assets/styling/profile.css"); #[component] -pub fn ProfileDisplay(ident: AtIdentifier<'static>) -> Element { +pub fn ProfileDisplay(ident: ReadSignal>) -> Element { // Fetch profile data - let profile = crate::data::use_profile_data(ident.clone())?; + let profile = crate::data::use_profile_data(ident()); - match profile().as_ref() { - Some(profile_view) => rsx! { - document::Stylesheet { href: PROFILE_CSS } + match &*profile?.read() { + Some(profile_view) => { + let profile_view = use_signal(|| profile_view.clone()); + rsx! { + document::Stylesheet { href: PROFILE_CSS } - div { class: "profile-display", - // Banner if present - {match &profile_view.inner { - ProfileDataViewInner::ProfileView(p) => { - if let Some(ref banner) = p.banner { - rsx! { - div { class: "profile-banner", - img { src: "{banner.as_ref()}", alt: "Profile banner" } + div { class: "profile-display", + // Banner if present + {match &profile_view.read().inner { + ProfileDataViewInner::ProfileView(p) => { + if let Some(ref banner) = p.banner { + rsx! { + div { class: "profile-banner", + img { src: "{banner.as_ref()}", alt: "Profile banner" } + } } + } else { + rsx! { } } - } else { - rsx! { } } - } - ProfileDataViewInner::ProfileViewDetailed(p) => { - if let Some(ref banner) = p.banner { - rsx! { - div { class: "profile-banner", - img { src: "{banner.as_ref()}", alt: "Profile banner" } + ProfileDataViewInner::ProfileViewDetailed(p) => { + if let Some(ref banner) = p.banner { + rsx! { + div { class: "profile-banner", + img { src: "{banner.as_ref()}", alt: "Profile banner" } + } } + } else { + rsx! { } } - } else { - rsx! { } } - } - _ => rsx! { } - }} + _ => rsx! { } + }} - div { class: "profile-content", - // Avatar and identity - ProfileIdentity { profile_view: profile_view.clone(), ident: ident.clone() } - div { - class: "profile-extras", - // Stats - ProfileStats { ident: ident.clone() } + div { class: "profile-content", + // Avatar and identity + ProfileIdentity { profile_view, ident } + div { + class: "profile-extras", + // Stats + ProfileStats { ident } - // Links - ProfileLinks { profile_view: profile_view.clone(), ident: ident.clone() } - } + // Links + ProfileLinks { profile_view, ident } + } + } } } - }, - None => rsx! { + } + _ => rsx! { div { class: "profile-display profile-loading", "Loading profile..." } @@ -76,10 +77,10 @@ pub fn ProfileDisplay(ident: AtIdentifier<'static>) -> Element { #[component] fn ProfileIdentity( - profile_view: weaver_api::sh_weaver::actor::ProfileDataView<'static>, - ident: AtIdentifier<'static>, + profile_view: ReadSignal>, + ident: ReadSignal>, ) -> Element { - match &profile_view.inner { + match &profile_view.read().inner { ProfileDataViewInner::ProfileView(profile) => { let display_name = profile .display_name @@ -121,7 +122,7 @@ fn ProfileIdentity( span { class: "profile-pronouns", " ({pronouns})" } } } - div { class: "profile-handle", "@{use_handle(ident.clone())?}" } + div { class: "profile-handle", "@{profile.handle}" } if let Some(ref location) = profile.location { div { class: "profile-location", "{location}" } @@ -155,7 +156,7 @@ fn ProfileIdentity( div { class: "profile-name-section", h1 { class: "profile-display-name", "{display_name}" } - div { class: "profile-handle", "@{use_handle(ident.clone())?}" } + div { class: "profile-handle", "@{profile.handle}" } } } @@ -192,11 +193,19 @@ fn ProfileIdentity( } #[component] -fn ProfileStats(ident: AtIdentifier<'static>) -> Element { +fn ProfileStats(ident: ReadSignal>) -> Element { // Fetch notebook count - let notebooks = crate::data::use_notebooks_for_did(ident.clone())?; + let notebooks = use_repo_notebook_context(); - let notebook_count = notebooks().as_ref().map(|n| n.len()).unwrap_or(0); + let notebook_count = if let Some(notebooks) = notebooks { + if let Some(Some(notebooks)) = &*notebooks.read() { + notebooks.len() + } else { + 0 + } + } else { + 0 + }; rsx! { div { class: "profile-stats", @@ -210,10 +219,11 @@ fn ProfileStats(ident: AtIdentifier<'static>) -> Element { #[component] fn ProfileLinks( - profile_view: weaver_api::sh_weaver::actor::ProfileDataView<'static>, - ident: AtIdentifier<'static>, + profile_view: ReadSignal>, + + ident: ReadSignal>, ) -> Element { - match &profile_view.inner { + match &profile_view.read().inner { ProfileDataViewInner::ProfileView(profile) => { rsx! { div { class: "profile-links", diff --git a/crates/weaver-app/src/components/record_editor.rs b/crates/weaver-app/src/components/record_editor.rs new file mode 100644 index 0000000..259c7b4 --- /dev/null +++ b/crates/weaver-app/src/components/record_editor.rs @@ -0,0 +1,1663 @@ +use crate::Route; +use crate::components::accordion::{Accordion, AccordionContent, AccordionItem, AccordionTrigger}; +use crate::components::dialog::{DialogContent, DialogDescription, DialogRoot, DialogTitle}; +use crate::components::record_view::{PathLabel, SchemaView, ViewMode}; +use crate::fetch::Fetcher; +use crate::record_utils::{create_array_item_default, infer_data_from_text, try_parse_as_type}; +use dioxus::prelude::{FormData, *}; +use http::StatusCode; +use humansize::format_size; +use jacquard::api::com_atproto::repo::get_record::GetRecordOutput; +use jacquard::bytes::Bytes; +use jacquard::client::AgentError; +use jacquard::{atproto, prelude::*}; +use jacquard::{ + client::AgentSessionExt, + common::{Data, IntoStatic}, + types::{aturi::AtUri, ident::AtIdentifier, string::Nsid}, +}; +use jacquard_lexicon::lexicon::LexiconDoc; +use jacquard_lexicon::validation::ValidationResult; +use mime_sniffer::MimeTypeSniffer; +use weaver_api::com_atproto::repo::{ + create_record::CreateRecord, delete_record::DeleteRecord, put_record::PutRecord, +}; +// ============================================================================ +// Pretty Editor: Component Hierarchy +// ============================================================================ + +/// Main dispatcher - routes to specific field editors based on Data type +#[component] +fn EditableDataView( + root: Signal>, + path: String, + did: String, + #[props(default)] remove_button: Option, +) -> Element { + let path_for_memo = path.clone(); + let root_read = root.read(); + + match root_read + .get_at_path(&path_for_memo) + .map(|d| d.clone().into_static()) + { + Some(Data::Object(_)) => { + rsx! { EditableObjectField { root, path: path.clone(), did, remove_button } } + } + Some(Data::Array(_)) => rsx! { EditableArrayField { root, path: path.clone(), did } }, + Some(Data::String(_)) => { + rsx! { EditableStringField { root, path: path.clone(), remove_button } } + } + Some(Data::Integer(_)) => { + rsx! { EditableIntegerField { root, path: path.clone(), remove_button } } + } + Some(Data::Boolean(_)) => { + rsx! { EditableBooleanField { root, path: path.clone(), remove_button } } + } + Some(Data::Null) => rsx! { EditableNullField { root, path: path.clone(), remove_button } }, + Some(Data::Blob(_)) => { + rsx! { EditableBlobField { root, path: path.clone(), did, remove_button } } + } + Some(Data::Bytes(_)) => { + rsx! { EditableBytesField { root, path: path.clone(), remove_button } } + } + Some(Data::CidLink(_)) => { + rsx! { EditableCidLinkField { root, path: path.clone(), remove_button } } + } + + None => rsx! { div { class: "field-error", "❌ Path not found: {path}" } }, + } +} + +// ============================================================================ +// Primitive Field Editors +// ============================================================================ + +/// String field with type preservation +#[component] +fn EditableStringField( + root: Signal>, + path: String, + #[props(default)] remove_button: Option, +) -> Element { + use jacquard::types::LexiconStringType; + + let path_for_text = path.clone(); + let path_for_type = path.clone(); + + // Get current string value + let current_text = use_memo(move || { + root.read() + .get_at_path(&path_for_text) + .and_then(|d| d.as_str()) + .map(|s| s.to_string()) + .unwrap_or_default() + }); + + // Get string type (Copy, cheap to store) + let string_type = use_memo(move || { + root.read() + .get_at_path(&path_for_type) + .and_then(|d| match d { + Data::String(s) => Some(s.string_type()), + _ => None, + }) + .unwrap_or(LexiconStringType::String) + }); + + // Local state for invalid input + let mut input_text = use_signal(|| current_text()); + let mut parse_error = use_signal(|| None::); + + // Sync input when current changes + use_effect(move || { + input_text.set(current_text()); + }); + + let path_for_mutation = path.clone(); + let handle_input = move |evt: Event| { + let new_text = evt.value(); + input_text.set(new_text.clone()); + + match try_parse_as_type(&new_text, string_type()) { + Ok(new_atproto_str) => { + parse_error.set(None); + let mut new_data = root.read().clone(); + new_data.set_at_path(&path_for_mutation, Data::String(new_atproto_str)); + root.set(new_data); + } + Err(e) => { + parse_error.set(Some(e)); + } + } + }; + + let type_label = format!("{:?}", string_type()).to_lowercase(); + let is_plain_string = string_type() == LexiconStringType::String; + + // Dynamic width based on content length + let input_width = use_memo(move || { + let len = input_text().len(); + let min_width = match string_type() { + LexiconStringType::Cid => 60, + LexiconStringType::Nsid => 40, + LexiconStringType::Did => 50, + LexiconStringType::AtUri => 50, + _ => 20, + }; + format!("{}ch", len.max(min_width)) + }); + + rsx! { + div { class: "record-field", + div { class: "field-header", + PathLabel { path: path.clone() } + if type_label != "string" { + span { class: "string-type-tag", " [{type_label}]" } + } + {remove_button} + } + if is_plain_string { + textarea { + value: "{input_text}", + oninput: handle_input, + class: if parse_error().is_some() { "invalid" } else { "" }, + rows: "1", + } + } else { + input { + r#type: "text", + value: "{input_text}", + style: "width: {input_width}", + oninput: handle_input, + class: if parse_error().is_some() { "invalid" } else { "" }, + } + } + if let Some(err) = parse_error() { + span { class: "field-error", " ❌ {err}" } + } + } + } +} + +/// Integer field with validation +#[component] +fn EditableIntegerField( + root: Signal>, + path: String, + #[props(default)] remove_button: Option, +) -> Element { + let path_for_memo = path.clone(); + let current_value = use_memo(move || { + root.read() + .get_at_path(&path_for_memo) + .and_then(|d| d.as_integer()) + .unwrap_or(0) + }); + + let mut input_text = use_signal(|| current_value().to_string()); + let mut parse_error = use_signal(|| None::); + + use_effect(move || { + input_text.set(current_value().to_string()); + }); + + let path_for_mutation = path.clone(); + + rsx! { + div { class: "record-field", + div { class: "field-header", + PathLabel { path: path.clone() } + {remove_button} + } + input { + r#type: "number", + value: "{input_text}", + oninput: move |evt| { + let text = evt.value(); + input_text.set(text.clone()); + + match text.parse::() { + Ok(num) => { + parse_error.set(None); + let mut data_edit = root.write_unchecked(); + data_edit.set_at_path(&path_for_mutation, Data::Integer(num)); + } + Err(_) => { + parse_error.set(Some("Must be a valid integer".to_string())); + } + } + } + } + if let Some(err) = parse_error() { + span { class: "field-error", " ❌ {err}" } + } + } + } +} + +/// Boolean field (toggle button) +#[component] +fn EditableBooleanField( + root: Signal>, + path: String, + #[props(default)] remove_button: Option, +) -> Element { + let path_for_memo = path.clone(); + let current_value = use_memo(move || { + root.read() + .get_at_path(&path_for_memo) + .and_then(|d| d.as_boolean()) + .unwrap_or(false) + }); + + let path_for_mutation = path.clone(); + rsx! { + div { class: "record-field", + div { class: "field-header", + PathLabel { path: path.clone() } + {remove_button} + } + button { + class: if current_value() { "boolean-toggle boolean-toggle-true" } else { "boolean-toggle boolean-toggle-false" }, + onclick: move |_| { + root.with_mut(|data| { + if let Some(target) = data.get_at_path_mut(path_for_mutation.as_str()) { + if let Some(bool_val) = target.as_boolean() { + *target = Data::Boolean(!bool_val); + } + } + }); + }, + "{current_value()}" + } + } + } +} + +/// Null field with type inference +#[component] +fn EditableNullField( + root: Signal>, + path: String, + #[props(default)] remove_button: Option, +) -> Element { + let mut input_text = use_signal(|| String::new()); + let mut parse_error = use_signal(|| None::); + + let path_for_mutation = path.clone(); + rsx! { + div { class: "record-field", + div { class: "field-header", + PathLabel { path: path.clone() } + span { class: "field-value muted", "null" } + {remove_button} + } + input { + r#type: "text", + placeholder: "Enter value (or {{}}, [], true, 123)...", + value: "{input_text}", + oninput: move |evt| { + input_text.set(evt.value()); + }, + onkeydown: move |evt| { + use dioxus::prelude::keyboard_types::Key; + if evt.key() == Key::Enter { + let text = input_text(); + match infer_data_from_text(&text) { + Ok(new_value) => { + root.with_mut(|data| { + if let Some(target) = data.get_at_path_mut(path_for_mutation.as_str()) { + *target = new_value; + } + }); + input_text.set(String::new()); + parse_error.set(None); + } + Err(e) => { + parse_error.set(Some(e)); + } + } + } + } + } + if let Some(err) = parse_error() { + span { class: "field-error", " ❌ {err}" } + } + } + } +} + +/// Blob field - shows CID, size (editable), mime type (read-only), file upload +#[component] +fn EditableBlobField( + root: Signal>, + path: String, + did: String, + #[props(default)] remove_button: Option, +) -> Element { + let path_for_memo = path.clone(); + let blob_data = use_memo(move || { + root.read() + .get_at_path(&path_for_memo) + .and_then(|d| match d { + Data::Blob(blob) => Some(( + blob.r#ref.to_string(), + blob.size, + blob.mime_type.as_str().to_string(), + )), + _ => None, + }) + }); + + let mut cid_input = use_signal(|| String::new()); + let mut size_input = use_signal(|| String::new()); + let mut cid_error = use_signal(|| None::); + let mut size_error = use_signal(|| None::); + let mut uploading = use_signal(|| false); + let mut upload_error = use_signal(|| None::); + let mut preview_data_url = use_signal(|| None::); + + // Sync inputs when blob data changes + use_effect(move || { + if let Some((cid, size, _)) = blob_data() { + cid_input.set(cid); + size_input.set(size.to_string()); + } + }); + + let fetcher = use_context::(); + let path_for_upload = path.clone(); + let handle_file = move |evt: Event| { + let fetcher = fetcher.clone(); + let path_upload_clone = path_for_upload.clone(); + spawn(async move { + uploading.set(true); + upload_error.set(None); + + let files = evt.files(); + for file_data in files { + match file_data.read_bytes().await { + Ok(bytes_data) => { + // Convert to jacquard Bytes and sniff MIME type + let bytes = Bytes::from(bytes_data.to_vec()); + let mime_str = bytes + .sniff_mime_type() + .unwrap_or("application/octet-stream"); + let mime_type = jacquard::types::blob::MimeType::new_owned(mime_str); + + // Create data URL for immediate preview if it's an image + if mime_str.starts_with("image/") { + let base64_data = base64::Engine::encode( + &base64::engine::general_purpose::STANDARD, + &bytes, + ); + let data_url = format!("data:{};base64,{}", mime_str, base64_data); + preview_data_url.set(Some(data_url.clone())); + + // Try to decode dimensions and populate aspectRatio field + #[cfg(target_arch = "wasm32")] + { + let path_clone = path_upload_clone.clone(); + spawn(async move { + if let Some((width, height)) = + decode_image_dimensions(&data_url).await + { + populate_aspect_ratio( + root, + &path_clone, + width as i64, + height as i64, + ); + } + }); + } + } + + // Upload blob + let client = fetcher.get_client(); + match client.upload_blob(bytes, mime_type).await { + Ok(new_blob) => { + // Update blob in record + let path_ref = path_upload_clone.clone(); + root.with_mut(|record_data| { + if let Some(Data::Blob(blob)) = + record_data.get_at_path_mut(&path_ref) + { + *blob = new_blob; + } + }); + upload_error.set(None); + } + Err(e) => { + upload_error.set(Some(format!("Upload failed: {:?}", e))); + } + } + } + Err(e) => { + upload_error.set(Some(format!("Failed to read file: {}", e))); + } + } + } + + uploading.set(false); + }); + }; + + let path_for_cid = path.clone(); + let handle_cid_change = move |evt: Event| { + let text = evt.value(); + cid_input.set(text.clone()); + + match jacquard::types::cid::CidLink::new_owned(text.as_bytes()) { + Ok(new_cid_link) => { + cid_error.set(None); + root.with_mut(|data| { + if let Some(Data::Blob(blob)) = data.get_at_path_mut(&path_for_cid) { + blob.r#ref = new_cid_link; + } + }); + } + Err(_) => { + cid_error.set(Some("Invalid CID format".to_string())); + } + } + }; + + let path_for_size = path.clone(); + let handle_size_change = move |evt: Event| { + let text = evt.value(); + size_input.set(text.clone()); + + match text.parse::() { + Ok(new_size) => { + size_input.set(format_size(new_size, humansize::BINARY)); + size_error.set(None); + root.with_mut(|data| { + if let Some(Data::Blob(blob)) = data.get_at_path_mut(&path_for_size) { + blob.size = new_size; + } + }); + } + Err(_) => { + size_error.set(Some("Must be a non-negative integer".to_string())); + } + } + }; + + let placeholder_cid = "bafkreiaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + let is_placeholder = blob_data() + .map(|(cid, _, _)| cid == placeholder_cid) + .unwrap_or(true); + let is_image = blob_data() + .map(|(_, _, mime)| mime.starts_with("image/")) + .unwrap_or(false); + + // Use preview data URL if available (fresh upload), otherwise CDN + let image_url = if let Some(data_url) = preview_data_url() { + Some(data_url) + } else if !is_placeholder && is_image { + blob_data().map(|(cid, _, mime)| { + let format = mime.strip_prefix("image/").unwrap_or("jpeg"); + format!( + "https://cdn.bsky.app/img/feed_fullsize/plain/{}/{}@{}", + did, cid, format + ) + }) + } else { + None + }; + + rsx! { + div { class: "record-field blob-field", + div { class: "field-header", + PathLabel { path: path.clone() } + span { class: "string-type-tag", " [blob]" } + {remove_button} + } + div { class: "blob-fields", + div { class: "blob-field-row blob-field-cid", + label { "CID:" } + input { + r#type: "text", + value: "{cid_input}", + oninput: handle_cid_change, + class: if cid_error().is_some() { "invalid" } else { "" }, + } + if let Some(err) = cid_error() { + span { class: "field-error", " ❌ {err}" } + } + } + div { class: "blob-field-row", + label { "Size:" } + input { + r#type: "number", + value: "{size_input}", + oninput: handle_size_change, + class: if size_error().is_some() { "invalid" } else { "" }, + } + if let Some(err) = size_error() { + span { class: "field-error", " ❌ {err}" } + } + } + div { class: "blob-field-row", + label { "MIME Type:" } + span { class: "readonly", + "{blob_data().map(|(_, _, mime)| mime).unwrap_or_default()}" + } + } + if let Some(url) = image_url { + img { + src: "{url}", + alt: "Blob preview", + class: "blob-image", + } + } + div { class: "blob-upload-section", + input { + r#type: "file", + accept: if is_image { "image/*" } else { "*/*" }, + onchange: handle_file, + disabled: uploading(), + } + if uploading() { + span { class: "upload-status", "Uploading..." } + } + if let Some(err) = upload_error() { + div { class: "field-error", "❌ {err}" } + } + } + } + } + } +} + +/// Decode image dimensions from data URL using browser Image API +#[cfg(target_arch = "wasm32")] +async fn decode_image_dimensions(data_url: &str) -> Option<(u32, u32)> { + use wasm_bindgen::JsCast; + use wasm_bindgen::prelude::*; + use wasm_bindgen_futures::JsFuture; + + let window = web_sys::window()?; + let document = window.document()?; + + let img = document.create_element("img").ok()?; + let img = img.dyn_into::().ok()?; + + img.set_src(data_url); + + // Wait for image to load + let promise = js_sys::Promise::new(&mut |resolve, _reject| { + let onload = Closure::wrap(Box::new(move || { + resolve.call0(&JsValue::NULL).ok(); + }) as Box); + + img.set_onload(Some(onload.as_ref().unchecked_ref())); + onload.forget(); + }); + + JsFuture::from(promise).await.ok()?; + + Some((img.natural_width(), img.natural_height())) +} + +/// Find and populate aspectRatio field for a blob +#[allow(unused)] +fn populate_aspect_ratio( + mut root: Signal>, + blob_path: &str, + width: i64, + height: i64, +) { + // Query for all aspectRatio fields and collect the path we want + let aspect_path_to_update = { + let data = root.read(); + let query_result = data.query("...aspectRatio"); + + query_result.multiple().and_then(|matches| { + // Find aspectRatio that's a sibling of our blob + // e.g. blob at "embed.images[0].image" -> look for "embed.images[0].aspectRatio" + let blob_parent = blob_path.rsplit_once('.').map(|(parent, _)| parent); + matches.iter().find_map(|query_match| { + let aspect_parent = query_match.path.rsplit_once('.').map(|(parent, _)| parent); + + // Check if they share the same parent + if blob_parent == aspect_parent { + Some(query_match.path.clone()) + } else { + None + } + }) + }) + }; + + // Update the aspectRatio if we found a matching field + if let Some(aspect_path) = aspect_path_to_update { + let aspect_obj = atproto! {{ + "width": width, + "height": height + }}; + + root.with_mut(|record_data| { + record_data.set_at_path(&aspect_path, aspect_obj); + }); + } +} + +/// Bytes field with hex/base64 auto-detection +#[component] +fn EditableBytesField( + root: Signal>, + path: String, + #[props(default)] remove_button: Option, +) -> Element { + let path_for_memo = path.clone(); + let current_bytes = use_memo(move || { + root.read() + .get_at_path(&path_for_memo) + .and_then(|d| match d { + Data::Bytes(b) => Some(bytes_to_hex(b)), + _ => None, + }) + }); + + let mut input_text = use_signal(|| String::new()); + let mut parse_error = use_signal(|| None::); + let mut detected_format = use_signal(|| None::); + + // Sync input when bytes change + use_effect(move || { + if let Some(hex) = current_bytes() { + input_text.set(hex); + } + }); + + let path_for_mutation = path.clone(); + let handle_input = move |evt: Event| { + let text = evt.value(); + input_text.set(text.clone()); + + match parse_bytes_input(&text) { + Ok((bytes, format)) => { + parse_error.set(None); + detected_format.set(Some(format)); + root.with_mut(|data| { + if let Some(target) = data.get_at_path_mut(&path_for_mutation) { + *target = Data::Bytes(bytes); + } + }); + } + Err(e) => { + parse_error.set(Some(e)); + detected_format.set(None); + } + } + }; + + let byte_count = current_bytes() + .map(|hex| hex.chars().filter(|c| c.is_ascii_hexdigit()).count() / 2) + .unwrap_or(0); + let size_label = if byte_count > 128 { + format_size(byte_count, humansize::BINARY) + } else { + format!("{} bytes", byte_count) + }; + + rsx! { + div { class: "record-field bytes-field", + div { class: "field-header", + PathLabel { path: path.clone() } + span { class: "string-type-tag", " [bytes: {size_label}]" } + if let Some(format) = detected_format() { + span { class: "bytes-format-tag", " ({format})" } + } + {remove_button} + } + textarea { + value: "{input_text}", + placeholder: "Paste hex (1a2b3c...) or base64 (YWJj...)", + oninput: handle_input, + class: if parse_error().is_some() { "invalid" } else { "" }, + rows: "3", + } + if let Some(err) = parse_error() { + span { class: "field-error", " ❌ {err}" } + } + } + } +} + +/// Parse bytes from hex or base64, auto-detecting format +fn parse_bytes_input(text: &str) -> Result<(Bytes, String), String> { + let trimmed = text.trim(); + if trimmed.is_empty() { + return Err("Input is empty".to_string()); + } + + // Remove common whitespace/separators + let cleaned: String = trimmed + .chars() + .filter(|c| !c.is_whitespace() && *c != ':' && *c != '-') + .collect(); + + // Try hex first (more restrictive) + if cleaned.chars().all(|c| c.is_ascii_hexdigit()) { + parse_hex_bytes(&cleaned).map(|b| (b, "hex".to_string())) + } else { + // Try base64 + parse_base64_bytes(&cleaned).map(|b| (b, "base64".to_string())) + } +} + +/// Parse hex string to bytes +fn parse_hex_bytes(hex: &str) -> Result { + if hex.len() % 2 != 0 { + return Err("Hex string must have even length".to_string()); + } + + let mut bytes = Vec::with_capacity(hex.len() / 2); + for chunk in hex.as_bytes().chunks(2) { + let hex_byte = std::str::from_utf8(chunk).map_err(|e| format!("Invalid UTF-8: {}", e))?; + let byte = + u8::from_str_radix(hex_byte, 16).map_err(|e| format!("Invalid hex digit: {}", e))?; + bytes.push(byte); + } + + Ok(Bytes::from(bytes)) +} + +/// Parse base64 string to bytes +fn parse_base64_bytes(b64: &str) -> Result { + use base64::Engine; + let engine = base64::engine::general_purpose::STANDARD; + + engine + .decode(b64) + .map(Bytes::from) + .map_err(|e| format!("Invalid base64: {}", e)) +} + +/// Convert bytes to hex display string (with spacing every 4 chars) +fn bytes_to_hex(bytes: &Bytes) -> String { + bytes + .iter() + .enumerate() + .map(|(i, b)| { + let hex = format!("{:02x}", b); + if i > 0 && i % 2 == 0 { + format!(" {}", hex) + } else { + hex + } + }) + .collect() +} + +/// CidLink field with validation +#[component] +fn EditableCidLinkField( + root: Signal>, + path: String, + #[props(default)] remove_button: Option, +) -> Element { + let path_for_memo = path.clone(); + let current_cid = use_memo(move || { + root.read() + .get_at_path(&path_for_memo) + .map(|d| match d { + Data::CidLink(cid) => cid.to_string(), + _ => String::new(), + }) + .unwrap_or_default() + }); + + let mut input_text = use_signal(|| String::new()); + let mut parse_error = use_signal(|| None::); + + use_effect(move || { + input_text.set(current_cid()); + }); + + let input_width = use_memo(move || { + let len = input_text().len(); + format!("{}ch", len.max(60)) + }); + + let path_for_mutation = path.clone(); + let handle_input = move |evt: Event| { + let text = evt.value(); + input_text.set(text.clone()); + + match jacquard::types::cid::Cid::new_owned(text.as_bytes()) { + Ok(new_cid) => { + parse_error.set(None); + root.with_mut(|data| { + if let Some(target) = data.get_at_path_mut(&path_for_mutation) { + *target = Data::CidLink(new_cid); + } + }); + } + Err(_) => { + parse_error.set(Some("Invalid CID format".to_string())); + } + } + }; + + rsx! { + div { class: "record-field cidlink-field", + div { class: "field-header", + PathLabel { path: path.clone() } + span { class: "string-type-tag", " [cid-link]" } + {remove_button} + } + input { + r#type: "text", + value: "{input_text}", + style: "width: {input_width}", + placeholder: "bafyrei...", + oninput: handle_input, + class: if parse_error().is_some() { "invalid" } else { "" }, + } + if let Some(err) = parse_error() { + span { class: "field-error", " ❌ {err}" } + } + } + } +} + +// ============================================================================ +// Field with Remove Button Wrapper +// ============================================================================ + +/// Wraps a field with an optional remove button in the header +#[component] +fn FieldWithRemove( + root: Signal>, + path: String, + did: String, + is_removable: bool, + parent_path: String, + field_key: String, +) -> Element { + let remove_button = if is_removable { + Some(rsx! { + button { + class: "field-remove-button", + onclick: move |_| { + let mut new_data = root.read().clone(); + if let Some(Data::Object(obj)) = new_data.get_at_path_mut(parent_path.as_str()) { + obj.0.remove(field_key.as_str()); + } + root.set(new_data); + }, + "Remove" + } + }) + } else { + None + }; + + rsx! { + EditableDataView { + root: root, + path: path.clone(), + did: did.clone(), + remove_button: remove_button, + } + } +} + +// ============================================================================ +// Array Field Editor (enables recursion) +// ============================================================================ + +/// Array field - iterates items and renders child EditableDataView for each +#[component] +fn EditableArrayField(root: Signal>, path: String, did: String) -> Element { + let path_for_memo = path.clone(); + let array_len = use_memo(move || { + root.read() + .get_at_path(&path_for_memo) + .and_then(|d| d.as_array()) + .map(|arr| arr.0.len()) + .unwrap_or(0) + }); + + let path_for_add = path.clone(); + + rsx! { + div { class: "record-section array-section", + Accordion { + id: "edit-array-{path}", + collapsible: true, + AccordionItem { + default_open: true, + index: 0, + AccordionTrigger { + div { class: "section-header", + div { class: "section-label", + { + let parts: Vec<&str> = path.split('.').collect(); + let final_part = parts.last().unwrap_or(&""); + rsx! { "{final_part}" } + } + } + span { class: "array-length", "[{array_len}]" } + } + } + AccordionContent { + div { class: "section-content", + for idx in 0..array_len() { + { + let item_path = format!("{}[{}]", path, idx); + let path_for_remove = path.clone(); + + rsx! { + div { + class: "array-item", + key: "{item_path}", + + EditableDataView { + root: root, + path: item_path.clone(), + did: did.clone(), + remove_button: rsx! { + button { + class: "field-remove-button", + onclick: move |_| { + root.with_mut(|data| { + if let Some(Data::Array(arr)) = data.get_at_path_mut(&path_for_remove) { + arr.0.remove(idx); + } + }); + }, + "Remove" + } + } + } + } + } + } + } + div { + class: "array-item", + div { + class: "add-field-widget", + button { + onclick: move |_| { + root.with_mut(|data| { + if let Some(Data::Array(arr)) = data.get_at_path_mut(&path_for_add) { + let new_item = create_array_item_default(arr); + arr.0.push(new_item); + } + }); + }, + "+ Add Item" + } + } + } + } + } + } + } + } + } +} + +// ============================================================================ +// Object Field Editor (enables recursion) +// ============================================================================ + +/// Object field - iterates fields and renders child EditableDataView for each +#[component] +fn EditableObjectField( + root: Signal>, + path: String, + did: String, + #[props(default)] remove_button: Option, +) -> Element { + let path_for_memo = path.clone(); + let field_keys = use_memo(move || { + root.read() + .get_at_path(&path_for_memo) + .and_then(|d| d.as_object()) + .map(|obj| obj.0.keys().cloned().collect::>()) + .unwrap_or_default() + }); + + let is_root = path.is_empty(); + + rsx! { + if !is_root { + div { class: "record-section object-section", + Accordion { + id: "edit-object-{path}", + collapsible: true, + AccordionItem { + default_open: true, + index: 0, + AccordionTrigger { + div { class: "section-header", + div { class: "section-label", + { + let parts: Vec<&str> = path.split('.').collect(); + let final_part = parts.last().unwrap_or(&""); + rsx! { "{final_part}" } + } + } + {remove_button} + } + } + AccordionContent { + div { class: "section-content", + for key in field_keys() { + { + let field_path = if path.is_empty() { + key.to_string() + } else { + format!("{}.{}", path, key) + }; + let is_type_field = key == "$type"; + + rsx! { + FieldWithRemove { + key: "{field_path}", + root: root, + path: field_path.clone(), + did: did.clone(), + is_removable: !is_type_field, + parent_path: path.clone(), + field_key: key.clone(), + } + } + } + } + + AddFieldWidget { root: root, path: path.clone() } + } + } + } + } + } + } else { + for key in field_keys() { + { + let field_path = key.to_string(); + let is_type_field = key == "$type"; + + rsx! { + FieldWithRemove { + key: "{field_path}", + root: root, + path: field_path.clone(), + did: did.clone(), + is_removable: !is_type_field, + parent_path: path.clone(), + field_key: key.clone(), + } + } + } + } + + AddFieldWidget { root: root, path: path.clone() } + } + } +} + +/// Widget for adding new fields to objects +#[component] +fn AddFieldWidget(root: Signal>, path: String) -> Element { + let mut field_name = use_signal(|| String::new()); + let mut field_value = use_signal(|| String::new()); + let mut error = use_signal(|| None::); + let mut show_form = use_signal(|| false); + + let path_for_enter = path.clone(); + let path_for_button = path.clone(); + + rsx! { + div { class: "add-field-widget", + if !show_form() { + button { + class: "add-button", + onclick: move |_| show_form.set(true), + "+ Add Field" + } + } else { + div { class: "add-field-form", + input { + r#type: "text", + placeholder: "Field name", + value: "{field_name}", + oninput: move |evt| field_name.set(evt.value()), + } + input { + r#type: "text", + placeholder: r#"Value: {{}}, [], true, 123, "text""#, + value: "{field_value}", + oninput: move |evt| field_value.set(evt.value()), + onkeydown: move |evt| { + use dioxus::prelude::keyboard_types::Key; + if evt.key() == Key::Enter { + let name = field_name(); + let value_text = field_value(); + + if name.is_empty() { + error.set(Some("Field name required".to_string())); + return; + } + + let new_value = match infer_data_from_text(&value_text) { + Ok(data) => data, + Err(e) => { + error.set(Some(e)); + return; + } + }; + + let mut new_data = root.read().clone(); + if let Some(Data::Object(obj)) = new_data.get_at_path_mut(path_for_enter.as_str()) { + obj.0.insert(name.into(), new_value); + } + root.set(new_data); + + // Reset form + field_name.set(String::new()); + field_value.set(String::new()); + show_form.set(false); + error.set(None); + } + } + } + button { + class: "add-field-widget-edit", + onclick: move |_| { + let name = field_name(); + let value_text = field_value(); + + if name.is_empty() { + error.set(Some("Field name required".to_string())); + return; + } + + let new_value = match infer_data_from_text(&value_text) { + Ok(data) => data, + Err(e) => { + error.set(Some(e)); + return; + } + }; + + let mut new_data = root.read().clone(); + if let Some(Data::Object(obj)) = new_data.get_at_path_mut(path_for_button.as_str()) { + obj.0.insert(name.into(), new_value); + } + root.set(new_data); + + // Reset form + field_name.set(String::new()); + field_value.set(String::new()); + show_form.set(false); + error.set(None); + }, + "Add" + } + button { + class: "add-field-widget-edit", + onclick: move |_| { + show_form.set(false); + field_name.set(String::new()); + field_value.set(String::new()); + error.set(None); + }, + "Cancel" + } + if let Some(err) = error() { + div { class: "field-error", "❌ {err}" } + } + } + } + } + } +} + +#[component] +pub fn EditableRecordContent( + record_value: Data<'static>, + uri: ReadSignal>, + view_mode: Signal, + edit_mode: Signal, + record_resource: Resource, AgentError>>, + schema: ReadSignal>>, +) -> Element { + let mut edit_data = use_signal(use_reactive!(|record_value| record_value.clone())); + let nsid = use_memo(move || edit_data().type_discriminator().map(|s| s.to_string())); + let navigator = use_navigator(); + let fetcher = use_context::(); + + // Validate edit_data whenever it changes and provide via context + let mut validation_result = use_signal(|| None); + use_effect(move || { + let _ = schema(); // Track schema changes + if let Some(nsid_str) = nsid() { + let data = edit_data(); + let validator = jacquard_lexicon::validation::SchemaValidator::global(); + let result = validator.validate_by_nsid(&nsid_str, &data); + validation_result.set(Some(result)); + } + }); + use_context_provider(|| validation_result); + + let update_fetcher = fetcher.clone(); + let create_fetcher = fetcher.clone(); + let replace_fetcher = fetcher.clone(); + let delete_fetcher = fetcher.clone(); + + rsx! { + div { + class: "tab-bar", + button { + class: if view_mode() == ViewMode::Pretty { "tab-button active" } else { "tab-button" }, + onclick: move |_| view_mode.set(ViewMode::Pretty), + "View" + } + button { + class: if view_mode() == ViewMode::Json { "tab-button active" } else { "tab-button" }, + onclick: move |_| view_mode.set(ViewMode::Json), + "JSON" + } + button { + class: if view_mode() == ViewMode::Schema { "tab-button active" } else { "tab-button" }, + onclick: move |_| view_mode.set(ViewMode::Schema), + "Schema" + } + ActionButtons { + on_update: move |_| { + let fetcher = update_fetcher.clone(); + let uri = uri(); + let data = edit_data(); + spawn(async move { + if let Some((did, _)) = fetcher.session_info().await { + if let (Some(collection_str), Some(rkey)) = (uri.collection(), uri.rkey()) { + let collection = Nsid::new(collection_str.as_str()).ok(); + if let Some(collection) = collection { + let request = PutRecord::new() + .repo(AtIdentifier::Did(did)) + .collection(collection) + .rkey(rkey.clone()) + .record(data.clone()) + .build(); + + match fetcher.send(request).await { + Ok(output) => { + if output.status() == StatusCode::OK.as_u16() { + tracing::info!("Record updated successfully"); + edit_data.set(data.clone()); + edit_mode.set(false); + } else { + tracing::error!("Unexpected status code: {:?}", output.status()); + } + } + Err(e) => { + tracing::error!("Failed to update record: {:?}", e); + } + } + } + } + } + }); + }, + on_save_new: move |_| { + let fetcher = create_fetcher.clone(); + let data = edit_data(); + let nav = navigator.clone(); + spawn(async move { + if let Some((did, _)) = fetcher.session_info().await { + if let Some(collection_str) = data.type_discriminator() { + let collection = Nsid::new(collection_str).ok(); + if let Some(collection) = collection { + let request = CreateRecord::new() + .repo(AtIdentifier::Did(did)) + .collection(collection) + .record(data.clone()) + .build(); + + match fetcher.send(request).await { + Ok(response) => { + if let Ok(output) = response.into_output() { + tracing::info!("Record created: {}", output.uri); + let link = format!("{}/record/{}", crate::env::WEAVER_APP_DOMAIN, output.uri); + nav.push(link); + } + } + Err(e) => { + tracing::error!("Failed to create record: {:?}", e); + } + } + } + } + } + }); + }, + on_replace: move |_| { + let fetcher = replace_fetcher.clone(); + let uri = uri(); + let data = edit_data(); + let nav = navigator.clone(); + spawn(async move { + if let Some((did, _)) = fetcher.session_info().await { + if let Some(new_collection_str) = data.type_discriminator() { + let new_collection = Nsid::new(new_collection_str).ok(); + if let Some(new_collection) = new_collection { + // Create new record + let create_req = CreateRecord::new() + .repo(AtIdentifier::Did(did.clone())) + .collection(new_collection) + .record(data.clone()) + .build(); + + match fetcher.send(create_req).await { + Ok(response) => { + if let Ok(create_output) = response.into_output() { + // Delete old record + if let (Some(old_collection_str), Some(old_rkey)) = (uri.collection(), uri.rkey()) { + let old_collection = Nsid::new(old_collection_str.as_str()).ok(); + if let Some(old_collection) = old_collection { + let delete_req = DeleteRecord::new() + .repo(AtIdentifier::Did(did)) + .collection(old_collection) + .rkey(old_rkey.clone()) + .build(); + + if let Err(e) = fetcher.send(delete_req).await { + tracing::warn!("Created new record but failed to delete old: {:?}", e); + } + } + } + + tracing::info!("Record replaced: {}", create_output.uri); + let link = format!("{}/record/{}", crate::env::WEAVER_APP_DOMAIN, create_output.uri); + nav.push(link); + } + } + Err(e) => { + tracing::error!("Failed to replace record: {:?}", e); + } + } + } + } + } + }); + }, + on_delete: move |_| { + let fetcher = delete_fetcher.clone(); + let uri = uri(); + let nav = navigator.clone(); + spawn(async move { + if let Some((did, _)) = fetcher.session_info().await { + if let (Some(collection_str), Some(rkey)) = (uri.collection(), uri.rkey()) { + let collection = Nsid::new(collection_str.as_str()).ok(); + if let Some(collection) = collection { + let request = DeleteRecord::new() + .repo(AtIdentifier::Did(did)) + .collection(collection) + .rkey(rkey.clone()) + .build(); + + match fetcher.send(request).await { + Ok(_) => { + tracing::info!("Record deleted"); + nav.push(Route::Home {}); + } + Err(e) => { + tracing::error!("Failed to delete record: {:?}", e); + } + } + } + } + } + }); + }, + on_cancel: move |_| { + edit_data.set(record_value.clone()); + edit_mode.set(false); + }, + } + } + div { + class: "tab-content", + match view_mode() { + ViewMode::Pretty => rsx! { + div { class: "pretty-record", + EditableDataView { + root: edit_data, + path: String::new(), + did: uri().authority().to_string(), + } + } + }, + ViewMode::Json => rsx! { + JsonEditor { data: edit_data, nsid, schema } + }, + ViewMode::Schema => rsx! { + SchemaView { schema } + }, + } + } + } +} + +#[component] +pub fn JsonEditor( + data: Signal>, + nsid: ReadSignal>, + schema: ReadSignal>>, +) -> Element { + let mut json_text = + use_signal(|| serde_json::to_string_pretty(&*data.read()).unwrap_or_default()); + + let height = use_memo(move || { + let line_count = json_text().lines().count(); + let min_lines = 10; + let lines = line_count.max(min_lines); + // line-height is 1.5, font-size is 0.9rem (approx 14.4px), so each line is ~21.6px + // Add padding (1rem top + 1rem bottom = 2rem = 32px) + format!("{}px", lines * 22 + 32) + }); + + let validation = use_resource(move || { + let text = json_text(); + let nsid_val = nsid(); + let _ = schema(); // Track schema changes + + async move { + // Only validate if we have an NSID + let nsid_str = nsid_val?; + + // Parse JSON to Data + let parsed = match serde_json::from_str::(&text) { + Ok(val) => val.into_static(), + Err(e) => { + return Some((None, Some(e.to_string()))); + } + }; + + // Use global validator (schema already registered) + let validator = jacquard_lexicon::validation::SchemaValidator::global(); + let result = validator.validate_by_nsid(&nsid_str, &parsed); + + Some((Some(result), None)) + } + }); + + rsx! { + div { class: "json-editor", + textarea { + class: "json-textarea", + style: "height: {height};", + value: "{json_text}", + oninput: move |evt| { + json_text.set(evt.value()); + // Update data signal on successful parse + if let Ok(parsed) = serde_json::from_str::(&evt.value()) { + data.set(parsed.into_static()); + } + }, + } + + ValidationPanel { + validation: validation, + } + } + } +} + +#[component] +pub fn ActionButtons( + on_update: EventHandler<()>, + on_save_new: EventHandler<()>, + on_replace: EventHandler<()>, + on_delete: EventHandler<()>, + on_cancel: EventHandler<()>, +) -> Element { + let mut show_save_dropdown = use_signal(|| false); + let mut show_replace_warning = use_signal(|| false); + let mut show_delete_confirm = use_signal(|| false); + + rsx! { + div { class: "action-buttons-group", + button { + class: "tab-button action-button", + onclick: move |_| on_update.call(()), + "Update" + } + + div { class: "dropdown-wrapper", + button { + class: "tab-button action-button", + onclick: move |_| show_save_dropdown.toggle(), + "Save as New ▼" + } + if show_save_dropdown() { + div { class: "dropdown-menu", + button { + onclick: move |_| { + show_save_dropdown.set(false); + on_save_new.call(()); + }, + "Save as New" + } + button { + onclick: move |_| { + show_save_dropdown.set(false); + show_replace_warning.set(true); + }, + "Replace" + } + } + } + } + + if show_replace_warning() { + div { class: "inline-warning", + "⚠️ This will delete the current record and create a new one with a different rkey. " + button { + onclick: move |_| { + show_replace_warning.set(false); + on_replace.call(()); + }, + "Yes" + } + button { + onclick: move |_| show_replace_warning.set(false), + "No" + } + } + } + + button { + class: "tab-button action-button action-button-danger", + onclick: move |_| show_delete_confirm.set(true), + "Delete" + } + + DialogRoot { + open: Some(show_delete_confirm()), + on_open_change: move |open: bool| { + show_delete_confirm.set(open); + }, + DialogContent { + DialogTitle { "Delete Record?" } + DialogDescription { + "This action cannot be undone." + } + div { class: "dialog-actions", + button { + onclick: move |_| { + show_delete_confirm.set(false); + on_delete.call(()); + }, + "Delete" + } + button { + onclick: move |_| show_delete_confirm.set(false), + "Cancel" + } + } + } + } + + button { + class: "tab-button action-button", + onclick: move |_| on_cancel.call(()), + "Cancel" + } + } + } +} + +#[component] +pub fn ValidationPanel( + validation: Resource, Option)>>, +) -> Element { + rsx! { + div { class: "validation-panel", + if let Some(Some((result_opt, parse_error_opt))) = validation.read().as_ref() { + if let Some(parse_err) = parse_error_opt { + div { class: "parse-error", + "❌ Invalid JSON: {parse_err}" + } + } + + if let Some(result) = result_opt { + // Structural validity + if result.is_structurally_valid() { + div { class: "validation-success", "✓ Structurally valid" } + } else { + div { class: "parse-error", "❌ Structurally invalid" } + } + + // Overall validity + if result.is_valid() { + div { class: "validation-success", "✓ Fully valid" } + } else { + div { class: "validation-warning", "⚠ Has errors" } + } + + // Show errors if any + if !result.is_valid() { + div { class: "validation-errors", + h4 { "Validation Errors:" } + for error in result.all_errors() { + div { class: "error", "{error}" } + } + } + } + } + } else { + div { "Validating..." } + } + } + } +} diff --git a/crates/weaver-app/src/components/record_view.rs b/crates/weaver-app/src/components/record_view.rs new file mode 100644 index 0000000..2b08cde --- /dev/null +++ b/crates/weaver-app/src/components/record_view.rs @@ -0,0 +1,610 @@ +use crate::components::accordion::{Accordion, AccordionContent, AccordionItem, AccordionTrigger}; +use crate::record_utils::{get_errors_at_exact_path, get_expected_string_format, get_hex_rep}; +use dioxus::prelude::*; +use humansize::format_size; +use jacquard::to_data; +use jacquard::types::string::AtprotoStr; +use jacquard::{ + common::{Data, IntoStatic}, + types::{aturi::AtUri, cid::Cid}, +}; +use jacquard_lexicon::lexicon::LexiconDoc; +use jacquard_lexicon::validation::ValidationResult; +use weaver_renderer::{code_pretty::highlight_code, css::generate_default_css}; + +#[derive(Clone, Copy, PartialEq)] +pub enum ViewMode { + Pretty, + Json, + Schema, +} + +/// Layout component for record view - handles header, metadata, and wraps children +#[component] +pub fn RecordViewLayout( + uri: AtUri<'static>, + cid: Option>, + schema: ReadSignal>>, + record_value: Data<'static>, + children: Element, +) -> Element { + // Validate the record if schema is available + let validation_status = use_memo(move || { + let _schema_doc = schema()?; + let nsid_str = record_value.type_discriminator()?; + + let validator = jacquard_lexicon::validation::SchemaValidator::global(); + let result = validator.validate_by_nsid(nsid_str, &record_value); + + Some(result.is_valid()) + }); + + rsx! { + div { + class: "record-metadata", + div { class: "metadata-row", + span { class: "metadata-label", "URI" } + span { class: "metadata-value", + HighlightedUri { uri: uri.clone() } + } + } + if let Some(cid) = cid { + div { class: "metadata-row", + span { class: "metadata-label", "CID" } + code { class: "metadata-value", "{cid}" } + } + } + if let Some(is_valid) = validation_status() { + div { class: "metadata-row", + span { class: "metadata-label", "Schema" } + span { + class: if is_valid { "metadata-value schema-valid" } else { "metadata-value schema-invalid" }, + if is_valid { "Valid" } else { "Invalid" } + } + } + } + } + + {children} + + } +} + +#[component] +pub fn SchemaView(schema: ReadSignal>>) -> Element { + if let Some(schema_doc) = schema() { + // Convert LexiconDoc to Data for display + let schema_data = to_data(&schema_doc).ok().map(|d| d.into_static()); + + if let Some(data) = schema_data { + rsx! { + div { + class: "pretty-record", + DataView { data: data.clone(), root_data: data, path: String::new(), did: String::new() } + } + } + } else { + rsx! { + div { class: "schema-error", "Failed to convert schema to displayable format" } + } + } + } else { + rsx! { + div { class: "schema-loading", "Loading schema..." } + } + } +} + +#[component] +pub fn DataView( + data: Data<'static>, + root_data: ReadSignal>, + path: String, + did: String, +) -> Element { + // Try to get validation result from context and get errors exactly at this path + let validation_result = try_use_context::>>(); + + let errors = if let Some(vr_signal) = validation_result { + get_errors_at_exact_path(&*vr_signal.read(), &path) + } else { + Vec::new() + }; + + let has_errors = !errors.is_empty(); + + match &data { + Data::Null => rsx! { + div { class: if has_errors { "record-field field-error" } else { "record-field" }, + PathLabel { path: path.clone() } + span { class: "field-value muted", "null" } + if has_errors { + for error in &errors { + div { class: "field-error-message", "{error}" } + } + } + } + }, + Data::Boolean(b) => rsx! { + div { class: if has_errors { "record-field field-error" } else { "record-field" }, + PathLabel { path: path.clone() } + span { class: "field-value", "{b}" } + if has_errors { + for error in &errors { + div { class: "field-error-message", "{error}" } + } + } + } + }, + Data::Integer(i) => rsx! { + div { class: if has_errors { "record-field field-error" } else { "record-field" }, + PathLabel { path: path.clone() } + span { class: "field-value", "{i}" } + if has_errors { + for error in &errors { + div { class: "field-error-message", "{error}" } + } + } + } + }, + Data::String(s) => { + use jacquard::types::string::AtprotoStr; + use jacquard_lexicon::lexicon::LexStringFormat; + + // Get expected format from schema + let expected_format = get_expected_string_format(&*root_data.read(), &path); + + // Get actual type from data + let actual_type_label = match s { + AtprotoStr::Datetime(_) => "datetime", + AtprotoStr::Language(_) => "language", + AtprotoStr::Tid(_) => "tid", + AtprotoStr::Nsid(_) => "nsid", + AtprotoStr::Did(_) => "did", + AtprotoStr::Handle(_) => "handle", + AtprotoStr::AtIdentifier(_) => "at-identifier", + AtprotoStr::AtUri(_) => "at-uri", + AtprotoStr::Uri(_) => "uri", + AtprotoStr::Cid(_) => "cid", + AtprotoStr::RecordKey(_) => "record-key", + AtprotoStr::String(_) => "string", + }; + + // Prefer schema format if available, otherwise use actual type + let type_label = if let Some(fmt) = expected_format { + match fmt { + LexStringFormat::Datetime => "datetime", + LexStringFormat::Uri => "uri", + LexStringFormat::AtUri => "at-uri", + LexStringFormat::Did => "did", + LexStringFormat::Handle => "handle", + LexStringFormat::AtIdentifier => "at-identifier", + LexStringFormat::Nsid => "nsid", + LexStringFormat::Cid => "cid", + LexStringFormat::Language => "language", + LexStringFormat::Tid => "tid", + LexStringFormat::RecordKey => "record-key", + } + } else { + actual_type_label + }; + + rsx! { + div { class: if has_errors { "record-field field-error" } else { "record-field" }, + PathLabel { path: path.clone() } + span { class: "field-value", + + HighlightedString { string_type: s.clone() } + if type_label != "string" { + span { class: "string-type-tag", " [{type_label}]" } + } + } + if has_errors { + for error in &errors { + div { class: "field-error-message", "{error}" } + } + } + } + } + } + Data::Bytes(b) => { + let hex_string = get_hex_rep(&mut b.to_vec()); + let byte_size = if b.len() > 128 { + format_size(b.len(), humansize::BINARY) + } else { + format!("{} bytes", b.len()) + }; + rsx! { + div { class: if has_errors { "record-field field-error" } else { "record-field" }, + PathLabel { path: path.clone() } + pre { class: "field-value bytes", "{hex_string} [{byte_size}]" } + if has_errors { + for error in &errors { + div { class: "field-error-message", "{error}" } + } + } + } + } + } + Data::CidLink(cid) => rsx! { + div { class: if has_errors { "record-field field-error" } else { "record-field" }, + span { class: "field-label", "{path}" } + span { class: "field-value", "{cid}" } + if has_errors { + for error in &errors { + div { class: "field-error-message", "{error}" } + } + } + } + }, + Data::Array(arr) => { + let label = path.split('.').last().unwrap_or(&path); + rsx! { + div { class: "record-section", + Accordion { + id: "array-{path}", + collapsible: true, + AccordionItem { + default_open: true, + index: 0, + AccordionTrigger { + div { class: "section-label", "{label}" span { class: "array-len", "[{arr.len()}]" } } + } + AccordionContent { + if has_errors { + for error in &errors { + div { class: "field-error-message", "{error}" } + } + } + div { class: "section-content", + for (idx, item) in arr.iter().enumerate() { + { + let item_path = format!("{}[{}]", label, idx); + let is_object = matches!(item, Data::Object(_)); + + if is_object { + rsx! { + div { + class: "array-item", + div { class: "record-section", + div { class: "section-label", "{item_path}" } + div { class: "section-content", + DataView { + data: item.clone(), + root_data, + path: item_path.clone(), + did: did.clone() + } + } + } + } + } + } else { + rsx! { + div { + class: "array-item", + DataView { + data: item.clone(), + root_data, + path: item_path, + did: did.clone() + } + } + } + } + } + } + } + } + } + } + } + } + } + Data::Object(obj) => { + let is_root = path.is_empty(); + let is_array_item = path.split('.').last().unwrap_or(&path).contains('['); + + if is_root || is_array_item { + // Root object or array item: just render children (array items already wrapped) + rsx! { + div { class: if !is_root { "record-section" } else {""}, + if has_errors { + for error in &errors { + div { class: "field-error-message", "{error}" } + } + } + for (key, value) in obj.iter() { + { + let new_path = if is_root { + key.to_string() + } else { + format!("{}.{}", path, key) + }; + let did_clone = did.clone(); + rsx! { + DataView { data: value.clone(), root_data, path: new_path, did: did_clone } + } + } + } + } + } + } else { + // Nested object (not array item): wrap in section + let label = path.split('.').last().unwrap_or(&path); + rsx! { + div { class: "record-section", + Accordion { + id: "object-{path}", + collapsible: true, + AccordionItem { + default_open: true, + index: 0, + AccordionTrigger { + div { class: "section-label", "{label}" } + } + AccordionContent { + if has_errors { + for error in &errors { + div { class: "field-error-message", "{error}" } + } + } + div { class: "section-content", + for (key, value) in obj.iter() { + { + let new_path = format!("{}.{}", path, key); + let did_clone = did.clone(); + rsx! { + DataView { data: value.clone(), root_data, path: new_path, did: did_clone } + } + } + } + } + } + } + } + } + } + } + } + Data::Blob(blob) => { + let is_image = blob.mime_type.starts_with("image/"); + let format = blob.mime_type.strip_prefix("image/").unwrap_or("jpeg"); + let image_url = format!( + "https://cdn.bsky.app/img/feed_fullsize/plain/{}/{}@{}", + did, + blob.cid(), + format + ); + + let blob_size = format_size(blob.size, humansize::BINARY); + rsx! { + div { class: "record-field", + span { class: "field-label", "{path}" } + span { class: "field-value mime", "[mimeType: {blob.mime_type}, size: {blob_size}]" } + if is_image { + img { + src: "{image_url}", + alt: "Blob image", + class: "blob-image", + } + } + } + } + } + } +} + +#[component] +pub fn HighlightedUri(uri: AtUri<'static>) -> Element { + let s = uri.as_str(); + let link = format!("{}/record/{}", crate::env::WEAVER_APP_DOMAIN, s); + + if let Some(rest) = s.strip_prefix("at://") { + let parts: Vec<&str> = rest.splitn(3, '/').collect(); + return rsx! { + a { + href: link, + class: "uri-link", + span { class: "string-at-uri", + span { class: "aturi-scheme", "at://" } + span { class: "aturi-authority", "{uri.authority()}" } + + if parts.len() > 1 { + span { class: "aturi-slash", "/" } + if let Some(collection) = uri.collection() { + span { class: "aturi-collection", "{collection.as_ref()}" } + } + } + if parts.len() > 2 { + span { class: "aturi-slash", "/" } + if let Some(rkey) = uri.rkey() { + span { class: "aturi-rkey", "{rkey.as_ref()}" } + } + } + } + } + }; + } + + rsx! { a { class: "string-at-uri", href: s } } +} + +#[component] +pub fn HighlightedString(string_type: AtprotoStr<'static>) -> Element { + use jacquard::types::string::AtprotoStr; + + match &string_type { + AtprotoStr::Nsid(nsid) => { + let parts: Vec<&str> = nsid.as_str().split('.').collect(); + rsx! { + span { class: "string-nsid", + for (i, part) in parts.iter().enumerate() { + span { class: "nsid-segment nsid-segment-{i % 3}", "{part}" } + if i < parts.len() - 1 { + span { class: "nsid-dot", "." } + } + } + } + } + } + AtprotoStr::Did(did) => { + let s = did.as_str(); + if let Some(rest) = s.strip_prefix("did:") { + if let Some((method, identifier)) = rest.split_once(':') { + return rsx! { + span { class: "string-did", + span { class: "did-scheme", "did:" } + span { class: "did-method", "{method}" } + span { class: "did-separator", ":" } + span { class: "did-identifier", "{identifier}" } + } + }; + } + } + rsx! { span { class: "string-did", "{s}" } } + } + AtprotoStr::Handle(handle) => { + let parts: Vec<&str> = handle.as_str().split('.').collect(); + rsx! { + span { class: "string-handle", + for (i, part) in parts.iter().enumerate() { + span { class: "handle-segment handle-segment-{i % 2}", "{part}" } + if i < parts.len() - 1 { + span { class: "handle-dot", "." } + } + } + } + } + } + AtprotoStr::AtUri(uri) => { + rsx! { + HighlightedUri { uri: uri.clone().into_static() } + } + } + AtprotoStr::Uri(uri) => { + let s = uri.as_str(); + if let Ok(at_uri) = AtUri::new(s) { + return rsx! { + HighlightedUri { uri: at_uri.into_static() } + }; + } + + // Try to parse scheme + if let Some((scheme, rest)) = s.split_once("://") { + // Split authority and path + let (authority, path) = if let Some(idx) = rest.find('/') { + (&rest[..idx], &rest[idx..]) + } else { + (rest, "") + }; + + return rsx! { + a { + href: "{s}", + target: "_blank", + rel: "noopener noreferrer", + class: "uri-link", + span { class: "string-uri", + span { class: "uri-scheme", "{scheme}" } + span { class: "uri-separator", "://" } + span { class: "uri-authority", "{authority}" } + if !path.is_empty() { + span { class: "uri-path", "{path}" } + } + } + } + }; + } + + rsx! { span { class: "string-uri", "{s}" } } + } + _ => { + let value = string_type.as_str(); + rsx! { "{value}" } + } + } +} + +#[derive(Props, Clone, PartialEq)] +pub struct CodeViewProps { + #[props(default)] + id: Signal, + #[props(default)] + class: Signal, + code: ReadSignal, + lang: Option, +} + +#[component] +pub fn PrettyRecordView( + record: Data<'static>, + uri: AtUri<'static>, + schema: ReadSignal>>, +) -> Element { + let did = uri.authority().to_string(); + let root_data = use_signal(|| record.clone()); + + // Validate the record and provide via context - only after schema is loaded + let mut validation_result = use_signal(|| None); + use_effect(move || { + // Wait for schema to be loaded + if schema().is_some() { + if let Some(nsid_str) = root_data.read().type_discriminator() { + let validator = jacquard_lexicon::validation::SchemaValidator::global(); + let result = validator.validate_by_nsid(nsid_str, &*root_data.read()); + validation_result.set(Some(result)); + } + } + }); + use_context_provider(|| validation_result); + + rsx! { + div { + class: "pretty-record", + DataView { data: record, root_data, path: String::new(), did } + } + } +} + +#[component] +pub fn CodeView(props: CodeViewProps) -> Element { + let code = &*props.code.read(); + + let mut html_buf = String::new(); + highlight_code(props.lang.as_deref(), code, &mut html_buf).unwrap(); + + rsx! { + document::Style { {generate_default_css().unwrap()}} + div { + id: "{&*props.id.read()}", + class: "{&*props.class.read()}", + dangerous_inner_html: "{html_buf}" + } + } +} + +#[component] +pub fn PathLabel(path: String) -> Element { + if path.is_empty() { + return rsx! {}; + } + + // Find the last separator + let last_sep = path.rfind(|c| c == '.'); + + if let Some(idx) = last_sep { + let prefix = &path[..idx + 1]; + let final_segment = &path[idx + 1..]; + rsx! { + span { class: "field-label", + span { class: "path-prefix", "{prefix}" } + span { class: "path-final", "{final_segment}" } + } + } + } else { + rsx! { + span { class: "field-label","{path}" } + } + } +} diff --git a/crates/weaver-app/src/data.rs b/crates/weaver-app/src/data.rs index 92c244f..7a4477d 100644 --- a/crates/weaver-app/src/data.rs +++ b/crates/weaver-app/src/data.rs @@ -3,35 +3,42 @@ //! In fullstack-server mode, hooks use `use_server_future` with inline async closures. //! In client-only mode, hooks use `use_resource` with context-provided fetchers. +use crate::auth::AuthState; #[cfg(feature = "server")] use crate::blobcache::BlobCache; use dioxus::prelude::*; #[cfg(feature = "fullstack-server")] #[allow(unused_imports)] use dioxus::{CapturedError, fullstack::extract::Extension}; -use jacquard::types::{did::Did, string::Handle}; +use jacquard::{ + IntoStatic, + identity::resolver::IdentityError, + types::{did::Did, string::Handle}, +}; #[allow(unused_imports)] use jacquard::{ prelude::IdentityResolver, smol_str::SmolStr, types::{cid::Cid, string::AtIdentifier}, }; +use std::cell::Ref; #[allow(unused_imports)] use std::sync::Arc; +use weaver_api::com_atproto::repo::strong_ref::StrongRef; +use weaver_api::sh_weaver::actor::ProfileDataView; use weaver_api::sh_weaver::notebook::{BookEntryView, NotebookView, entry::Entry}; // ============================================================================ // Wrapper Hooks (feature-gated) // ============================================================================ /// Fetches entry data with SSR support in fullstack mode. -/// Returns a MappedSignal over the server future resource. #[cfg(feature = "fullstack-server")] pub fn use_entry_data( ident: AtIdentifier<'static>, book_title: SmolStr, title: SmolStr, ) -> Result, Entry<'static>)>>, RenderError> { - let fetcher = use_context::(); + let fetcher = use_context::(); let fetcher = fetcher.clone(); let ident = use_signal(|| ident); let book_title = use_signal(|| book_title); @@ -88,7 +95,6 @@ pub fn use_entry_data( }) }) } - /// Fetches entry data client-side only (no SSR). #[cfg(not(feature = "fullstack-server"))] pub fn use_entry_data( @@ -96,12 +102,12 @@ pub fn use_entry_data( book_title: SmolStr, title: SmolStr, ) -> Result, Entry<'static>)>>, RenderError> { - let fetcher = use_context::(); + let fetcher = use_context::(); let fetcher = fetcher.clone(); let ident = use_signal(|| ident); let book_title = use_signal(|| book_title); let entry_title = use_signal(|| title); - let r = use_resource(move || { + let res = use_resource(move || { let fetcher = fetcher.clone(); async move { fetcher @@ -109,65 +115,84 @@ pub fn use_entry_data( .await .ok() .flatten() - .map(|arc| (arc.0.clone(), arc.1.clone())) + .map(|arc| { + ( + serde_json::to_value(entry.0.clone()).unwrap(), + serde_json::to_value(entry.1.clone()).unwrap(), + ) + }) } }); - Ok(use_memo(move || { - if let Some(Some((ev, e))) = &*r.read_unchecked() { - Some((ev.clone(), e.clone())) - } else { - None - } - })) + res.map(|r| { + use_memo(move || { + if let Some(Some((ev, e))) = &*r.read_unchecked() { + use jacquard::from_json_value; + + let book_entry = from_json_value::(ev.clone()).unwrap(); + let entry = from_json_value::(e.clone()).unwrap(); + + Some((book_entry, entry)) + } else { + None + } + }) + }) +} + +pub fn get_handle(did: Did<'static>) -> AtIdentifier<'static> { + let ident = AtIdentifier::Did(did); + use_handle(ident.clone()) + .read() + .as_ref() + .unwrap_or(&Ok(ident)) + .as_ref() + .unwrap() + .clone() } pub fn use_handle( ident: AtIdentifier<'static>, -) -> Result>, RenderError> { - let fetcher = use_context::(); +) -> Resource, IdentityError>> { + let fetcher = use_context::(); let fetcher = fetcher.clone(); let ident = use_signal(|| ident); - #[cfg(feature = "fullstack-server")] - let h_str = { - use_server_future(move || { - let client = fetcher.get_client(); - async move { - use jacquard::smol_str::ToSmolStr; - client - .resolve_ident_owned(&ident()) - .await - .map(|doc| doc.handles().first().map(|h| h.to_smolstr())) - .ok() - .flatten() - } - }) - }; - #[cfg(not(feature = "fullstack-server"))] - let h_str = { - use_resource(move || { - let client = fetcher.get_client(); - async move { - use jacquard::smol_str::ToSmolStr; - client - .resolve_ident_owned(&ident()) - .await - .map(|doc| doc.handles().first().map(|h| h.to_smolstr())) - .ok() - .flatten() - } - }) - }; - Ok(h_str.map(|h_str| { - use_memo(move || { - if let Some(Some(e)) = &*h_str.read_unchecked() { - use jacquard::IntoStatic; - AtIdentifier::Handle(Handle::raw(&e).into_static()) - } else { - ident() - } - }) - })?) + use_resource(move || { + let client = fetcher.get_client(); + async move { + client + .resolve_ident_owned(&*ident.read()) + .await + .map(|doc| { + doc.handles() + .first() + .map(|h| AtIdentifier::Handle(h.clone()).into_static()) + }) + .map(|h| h.ok_or(IdentityError::invalid_well_known()))? + } + }) +} + +#[derive(Clone, PartialEq, Eq)] +pub struct NotebookHandle(pub Arc>>); + +impl NotebookHandle { + pub fn as_ref(&self) -> Option<&AtIdentifier<'static>> { + self.0.as_ref().as_ref() + } +} + +pub fn use_notebook_handle(ident: Signal>>) -> NotebookHandle { + let ident = if let Some(ident) = &*ident.read() { + if let Some(Ok(handle)) = &*use_handle(ident.clone()).read() { + Some(handle.clone()) + } else { + Some(ident.clone()) + } + } else { + ident.read().clone() + }; + use_context_provider(|| NotebookHandle(Arc::new(ident))) } /// Hook to render markdown client-side only (no SSR). @@ -178,7 +203,7 @@ pub fn use_rendered_markdown( ) -> Result>, RenderError> { let ident = use_signal(|| ident); let content = use_signal(|| content); - let fetcher = use_context::(); + let fetcher = use_context::(); Ok(use_server_future(move || { let client = fetcher.get_client(); async move { @@ -199,7 +224,7 @@ pub fn use_rendered_markdown( ) -> Result>, RenderError> { let ident = use_signal(|| ident); let content = use_signal(|| content); - let fetcher = use_context::(); + let fetcher = use_context::(); Ok(use_resource(move || { let client = fetcher.get_client(); async move { @@ -221,7 +246,8 @@ async fn render_markdown_impl(content: Entry<'static>, did: Did<'static>) -> Str }; let ctx = ClientContext::<()>::new(content.clone(), did); - let parser = markdown_weaver::Parser::new(&content.content); + let parser = + markdown_weaver::Parser::new_ext(&content.content, weaver_renderer::default_md_options()); let iter = ContextIterator::default(parser); let processor = NotebookProcessor::new(ctx, iter); @@ -236,8 +262,8 @@ async fn render_markdown_impl(content: Entry<'static>, did: Did<'static>) -> Str #[cfg(feature = "fullstack-server")] pub fn use_profile_data( ident: AtIdentifier<'static>, -) -> Result>>, RenderError> { - let fetcher = use_context::(); +) -> Result>>, RenderError> { + let fetcher = use_context::(); let ident = use_signal(|| ident); let res = use_server_future(move || { let fetcher = fetcher.clone(); @@ -252,10 +278,7 @@ pub fn use_profile_data( })?; Ok(use_memo(move || { if let Some(Some(value)) = &*res.read_unchecked() { - jacquard::from_json_value::( - value.clone(), - ) - .ok() + jacquard::from_json_value::(value.clone()).ok() } else { None } @@ -266,20 +289,26 @@ pub fn use_profile_data( #[cfg(not(feature = "fullstack-server"))] pub fn use_profile_data( ident: AtIdentifier<'static>, -) -> Result>>, RenderError> { - let fetcher = use_context::(); - let r = use_resource(use_reactive!(|ident| { +) -> Result>>, RenderError> { + let fetcher = use_context::(); + let ident = use_signal(|| ident); + let res = use_resource(move || { let fetcher = fetcher.clone(); async move { fetcher - .fetch_profile(&ident) + .fetch_profile(&ident()) .await .ok() - .map(|arc| (*arc).clone()) + .map(|arc| serde_json::to_value(&*arc).ok()) + .flatten() } - })); + }); Ok(use_memo(move || { - r.read_unchecked().as_ref().and_then(|v| v.clone()) + if let Some(Some(value)) = &*res.read_unchecked() { + jacquard::from_json_value::(value.clone()).ok() + } else { + None + } })) } @@ -287,18 +316,8 @@ pub fn use_profile_data( #[cfg(feature = "fullstack-server")] pub fn use_notebooks_for_did( ident: AtIdentifier<'static>, -) -> Result< - Memo< - Option< - Vec<( - weaver_api::sh_weaver::notebook::NotebookView<'static>, - Vec>, - )>, - >, - >, - RenderError, -> { - let fetcher = use_context::(); +) -> Result, Vec>)>>>, RenderError> { + let fetcher = use_context::(); let ident = use_signal(|| ident); let res = use_server_future(move || { let fetcher = fetcher.clone(); @@ -321,11 +340,7 @@ pub fn use_notebooks_for_did( values .iter() .map(|v| { - jacquard::from_json_value::<( - weaver_api::sh_weaver::notebook::NotebookView, - Vec, - )>(v.clone()) - .ok() + jacquard::from_json_value::<(NotebookView, Vec)>(v.clone()).ok() }) .collect::>>() } else { @@ -338,40 +353,44 @@ pub fn use_notebooks_for_did( #[cfg(not(feature = "fullstack-server"))] pub fn use_notebooks_for_did( ident: AtIdentifier<'static>, -) -> Result< - Memo< - Option< - Vec<( - weaver_api::sh_weaver::notebook::NotebookView<'static>, - Vec>, - )>, - >, - >, - RenderError, -> { - let fetcher = use_context::(); - let r = use_resource(use_reactive!(|ident| { +) -> Result, Vec>)>>>, RenderError> { + let fetcher = use_context::(); + let ident = use_signal(|| ident); + let res = use_resource(move || { let fetcher = fetcher.clone(); async move { fetcher - .fetch_notebooks_for_did(&ident) + .fetch_notebooks_for_did(&ident()) .await .ok() - .map(|notebooks| notebooks.iter().map(|arc| (*arc).clone()).collect()) + .map(|notebooks| { + notebooks + .iter() + .map(|arc| serde_json::to_value(arc.as_ref()).ok()) + .collect::>>() + }) + .flatten() } - })); + }); Ok(use_memo(move || { - r.read_unchecked().as_ref().and_then(|v| v.clone()) + if let Some(Some(values)) = &*res.read_unchecked() { + values + .iter() + .map(|v| { + jacquard::from_json_value::<(NotebookView, Vec)>(v.clone()).ok() + }) + .collect::>>() + } else { + None + } })) } /// Fetches notebooks from UFOS with SSR support in fullstack mode #[cfg(feature = "fullstack-server")] -pub fn use_notebooks_from_ufos() -> Result< - Memo, Vec>)>>>, - RenderError, -> { - let fetcher = use_context::(); +pub fn use_notebooks_from_ufos() +-> Result, Vec>)>>>, RenderError> { + let fetcher = use_context::(); let res = use_server_future(move || { let fetcher = fetcher.clone(); async move { @@ -393,11 +412,7 @@ pub fn use_notebooks_from_ufos() -> Result< values .iter() .map(|v| { - jacquard::from_json_value::<( - NotebookView, - Vec, - )>(v.clone()) - .ok() + jacquard::from_json_value::<(NotebookView, Vec)>(v.clone()).ok() }) .collect::>>() } else { @@ -408,23 +423,36 @@ pub fn use_notebooks_from_ufos() -> Result< /// Fetches notebooks from UFOS client-side only (no SSR) #[cfg(not(feature = "fullstack-server"))] -pub fn use_notebooks_from_ufos() -> Result< - Memo, Vec>)>>>, - RenderError, -> { - let fetcher = use_context::(); - let r = use_resource(move || { +pub fn use_notebooks_from_ufos() +-> Result, Vec>)>>>, RenderError> { + let fetcher = use_context::(); + let res = use_resource(move || { let fetcher = fetcher.clone(); async move { fetcher .fetch_notebooks_from_ufos() .await .ok() - .map(|notebooks| notebooks.iter().map(|arc| (*arc).clone()).collect()) + .map(|notebooks| { + notebooks + .iter() + .map(|arc| serde_json::to_value(arc.as_ref()).ok()) + .collect::>>() + }) + .flatten() } }); Ok(use_memo(move || { - r.read_unchecked().as_ref().and_then(|v| v.clone()) + if let Some(Some(values)) = &*res.read_unchecked() { + values + .iter() + .map(|v| { + jacquard::from_json_value::<(NotebookView, Vec)>(v.clone()).ok() + }) + .collect::>>() + } else { + None + } })) } @@ -433,11 +461,8 @@ pub fn use_notebooks_from_ufos() -> Result< pub fn use_notebook( ident: AtIdentifier<'static>, book_title: SmolStr, -) -> Result< - Memo, Vec>)>>, - RenderError, -> { - let fetcher = use_context::(); +) -> Result, Vec>)>>, RenderError> { + let fetcher = use_context::(); let ident = use_signal(|| ident); let book_title = use_signal(|| book_title); let res = use_server_future(move || { @@ -454,11 +479,7 @@ pub fn use_notebook( })?; Ok(use_memo(move || { if let Some(Some(value)) = &*res.read_unchecked() { - jacquard::from_json_value::<( - NotebookView, - Vec, - )>(value.clone()) - .ok() + jacquard::from_json_value::<(NotebookView, Vec)>(value.clone()).ok() } else { None } @@ -470,24 +491,28 @@ pub fn use_notebook( pub fn use_notebook( ident: AtIdentifier<'static>, book_title: SmolStr, -) -> Result< - Memo, Vec>)>>, - RenderError, -> { - let fetcher = use_context::(); - let r = use_resource(use_reactive!(|(ident, book_title)| { +) -> Result, Vec>)>>, RenderError> { + let fetcher = use_context::(); + let ident = use_signal(|| ident); + let book_title = use_signal(|| book_title); + let res = use_resource(move || { let fetcher = fetcher.clone(); async move { fetcher - .get_notebook(ident, book_title) + .get_notebook(ident(), book_title()) .await .ok() .flatten() - .map(|arc| (*arc).clone()) + .map(|arc| serde_json::to_value(arc.as_ref()).ok()) + .flatten() } - })); + }); Ok(use_memo(move || { - r.read_unchecked().as_ref().and_then(|v| v.clone()) + if let Some(Some(value)) = &*res.read_unchecked() { + jacquard::from_json_value::<(NotebookView, Vec)>(value.clone()).ok() + } else { + None + } })) } @@ -497,7 +522,7 @@ pub fn use_notebook_entries( ident: AtIdentifier<'static>, book_title: SmolStr, ) -> Result>>>, RenderError> { - let fetcher = use_context::(); + let fetcher = use_context::(); let ident = use_signal(|| ident); let book_title = use_signal(|| book_title); let res = use_server_future(move || { @@ -535,7 +560,7 @@ pub fn use_notebook_entries( ident: AtIdentifier<'static>, book_title: SmolStr, ) -> Result>>>, RenderError> { - let fetcher = use_context::(); + let fetcher = use_context::(); let r = use_resource(use_reactive!(|(ident, book_title)| { let fetcher = fetcher.clone(); async move { diff --git a/crates/weaver-app/src/env.rs b/crates/weaver-app/src/env.rs index b8c902b..e130e54 100644 --- a/crates/weaver-app/src/env.rs +++ b/crates/weaver-app/src/env.rs @@ -1,11 +1,11 @@ // This file is automatically generated by build.rs #[allow(unused)] -pub const WEAVER_APP_ENV: &'static str = "dev"; +pub const WEAVER_APP_ENV: &'static str = "prod"; #[allow(unused)] -pub const WEAVER_APP_HOST: &'static str = "http://localhost"; +pub const WEAVER_APP_HOST: &'static str = "https://alpha.weaver.sh"; #[allow(unused)] -pub const WEAVER_APP_DOMAIN: &'static str = ""; +pub const WEAVER_APP_DOMAIN: &'static str = "https://alpha.weaver.sh"; #[allow(unused)] pub const WEAVER_PORT: &'static str = "8080"; #[allow(unused)] diff --git a/crates/weaver-app/src/fetch.rs b/crates/weaver-app/src/fetch.rs index 8731a59..d1f4848 100644 --- a/crates/weaver-app/src/fetch.rs +++ b/crates/weaver-app/src/fetch.rs @@ -326,33 +326,17 @@ impl AgentSession for Client { } } +//#[cfg(not(feature = "server"))] #[derive(Clone)] -pub struct CachedFetcher { +pub struct Fetcher { pub client: Arc, - book_cache: cache_impl::Cache< - (AtIdentifier<'static>, SmolStr), - Arc<(NotebookView<'static>, Vec>)>, - >, - entry_cache: cache_impl::Cache< - (AtIdentifier<'static>, SmolStr), - Arc<(BookEntryView<'static>, Entry<'static>)>, - >, - profile_cache: cache_impl::Cache, Arc>>, } -/// SAFETY: This isn't thread-safe on WASM, but we aren't multithreaded on WASM -unsafe impl Sync for CachedFetcher {} - -/// SAFETY: This isn't thread-safe on WASM, but we aren't multithreaded on WASM -unsafe impl Send for CachedFetcher {} - -impl CachedFetcher { +//#[cfg(not(feature = "server"))] +impl Fetcher { pub fn new(client: OAuthClient) -> Self { Self { client: Arc::new(Client::new(client)), - book_cache: cache_impl::new_cache(100, Duration::from_secs(1200)), - entry_cache: cache_impl::new_cache(100, Duration::from_secs(600)), - profile_cache: cache_impl::new_cache(100, Duration::from_secs(1800)), } } @@ -390,21 +374,16 @@ impl CachedFetcher { ident: AtIdentifier<'static>, title: SmolStr, ) -> Result, Vec>)>>> { - if let Some(entry) = cache_impl::get(&self.book_cache, &(ident.clone(), title.clone())) { - Ok(Some(entry)) + let client = self.get_client(); + if let Some((notebook, entries)) = client + .notebook_by_title(&ident, &title) + .await + .map_err(|e| dioxus::CapturedError::from_display(e))? + { + let stored = Arc::new((notebook, entries)); + Ok(Some(stored)) } else { - let client = self.get_client(); - if let Some((notebook, entries)) = client - .notebook_by_title(&ident, &title) - .await - .map_err(|e| dioxus::CapturedError::from_display(e))? - { - let stored = Arc::new((notebook, entries)); - cache_impl::insert(&self.book_cache, (ident, title), stored.clone()); - Ok(Some(stored)) - } else { - Ok(None) - } + Ok(None) } } @@ -416,23 +395,16 @@ impl CachedFetcher { ) -> Result, Entry<'static>)>>> { if let Some(result) = self.get_notebook(ident.clone(), book_title).await? { let (notebook, entries) = result.as_ref(); - if let Some(entry) = - cache_impl::get(&self.entry_cache, &(ident.clone(), entry_title.clone())) + let client = self.get_client(); + if let Some(entry) = client + .entry_by_title(notebook, entries.as_ref(), &entry_title) + .await + .map_err(|e| dioxus::CapturedError::from_display(e))? { - Ok(Some(entry)) + let stored = Arc::new(entry); + Ok(Some(stored)) } else { - let client = self.get_client(); - if let Some(entry) = client - .entry_by_title(notebook, entries.as_ref(), &entry_title) - .await - .map_err(|e| dioxus::CapturedError::from_display(e))? - { - let stored = Arc::new(entry); - cache_impl::insert(&self.entry_cache, (ident, entry_title), stored.clone()); - Ok(Some(stored)) - } else { - Ok(None) - } + Ok(None) } } else { Ok(None) @@ -477,8 +449,6 @@ impl CachedFetcher { .unwrap_or_else(|| SmolStr::new("Untitled")); let result = Arc::new((notebook, entries)); - // Cache it - cache_impl::insert(&self.book_cache, (ident, title), result.clone()); notebooks.push(result); } Err(_) => continue, // Skip notebooks that fail to load @@ -546,8 +516,6 @@ impl CachedFetcher { .unwrap_or_else(|| SmolStr::new("Untitled")); let result = Arc::new((notebook, entries)); - // Cache it - cache_impl::insert(&self.book_cache, (ident, title), result.clone()); notebooks.push(result); } Err(_) => continue, // Skip notebooks that fail to load @@ -585,14 +553,6 @@ impl CachedFetcher { &self, ident: &AtIdentifier<'_>, ) -> Result>> { - use jacquard::IntoStatic; - - let ident_static = ident.clone().into_static(); - - if let Some(cached) = cache_impl::get(&self.profile_cache, &ident_static) { - return Ok(cached); - } - let client = self.get_client(); let did = match ident { @@ -609,13 +569,305 @@ impl CachedFetcher { .map_err(|e| dioxus::CapturedError::from_display(e))?; let result = Arc::new(profile_view); - cache_impl::insert(&self.profile_cache, ident_static, result.clone()); Ok(result) } } -impl HttpClient for CachedFetcher { +// //#[cfg(feature = "server")] +// #[derive(Clone)] +// pub struct Fetcher { +// pub client: Arc, +// book_cache: cache_impl::Cache< +// (AtIdentifier<'static>, SmolStr), +// Arc<(NotebookView<'static>, Vec>)>, +// >, +// entry_cache: cache_impl::Cache< +// (AtIdentifier<'static>, SmolStr), +// Arc<(BookEntryView<'static>, Entry<'static>)>, +// >, +// profile_cache: cache_impl::Cache, Arc>>, +// } + +// // /// SAFETY: This isn't thread-safe on WASM, but we aren't multithreaded on WASM +// //#[cfg(feature = "server")] +// unsafe impl Sync for Fetcher {} + +// // /// SAFETY: This isn't thread-safe on WASM, but we aren't multithreaded on WASM +// //#[cfg(feature = "server")] +// unsafe impl Send for Fetcher {} + +// //#[cfg(feature = "server")] +// impl Fetcher { +// pub fn new(client: OAuthClient) -> Self { +// Self { +// client: Arc::new(Client::new(client)), +// book_cache: cache_impl::new_cache(100, Duration::from_secs(30)), +// entry_cache: cache_impl::new_cache(100, Duration::from_secs(30)), +// profile_cache: cache_impl::new_cache(100, Duration::from_secs(1800)), +// } +// } + +// pub async fn upgrade_to_authenticated( +// &self, +// session: OAuthSession, +// ) { +// let mut session_slot = self.client.session.write().await; +// *session_slot = Some(Arc::new(Agent::new(session))); +// } + +// pub async fn downgrade_to_unauthenticated(&self) { +// let mut session_slot = self.client.session.write().await; +// if let Some(session) = session_slot.take() { +// session.inner().logout().await.ok(); +// } +// } + +// #[allow(dead_code)] +// pub async fn current_did(&self) -> Option> { +// let session_slot = self.client.session.read().await; +// if let Some(session) = session_slot.as_ref() { +// session.info().await.map(|(d, _)| d) +// } else { +// None +// } +// } + +// pub fn get_client(&self) -> Arc { +// self.client.clone() +// } + +// pub async fn get_notebook( +// &self, +// ident: AtIdentifier<'static>, +// title: SmolStr, +// ) -> Result, Vec>)>>> { +// if let Some(entry) = cache_impl::get(&self.book_cache, &(ident.clone(), title.clone())) { +// Ok(Some(entry)) +// } else { +// let client = self.get_client(); +// if let Some((notebook, entries)) = client +// .notebook_by_title(&ident, &title) +// .await +// .map_err(|e| dioxus::CapturedError::from_display(e))? +// { +// let stored = Arc::new((notebook, entries)); +// cache_impl::insert(&self.book_cache, (ident, title), stored.clone()); +// Ok(Some(stored)) +// } else { +// Ok(None) +// } +// } +// } + +// pub async fn get_entry( +// &self, +// ident: AtIdentifier<'static>, +// book_title: SmolStr, +// entry_title: SmolStr, +// ) -> Result, Entry<'static>)>>> { +// if let Some(result) = self.get_notebook(ident.clone(), book_title).await? { +// let (notebook, entries) = result.as_ref(); +// if let Some(entry) = +// cache_impl::get(&self.entry_cache, &(ident.clone(), entry_title.clone())) +// { +// Ok(Some(entry)) +// } else { +// let client = self.get_client(); +// if let Some(entry) = client +// .entry_by_title(notebook, entries.as_ref(), &entry_title) +// .await +// .map_err(|e| dioxus::CapturedError::from_display(e))? +// { +// let stored = Arc::new(entry); +// cache_impl::insert(&self.entry_cache, (ident, entry_title), stored.clone()); +// Ok(Some(stored)) +// } else { +// Ok(None) +// } +// } +// } else { +// Ok(None) +// } +// } + +// pub async fn fetch_notebooks_from_ufos( +// &self, +// ) -> Result, Vec>)>>> { +// use jacquard::{IntoStatic, types::aturi::AtUri}; + +// let url = "https://ufos-api.microcosm.blue/records?collection=sh.weaver.notebook.book"; +// let response = reqwest::get(url) +// .await +// .map_err(|e| dioxus::CapturedError::from_display(e))?; + +// let records: Vec = response +// .json() +// .await +// .map_err(|e| dioxus::CapturedError::from_display(e))?; + +// let mut notebooks = Vec::new(); +// let client = self.get_client(); + +// for ufos_record in records { +// // Construct URI +// let uri_str = format!( +// "at://{}/{}/{}", +// ufos_record.did, ufos_record.collection, ufos_record.rkey +// ); +// let uri = AtUri::new_owned(uri_str) +// .map_err(|e| dioxus::CapturedError::from_display(format!("Invalid URI: {}", e)))?; + +// // Fetch the full notebook view (which hydrates authors) +// match client.view_notebook(&uri).await { +// Ok((notebook, entries)) => { +// let ident = uri.authority().clone().into_static(); +// let title = notebook +// .title +// .as_ref() +// .map(|t| SmolStr::new(t.as_ref())) +// .unwrap_or_else(|| SmolStr::new("Untitled")); + +// let result = Arc::new((notebook, entries)); +// // Cache it +// cache_impl::insert(&self.book_cache, (ident, title), result.clone()); +// notebooks.push(result); +// } +// Err(_) => continue, // Skip notebooks that fail to load +// } +// } + +// Ok(notebooks) +// } + +// pub async fn fetch_notebooks_for_did( +// &self, +// ident: &AtIdentifier<'_>, +// ) -> Result, Vec>)>>> { +// use jacquard::{ +// IntoStatic, +// types::{collection::Collection, nsid::Nsid}, +// xrpc::XrpcExt, +// }; +// use weaver_api::{ +// com_atproto::repo::list_records::ListRecords, sh_weaver::notebook::book::Book, +// }; + +// let client = self.get_client(); + +// // Resolve DID and PDS +// let (repo_did, pds_url) = match ident { +// AtIdentifier::Did(did) => { +// let pds = client +// .pds_for_did(did) +// .await +// .map_err(|e| dioxus::CapturedError::from_display(e))?; +// (did.clone(), pds) +// } +// AtIdentifier::Handle(handle) => client +// .pds_for_handle(handle) +// .await +// .map_err(|e| dioxus::CapturedError::from_display(e))?, +// }; + +// // Fetch all notebook records for this repo +// let resp = client +// .xrpc(pds_url) +// .send( +// &ListRecords::new() +// .repo(repo_did) +// .collection(Nsid::raw(Book::NSID)) +// .limit(100) +// .build(), +// ) +// .await +// .map_err(|e| dioxus::CapturedError::from_display(e))?; + +// let mut notebooks = Vec::new(); + +// if let Ok(list) = resp.parse() { +// for record in list.records { +// // View the notebook (which hydrates authors) +// match client.view_notebook(&record.uri).await { +// Ok((notebook, entries)) => { +// let ident = record.uri.authority().clone().into_static(); +// let title = notebook +// .title +// .as_ref() +// .map(|t| SmolStr::new(t.as_ref())) +// .unwrap_or_else(|| SmolStr::new("Untitled")); + +// let result = Arc::new((notebook, entries)); +// // Cache it +// cache_impl::insert(&self.book_cache, (ident, title), result.clone()); +// notebooks.push(result); +// } +// Err(_) => continue, // Skip notebooks that fail to load +// } +// } +// } + +// Ok(notebooks) +// } + +// pub async fn list_notebook_entries( +// &self, +// ident: AtIdentifier<'static>, +// book_title: SmolStr, +// ) -> Result>>> { +// if let Some(result) = self.get_notebook(ident.clone(), book_title).await? { +// let (notebook, entries) = result.as_ref(); +// let mut book_entries = Vec::new(); +// let client = self.get_client(); + +// for index in 0..entries.len() { +// match client.view_entry(notebook, entries, index).await { +// Ok(book_entry) => book_entries.push(book_entry), +// Err(_) => continue, // Skip entries that fail to load +// } +// } + +// Ok(Some(book_entries)) +// } else { +// Ok(None) +// } +// } + +// pub async fn fetch_profile( +// &self, +// ident: &AtIdentifier<'_>, +// ) -> Result>> { +// use jacquard::IntoStatic; + +// let ident_static = ident.clone().into_static(); + +// if let Some(cached) = cache_impl::get(&self.profile_cache, &ident_static) { +// return Ok(cached); +// } + +// let client = self.get_client(); + +// let did = match ident { +// AtIdentifier::Did(d) => d.clone(), +// AtIdentifier::Handle(h) => client +// .resolve_handle(h) +// .await +// .map_err(|e| dioxus::CapturedError::from_display(e))?, +// }; + +// let (_uri, profile_view) = client +// .hydrate_profile_view(&did) +// .await +// .map_err(|e| dioxus::CapturedError::from_display(e))?; + +// let result = Arc::new(profile_view); +// cache_impl::insert(&self.profile_cache, ident_static, result.clone()); + +// Ok(result) +// } +// } + +impl HttpClient for Fetcher { #[doc = " Error type returned by the HTTP client"] type Error = reqwest::Error; @@ -645,7 +897,7 @@ impl HttpClient for CachedFetcher { } } -impl XrpcClient for CachedFetcher { +impl XrpcClient for Fetcher { #[doc = " Get the base URI for the client."] fn base_uri(&self) -> impl Future> + Send { self.client.base_uri() @@ -717,7 +969,7 @@ impl XrpcClient for CachedFetcher { } } -impl IdentityResolver for CachedFetcher { +impl IdentityResolver for Fetcher { #[doc = " Access options for validation decisions in default methods"] fn options(&self) -> &ResolverOptions { self.client.options() @@ -766,7 +1018,7 @@ impl IdentityResolver for CachedFetcher { } } -impl LexiconSchemaResolver for CachedFetcher { +impl LexiconSchemaResolver for Fetcher { #[cfg(not(target_arch = "wasm32"))] async fn resolve_lexicon_schema( &self, @@ -784,7 +1036,7 @@ impl LexiconSchemaResolver for CachedFetcher { } } -impl AgentSession for CachedFetcher { +impl AgentSession for Fetcher { #[doc = " Identify the kind of session."] fn session_kind(&self) -> AgentKind { self.client.session_kind() diff --git a/crates/weaver-app/src/main.rs b/crates/weaver-app/src/main.rs index 5ca95cd..22e1539 100644 --- a/crates/weaver-app/src/main.rs +++ b/crates/weaver-app/src/main.rs @@ -1,6 +1,6 @@ // The dioxus prelude contains a ton of common items used in dioxus apps. It's a good idea to import wherever you // need dioxus -use components::{Entry, Repository, RepositoryIndex}; +use components::{EntryPage, Repository, RepositoryIndex}; #[allow(unused)] use dioxus::{CapturedError, prelude::*}; @@ -14,12 +14,10 @@ use jacquard::{ smol_str::SmolStr, types::{cid::Cid, string::AtIdentifier}, }; -#[cfg(feature = "server")] -use std::sync::Arc; -use std::sync::LazyLock; +use std::sync::{Arc, LazyLock}; #[allow(unused)] use views::{ - Callback, Home, Navbar, Notebook, NotebookIndex, NotebookPage, RecordIndex, RecordView, + Callback, Home, Navbar, Notebook, NotebookIndex, NotebookPage, RecordIndex, RecordPage, }; use crate::{ @@ -37,6 +35,7 @@ mod config; mod data; mod env; mod fetch; +mod record_utils; mod service_worker; /// Define a views module that contains the UI for all Layouts and Routes for our app. mod views; @@ -59,7 +58,7 @@ enum Route { #[nest("/record")] #[layout(RecordIndex)] #[route("/:..uri")] - RecordView { uri: Vec }, + RecordPage { uri: Vec }, #[end_layout] #[end_nest] #[route("/callback?:state&:iss&:code")] @@ -73,7 +72,7 @@ enum Route { #[route("/")] NotebookIndex { ident: AtIdentifier<'static>, book_title: SmolStr }, #[route("/:title")] - Entry { ident: AtIdentifier<'static>, book_title: SmolStr, title: SmolStr } + EntryPage { ident: AtIdentifier<'static>, book_title: SmolStr, title: SmolStr } } const FAVICON: Asset = asset!("/assets/weaver_photo_sm.jpg"); @@ -122,7 +121,7 @@ fn main() { #[cfg(feature = "fullstack-server")] let router = { use jacquard::client::UnauthenticatedSession; - let fetcher = Arc::new(fetch::CachedFetcher::new(OAuthClient::new( + let fetcher = Arc::new(fetch::Fetcher::new(OAuthClient::new( AuthStore::new(), ClientData::new_public(CONFIG.oauth.clone()), ))); @@ -132,14 +131,13 @@ fn main() { axum::Router::new() // Server side render the application, serve static assets, and register server functions .serve_dioxus_application( - ServeConfig::builder() - // Enable incremental rendering - .incremental( - dioxus::server::IncrementalRendererConfig::new() - .pre_render(true) - .clear_cache(false), - ) - .enable_out_of_order_streaming(), + ServeConfig::builder(), // Enable incremental rendering + // .incremental( + // dioxus::server::IncrementalRendererConfig::new() + // .pre_render(true) + // .clear_cache(false), + // ) + //.enable_out_of_order_streaming(), App, ) .layer(middleware::from_fn({ @@ -165,18 +163,18 @@ fn main() { #[component] fn App() -> Element { - use_context_provider(|| { - fetch::CachedFetcher::new(OAuthClient::new( + let fetcher = use_context_provider(|| { + fetch::Fetcher::new(OAuthClient::new( AuthStore::new(), ClientData::new_public(CONFIG.oauth.clone()), )) }); use_context_provider(|| Signal::new(AuthState::default())); - use_effect(move || { + let fetcher = fetcher.clone(); spawn(async move { - if let Err(e) = auth::restore_session().await { - tracing::warn!("Session restoration failed: {}", e); + if let Err(e) = auth::restore_session(fetcher).await { + tracing::debug!("Session restoration failed: {}", e); } }); }); @@ -186,10 +184,8 @@ fn App() -> Element { target_os = "unknown", not(feature = "fullstack-server") ))] - use_effect(move || { - spawn(async move { - let _ = service_worker::register_service_worker().await; - }); + spawn(async move { + let _ = service_worker::register_service_worker().await; }); rsx! { diff --git a/crates/weaver-app/src/record_utils.rs b/crates/weaver-app/src/record_utils.rs new file mode 100644 index 0000000..5edbf7a --- /dev/null +++ b/crates/weaver-app/src/record_utils.rs @@ -0,0 +1,361 @@ +use dioxus::prelude::*; +use jacquard::bytes::Bytes; +use jacquard::common::{Data, IntoStatic}; +use jacquard::types::LexiconStringType; +use jacquard::types::string::AtprotoStr; +use jacquard_lexicon::validation::{ + ConstraintError, StructuralError, ValidationError, ValidationResult, +}; + +// ============================================================================ +// Validation Helper Functions +// ============================================================================ + +/// Parse UI path into segments (fields and indices only) +fn parse_ui_path(ui_path: &str) -> Vec { + if ui_path.is_empty() { + return vec![]; + } + + let mut segments = Vec::new(); + let mut current = String::new(); + + for ch in ui_path.chars() { + match ch { + '.' => { + if !current.is_empty() { + segments.push(UiPathSegment::Field(current.clone())); + current.clear(); + } + } + '[' => { + if !current.is_empty() { + segments.push(UiPathSegment::Field(current.clone())); + current.clear(); + } + } + ']' => { + if !current.is_empty() { + if let Ok(idx) = current.parse::() { + segments.push(UiPathSegment::Index(idx)); + } + current.clear(); + } + } + c => current.push(c), + } + } + + if !current.is_empty() { + segments.push(UiPathSegment::Field(current)); + } + + segments +} + +#[derive(Debug, PartialEq)] +enum UiPathSegment { + Field(String), + Index(usize), +} + +/// Get all validation errors at exactly this path (not children) +pub fn get_errors_at_exact_path( + validation_result: &Option, + ui_path: &str, +) -> Vec { + use jacquard_lexicon::validation::PathSegment; + + if let Some(result) = validation_result { + let ui_segments = parse_ui_path(ui_path); + + result + .all_errors() + .filter_map(|err| { + let validation_path = match &err { + ValidationError::Structural(s) => match s { + StructuralError::TypeMismatch { path, .. } => Some(path), + StructuralError::MissingRequiredField { path, .. } => Some(path), + StructuralError::MissingUnionDiscriminator { path } => Some(path), + StructuralError::UnionNoMatch { path, .. } => Some(path), + StructuralError::UnresolvedRef { path, .. } => Some(path), + StructuralError::RefCycle { path, .. } => Some(path), + StructuralError::MaxDepthExceeded { path, .. } => Some(path), + }, + ValidationError::Constraint(c) => match c { + ConstraintError::MaxLength { path, .. } => Some(path), + ConstraintError::MaxGraphemes { path, .. } => Some(path), + ConstraintError::MinLength { path, .. } => Some(path), + ConstraintError::MinGraphemes { path, .. } => Some(path), + ConstraintError::Maximum { path, .. } => Some(path), + ConstraintError::Minimum { path, .. } => Some(path), + }, + }; + + if let Some(path) = validation_path { + // Convert validation path to UI segments + let validation_ui_segments: Vec<_> = path + .segments() + .iter() + .filter_map(|seg| match seg { + PathSegment::Field(name) => { + Some(UiPathSegment::Field(name.to_string())) + } + PathSegment::Index(idx) => Some(UiPathSegment::Index(*idx)), + PathSegment::UnionVariant(_) => None, + }) + .collect(); + + // Exact match only + if validation_ui_segments == ui_segments { + return Some(err.to_string()); + } + } + None + }) + .collect() + } else { + Vec::new() + } +} + +// ============================================================================ +// Pretty Editor: Helper Functions +// ============================================================================ + +/// Infer Data type from text input +pub fn infer_data_from_text(text: &str) -> Result, String> { + let trimmed = text.trim(); + + if trimmed == "true" || trimmed == "false" { + Ok(Data::Boolean(trimmed == "true")) + } else if trimmed == "{}" { + use jacquard::types::value::Object; + use std::collections::BTreeMap; + Ok(Data::Object(Object(BTreeMap::new()))) + } else if trimmed == "[]" { + use jacquard::types::value::Array; + Ok(Data::Array(Array(Vec::new()))) + } else if trimmed == "null" { + Ok(Data::Null) + } else if let Ok(num) = trimmed.parse::() { + Ok(Data::Integer(num)) + } else { + // Smart string parsing + use jacquard::types::value::parsing; + Ok(Data::String(parsing::parse_string(trimmed).into_static())) + } +} + +/// Parse text as specific AtprotoStr type, preserving type information +pub fn try_parse_as_type( + text: &str, + string_type: LexiconStringType, +) -> Result, String> { + use jacquard::types::string::*; + use std::str::FromStr; + + match string_type { + LexiconStringType::Datetime => Datetime::from_str(text) + .map(AtprotoStr::Datetime) + .map_err(|e| format!("Invalid datetime: {}", e)), + LexiconStringType::Did => Did::new(text) + .map(|v| AtprotoStr::Did(v.into_static())) + .map_err(|e| format!("Invalid DID: {}", e)), + LexiconStringType::Handle => Handle::new(text) + .map(|v| AtprotoStr::Handle(v.into_static())) + .map_err(|e| format!("Invalid handle: {}", e)), + LexiconStringType::AtUri => AtUri::new(text) + .map(|v| AtprotoStr::AtUri(v.into_static())) + .map_err(|e| format!("Invalid AT-URI: {}", e)), + LexiconStringType::AtIdentifier => AtIdentifier::new(text) + .map(|v| AtprotoStr::AtIdentifier(v.into_static())) + .map_err(|e| format!("Invalid identifier: {}", e)), + LexiconStringType::Nsid => Nsid::new(text) + .map(|v| AtprotoStr::Nsid(v.into_static())) + .map_err(|e| format!("Invalid NSID: {}", e)), + LexiconStringType::Tid => Tid::new(text) + .map(|v| AtprotoStr::Tid(v.into_static())) + .map_err(|e| format!("Invalid TID: {}", e)), + LexiconStringType::RecordKey => Rkey::new(text) + .map(|rk| AtprotoStr::RecordKey(RecordKey::from(rk))) + .map_err(|e| format!("Invalid record key: {}", e)), + LexiconStringType::Cid => Cid::new(text.as_bytes()) + .map(|v| AtprotoStr::Cid(v.into_static())) + .map_err(|_| "Invalid CID".to_string()), + LexiconStringType::Language => Language::new(text) + .map(AtprotoStr::Language) + .map_err(|e| format!("Invalid language: {}", e)), + LexiconStringType::Uri(_) => Uri::new(text) + .map(|u| AtprotoStr::Uri(u.into_static())) + .map_err(|e| format!("Invalid URI: {}", e)), + LexiconStringType::String => { + // Plain strings: use smart inference + use jacquard::types::value::parsing; + Ok(parsing::parse_string(text).into_static()) + } + } +} + +/// Create default value for new array item by cloning structure of existing items +pub fn create_array_item_default(arr: &jacquard::types::value::Array) -> Data<'static> { + if let Some(existing) = arr.0.first() { + clone_structure(existing) + } else { + // Empty array, default to null (user can change type) + Data::Null + } +} + +/// Clone structure of Data, setting sensible defaults for leaf values +pub fn clone_structure(data: &Data) -> Data<'static> { + use jacquard::types::string::*; + use jacquard::types::value::{Array, Object}; + use jacquard::types::{LexiconStringType, blob::*}; + use std::collections::BTreeMap; + + match data { + Data::Object(obj) => { + let mut new_obj = BTreeMap::new(); + for (key, value) in obj.0.iter() { + new_obj.insert(key.clone(), clone_structure(value)); + } + Data::Object(Object(new_obj)) + } + Data::Array(_) => Data::Array(Array(Vec::new())), + Data::String(s) => match s.string_type() { + LexiconStringType::Datetime => { + // Sensible default: now + Data::String(AtprotoStr::Datetime(Datetime::now())) + } + LexiconStringType::Tid => Data::String(AtprotoStr::Tid(Tid::now_0())), + _ => { + // Empty string, type inference will handle it + Data::String(AtprotoStr::String("".into())) + } + }, + Data::Integer(_) => Data::Integer(0), + Data::Boolean(_) => Data::Boolean(false), + Data::Blob(blob) => { + // Placeholder blob + Data::Blob( + Blob { + r#ref: CidLink::str( + "bafkreiaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + ), + mime_type: blob.mime_type.clone(), + size: 0, + } + .into_static(), + ) + } + Data::CidLink(_) => Data::CidLink(Cid::str( + "bafkreiaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + )), + Data::Bytes(_) => Data::Bytes(Bytes::new()), + Data::Null => Data::Null, + } +} + +/// Get expected string format from schema by navigating path +pub fn get_expected_string_format( + root_data: &Data<'_>, + current_path: &str, +) -> Option { + use jacquard_lexicon::lexicon::*; + + // Get root type discriminator + let root_nsid = root_data.type_discriminator()?; + + // Look up schema in global registry + let validator = jacquard_lexicon::validation::SchemaValidator::global(); + let registry = validator.registry(); + let schema = registry.get(root_nsid)?; + + // Navigate to the property at this path + let segments: Vec<&str> = if current_path.is_empty() { + vec![] + } else { + current_path.split('.').collect() + }; + + // Start with the record's main object definition + let main_obj = match schema.defs.get("main")? { + LexUserType::Record(rec) => match &rec.record { + LexRecordRecord::Object(obj) => obj, + }, + _ => return None, + }; + + // Track current position in schema + enum SchemaType<'a> { + ObjectProp(&'a LexObjectProperty<'a>), + ArrayItem(&'a LexArrayItem<'a>), + } + + let mut current_type: Option = None; + let mut current_obj = Some(main_obj); + + for segment in segments { + // Handle array indices - strip them to get field name + let field_name = segment.trim_end_matches(|c: char| c.is_numeric() || c == '[' || c == ']'); + + if field_name.is_empty() { + continue; // Pure array index like [0], skip + } + + if let Some(obj) = current_obj.take() { + if let Some(prop) = obj.properties.get(field_name) { + current_type = Some(SchemaType::ObjectProp(prop)); + } + } + + // Process current type + match current_type { + Some(SchemaType::ObjectProp(LexObjectProperty::Array(arr))) => { + // Array - unwrap to item type + current_type = Some(SchemaType::ArrayItem(&arr.items)); + } + Some(SchemaType::ObjectProp(LexObjectProperty::Object(obj))) => { + // Nested object - descend into it + current_obj = Some(obj); + current_type = None; + } + Some(SchemaType::ArrayItem(LexArrayItem::Object(obj))) => { + // Array of objects - descend into object + current_obj = Some(obj); + current_type = None; + } + _ => {} + } + } + + // Check if final type is a string with format + match current_type? { + SchemaType::ObjectProp(LexObjectProperty::String(lex_string)) => lex_string.format, + SchemaType::ArrayItem(LexArrayItem::String(lex_string)) => lex_string.format, + _ => None, + } +} + +pub fn get_hex_rep(byte_array: &mut [u8]) -> String { + let build_string_vec: Vec = byte_array + .chunks(2) + .enumerate() + .map(|(i, c)| { + let sep = if i % 16 == 0 && i > 0 { + "\n" + } else if i == 0 { + "" + } else { + " " + }; + if c.len() == 2 { + format!("{}{:02x}{:02x}", sep, c[0], c[1]) + } else { + format!("{}{:02x}", sep, c[0]) + } + }) + .collect(); + build_string_vec.join("") +} diff --git a/crates/weaver-app/src/service_worker.rs b/crates/weaver-app/src/service_worker.rs index 9df41e6..a96f879 100644 --- a/crates/weaver-app/src/service_worker.rs +++ b/crates/weaver-app/src/service_worker.rs @@ -25,7 +25,7 @@ pub async fn register_entry_blobs( ident: &jacquard::types::ident::AtIdentifier<'_>, book_title: &str, images: &weaver_api::sh_weaver::embed::images::Images<'_>, - fetcher: &crate::fetch::CachedFetcher, + fetcher: &crate::fetch::Fetcher, ) -> Result<(), JsValue> { use jacquard::prelude::IdentityResolver; use std::collections::HashMap; diff --git a/crates/weaver-app/src/views/callback.rs b/crates/weaver-app/src/views/callback.rs index 9a79d9a..d7712a1 100644 --- a/crates/weaver-app/src/views/callback.rs +++ b/crates/weaver-app/src/views/callback.rs @@ -1,5 +1,5 @@ use crate::auth::AuthState; -use crate::fetch::CachedFetcher; +use crate::fetch::Fetcher; use dioxus::prelude::*; use jacquard::{ IntoStatic, @@ -15,7 +15,7 @@ pub fn Callback( iss: ReadSignal, code: ReadSignal, ) -> Element { - let fetcher = use_context::(); + let fetcher = use_context::(); let mut auth = use_context::>(); #[cfg(feature = "web")] let result = { @@ -43,6 +43,8 @@ pub fn Callback( }; #[cfg(not(feature = "web"))] let result = { use_resource(move || async { Ok::<(), OAuthError>(()) }) }; + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + let nav = use_navigator(); match &*result.read_unchecked() { Some(Ok(())) => { @@ -52,7 +54,7 @@ pub fn Callback( let mut prev = gloo_storage::LocalStorage::get::("cached_route").ok(); if let Some(prev) = prev.take() { tracing::info!("Navigating to previous page"); - let nav = use_navigator(); + gloo_storage::LocalStorage::delete("cached_route"); nav.replace(prev); } diff --git a/crates/weaver-app/src/views/editor.rs b/crates/weaver-app/src/views/editor.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/weaver-app/src/views/home.rs b/crates/weaver-app/src/views/home.rs index 212a6c0..a48f5fd 100644 --- a/crates/weaver-app/src/views/home.rs +++ b/crates/weaver-app/src/views/home.rs @@ -8,7 +8,7 @@ const NOTEBOOK_CARD_CSS: Asset = asset!("/assets/styling/notebook-card.css"); #[component] pub fn Home() -> Element { // Fetch notebooks from UFOS with SSR support - let notebooks = data::use_notebooks_from_ufos().ok(); + let notebooks = data::use_notebooks_from_ufos()?; let navigator = use_navigator(); let mut uri_input = use_signal(|| String::new()); @@ -16,7 +16,7 @@ pub fn Home() -> Element { let input_uri = uri_input.read().clone(); if !input_uri.is_empty() { if let Ok(parsed) = AtUri::new(&input_uri) { - navigator.push(Route::RecordView { + navigator.push(Route::RecordPage { uri: vec![parsed.to_string()], }); } @@ -44,31 +44,27 @@ pub fn Home() -> Element { } } div { class: "notebooks-list", - if let Some(notebooks_memo) = ¬ebooks { - match &*notebooks_memo.read_unchecked() { - Some(notebook_list) => rsx! { - for notebook in notebook_list.iter() { - { - let view = ¬ebook.0; - let entries = ¬ebook.1; - rsx! { - div { - key: "{view.cid}", - NotebookCard { - notebook: view.clone(), - entry_refs: entries.clone() - } + match &*notebooks.read() { + Some(notebook_list) => rsx! { + for notebook in notebook_list.iter() { + { + let view = ¬ebook.0; + let entries = ¬ebook.1; + rsx! { + div { + key: "{view.cid}", + NotebookCard { + notebook: view.clone(), + entry_refs: entries.clone() } } } } - }, - None => rsx! { - div { "Loading notebooks..." } } + }, + _ => rsx! { + div { "Loading notebooks..." } } - } else { - div { "Loading notebooks..." } } } } diff --git a/crates/weaver-app/src/views/mod.rs b/crates/weaver-app/src/views/mod.rs index fbd78b7..4e3503f 100644 --- a/crates/weaver-app/src/views/mod.rs +++ b/crates/weaver-app/src/views/mod.rs @@ -21,7 +21,9 @@ mod notebook; pub use notebook::{Notebook, NotebookIndex}; mod record; -pub use record::{RecordIndex, RecordView}; +pub use record::{RecordIndex, RecordPage, RecordView}; mod callback; pub use callback::Callback; + +mod editor; diff --git a/crates/weaver-app/src/views/navbar.rs b/crates/weaver-app/src/views/navbar.rs index d496f6c..299b944 100644 --- a/crates/weaver-app/src/views/navbar.rs +++ b/crates/weaver-app/src/views/navbar.rs @@ -1,10 +1,9 @@ use crate::Route; use crate::components::button::{Button, ButtonVariant}; use crate::components::login::LoginModal; -use crate::data::use_handle; -use crate::fetch::CachedFetcher; +use crate::data::{get_handle, use_notebook_handle}; +use crate::fetch::Fetcher; use dioxus::prelude::*; -use jacquard::types::string::AtIdentifier; const THEME_DEFAULTS_CSS: Asset = asset!("/assets/styling/theme-defaults.css"); const NAVBAR_CSS: Asset = asset!("/assets/styling/navbar.css"); @@ -19,7 +18,14 @@ pub fn Navbar() -> Element { let route = use_route::(); let mut auth_state = use_context::>(); let mut show_login_modal = use_signal(|| false); - let fetcher = use_context::(); + let fetcher = use_context::(); + let route_handle = use_signal(|| match &route { + Route::EntryPage { ident, .. } => Some(ident.clone()), + Route::RepositoryIndex { ident } => Some(ident.clone()), + Route::NotebookIndex { ident, .. } => Some(ident.clone()), + _ => None, + }); + let notebook_handle = use_notebook_handle(route_handle); rsx! { document::Link { rel: "stylesheet", href: THEME_DEFAULTS_CSS } @@ -36,32 +42,41 @@ pub fn Navbar() -> Element { // Show repository breadcrumb if we're on a repository page match route { - Route::RepositoryIndex { ident } => rsx! { - span { class: "breadcrumb-separator", " > " } - span { class: "breadcrumb breadcrumb-current", "@{use_handle(ident.clone())?}" } - }, - Route::NotebookIndex { ident, book_title } => rsx! { - span { class: "breadcrumb-separator", " > " } - Link { - to: Route::RepositoryIndex { ident: ident.clone() }, - class: "breadcrumb", - "@{use_handle(ident.clone())?}" + Route::RepositoryIndex { .. } => { + let handle = notebook_handle.as_ref().unwrap(); + rsx! { + span { class: "breadcrumb-separator", " > " } + span { class: "breadcrumb breadcrumb-current", "@{handle}" } } - span { class: "breadcrumb-separator", " > " } - span { class: "breadcrumb breadcrumb-current", "{book_title}" } }, - Route::Entry { ident, book_title, .. } => rsx! { - span { class: "breadcrumb-separator", " > " } - Link { - to: Route::RepositoryIndex { ident: ident.clone() }, - class: "breadcrumb", - "@{use_handle(ident.clone())?}" + Route::NotebookIndex { ident, book_title } => { + let handle = notebook_handle.as_ref().unwrap(); + rsx! { + span { class: "breadcrumb-separator", " > " } + Link { + to: Route::RepositoryIndex { ident: ident.clone() }, + class: "breadcrumb", + "@{handle}" + } + span { class: "breadcrumb-separator", " > " } + span { class: "breadcrumb breadcrumb-current", "{book_title}" } } - span { class: "breadcrumb-separator", " > " } - Link { - to: Route::NotebookIndex { ident: ident.clone(), book_title: book_title.clone() }, - class: "breadcrumb", - "{book_title}" + }, + Route::EntryPage { ident, book_title, .. } => { + let handle = notebook_handle.as_ref().unwrap(); + rsx! { + span { class: "breadcrumb-separator", " > " } + Link { + to: Route::RepositoryIndex { ident: ident.clone() }, + class: "breadcrumb", + "@{handle}" + } + span { class: "breadcrumb-separator", " > " } + Link { + to: Route::NotebookIndex { ident: ident.clone(), book_title: book_title.clone() }, + class: "breadcrumb", + "{book_title}" + } } }, _ => rsx! {} @@ -78,9 +93,10 @@ pub fn Navbar() -> Element { fetcher.downgrade_to_unauthenticated().await; } }, - span { class: "auth-handle", "@{use_handle(AtIdentifier::Did(did.clone()))?}" } + span { class: "auth-handle", "@{get_handle(did.clone())}" } } } + } else { div { class: "auth-button", @@ -97,8 +113,6 @@ pub fn Navbar() -> Element { } } - // The `Outlet` component is used to render the next component inside the layout. In this case, it will render either - // the [`Home`] or [`Blog`] component depending on the current route. Outlet:: {} } } diff --git a/crates/weaver-app/src/views/notebook.rs b/crates/weaver-app/src/views/notebook.rs index 5e26177..916c468 100644 --- a/crates/weaver-app/src/views/notebook.rs +++ b/crates/weaver-app/src/views/notebook.rs @@ -29,47 +29,43 @@ pub fn NotebookIndex( book_title: ReadSignal, ) -> Element { // Fetch full notebook metadata with SSR support - let notebook_data = data::use_notebook(ident(), book_title()).ok(); + let notebook_data = data::use_notebook(ident(), book_title())?; // Fetch entries with SSR support - let entries_resource = data::use_notebook_entries(ident(), book_title()).ok(); + let entries_resource = data::use_notebook_entries(ident(), book_title())?; rsx! { document::Link { rel: "stylesheet", href: ENTRY_CARD_CSS } - if let (Some(notebook_memo), Some(entries_memo)) = (¬ebook_data, &entries_resource) { - match (&*notebook_memo.read_unchecked(), &*entries_memo.read_unchecked()) { - (Some(data), Some(entries)) => { - let (notebook_view, _) = data; - let author_count = notebook_view.authors.len(); + match (&*notebook_data.read(), &*entries_resource.read()) { + (Some(data), Some(entries)) => { + let (notebook_view, _) = data; + let author_count = notebook_view.authors.len(); - rsx! { - div { class: "notebook-layout", - aside { class: "notebook-sidebar", - NotebookCover { - notebook: notebook_view.clone(), - title: book_title().to_string() - } + rsx! { + div { class: "notebook-layout", + aside { class: "notebook-sidebar", + NotebookCover { + notebook: notebook_view.clone(), + title: book_title().to_string() } + } - main { class: "notebook-main", - div { class: "entries-list", - for entry in entries { - EntryCard { - entry: entry.clone(), - book_title: book_title(), - author_count - } + main { class: "notebook-main", + div { class: "entries-list", + for entry in entries { + EntryCard { + entry: entry.clone(), + book_title: book_title(), + author_count } } } } } - }, - _ => rsx! { div { class: "loading", "Loading..." } } - } - } else { - div { class: "loading", "Loading..." } + } + }, + _ => rsx! { div { class: "loading", "Loading..." } } } } } diff --git a/crates/weaver-app/src/views/record.rs b/crates/weaver-app/src/views/record.rs index 60b0bb7..bb6d7e8 100644 --- a/crates/weaver-app/src/views/record.rs +++ b/crates/weaver-app/src/views/record.rs @@ -1,41 +1,20 @@ use crate::Route; use crate::auth::AuthState; -use crate::components::accordion::{Accordion, AccordionContent, AccordionItem, AccordionTrigger}; -use crate::components::dialog::{DialogContent, DialogDescription, DialogRoot, DialogTitle}; -use crate::fetch::CachedFetcher; -use dioxus::prelude::Event; -use dioxus::{CapturedError, prelude::*}; -use humansize::format_size; -use jacquard::api::com_atproto::repo::get_record::GetRecordOutput; -use jacquard::bytes::Bytes; -use jacquard::client::AgentError; +use crate::components::record_editor::EditableRecordContent; +use crate::components::record_view::{ + CodeView, PrettyRecordView, RecordViewLayout, SchemaView, ViewMode, +}; +use crate::fetch::Fetcher; +use dioxus::prelude::*; use jacquard::common::to_data; use jacquard::smol_str::ToSmolStr; -use jacquard::types::LexiconStringType; -use jacquard::types::string::AtprotoStr; -use jacquard::{atproto, prelude::*}; use jacquard::{ client::AgentSessionExt, - common::{Data, IntoStatic}, + common::IntoStatic, identity::lexicon_resolver::LexiconSchemaResolver, - types::{aturi::AtUri, cid::Cid, ident::AtIdentifier, string::Nsid}, + types::{aturi::AtUri, ident::AtIdentifier, string::Nsid}, }; use jacquard_lexicon::lexicon::LexiconDoc; -use jacquard_lexicon::validation::{ - ConstraintError, StructuralError, ValidationError, ValidationPath, ValidationResult, -}; -use mime_sniffer::MimeTypeSniffer; -use weaver_api::com_atproto::repo::{ - create_record::CreateRecord, delete_record::DeleteRecord, put_record::PutRecord, -}; -use weaver_renderer::{code_pretty::highlight_code, css::generate_default_css}; - -#[derive(Clone, Copy, PartialEq)] -enum ViewMode { - Pretty, - Json, - Schema, -} #[component] pub fn RecordIndex() -> Element { @@ -77,9 +56,16 @@ pub fn RecordIndex() -> Element { } } +#[component] +pub fn RecordPage(uri: ReadSignal>) -> Element { + rsx! { + {std::iter::once(rsx! {RecordView {uri}})} + } +} + #[component] pub fn RecordView(uri: ReadSignal>) -> Element { - let fetcher = use_context::(); + let fetcher = use_context::(); info!("Uri:{:?}", uri().join("/")); let at_uri = AtUri::new_owned(&*uri.read().join("/")); if at_uri.is_err() { @@ -109,7 +95,7 @@ pub fn RecordView(uri: ReadSignal>) -> Element { // Helper to recursively resolve a schema and its refs fn resolve_schema_with_refs<'a>( - fetcher: &'a CachedFetcher, + fetcher: &'a Fetcher, type_str: &'a str, validator: &'a jacquard_lexicon::validation::SchemaValidator, resolved: &'a mut std::collections::HashSet, @@ -278,2587 +264,3 @@ pub fn RecordView(uri: ReadSignal>) -> Element { rsx! {} } } - -#[component] -fn PrettyRecordView( - record: Data<'static>, - uri: AtUri<'static>, - schema: ReadSignal>>, -) -> Element { - let did = uri.authority().to_string(); - let root_data = use_signal(|| record.clone()); - - // Validate the record and provide via context - only after schema is loaded - let mut validation_result = use_signal(|| None); - use_effect(move || { - // Wait for schema to be loaded - if schema().is_some() { - if let Some(nsid_str) = root_data.read().type_discriminator() { - let validator = jacquard_lexicon::validation::SchemaValidator::global(); - let result = validator.validate_by_nsid(nsid_str, &*root_data.read()); - validation_result.set(Some(result)); - } - } - }); - use_context_provider(|| validation_result); - - rsx! { - div { - class: "pretty-record", - DataView { data: record, root_data, path: String::new(), did } - } - } -} - -#[component] -fn SchemaView(schema: ReadSignal>>) -> Element { - if let Some(schema_doc) = schema() { - // Convert LexiconDoc to Data for display - let schema_data = use_memo(move || to_data(&schema_doc).ok().map(|d| d.into_static())); - - if let Some(data) = schema_data() { - let root_data = use_signal(|| data.clone()); - rsx! { - div { - class: "pretty-record", - DataView { data: data, root_data, path: String::new(), did: String::new() } - } - } - } else { - rsx! { - div { class: "schema-error", "Failed to convert schema to displayable format" } - } - } - } else { - rsx! { - div { class: "schema-loading", "Loading schema..." } - } - } -} -fn get_hex_rep(byte_array: &mut [u8]) -> String { - let build_string_vec: Vec = byte_array - .chunks(2) - .enumerate() - .map(|(i, c)| { - let sep = if i % 16 == 0 && i > 0 { - "\n" - } else if i == 0 { - "" - } else { - " " - }; - if c.len() == 2 { - format!("{}{:02x}{:02x}", sep, c[0], c[1]) - } else { - format!("{}{:02x}", sep, c[0]) - } - }) - .collect(); - build_string_vec.join("") -} - -#[component] -fn PathLabel(path: String) -> Element { - if path.is_empty() { - return rsx! {}; - } - - // Find the last separator - let last_sep = path.rfind(|c| c == '.'); - - if let Some(idx) = last_sep { - let prefix = &path[..idx + 1]; - let final_segment = &path[idx + 1..]; - rsx! { - span { class: "field-label", - span { class: "path-prefix", "{prefix}" } - span { class: "path-final", "{final_segment}" } - } - } - } else { - rsx! { - span { class: "field-label","{path}" } - } - } -} - -/// Get expected string format from schema by navigating path -fn get_expected_string_format( - root_data: &Data<'_>, - current_path: &str, -) -> Option { - use jacquard_lexicon::lexicon::*; - - // Get root type discriminator - let root_nsid = root_data.type_discriminator()?; - - // Look up schema in global registry - let validator = jacquard_lexicon::validation::SchemaValidator::global(); - let registry = validator.registry(); - let schema = registry.get(root_nsid)?; - - // Navigate to the property at this path - let segments: Vec<&str> = if current_path.is_empty() { - vec![] - } else { - current_path.split('.').collect() - }; - - // Start with the record's main object definition - let main_obj = match schema.defs.get("main")? { - LexUserType::Record(rec) => match &rec.record { - LexRecordRecord::Object(obj) => obj, - }, - _ => return None, - }; - - // Track current position in schema - enum SchemaType<'a> { - ObjectProp(&'a LexObjectProperty<'a>), - ArrayItem(&'a LexArrayItem<'a>), - } - - let mut current_type: Option = None; - let mut current_obj = Some(main_obj); - - for segment in segments { - // Handle array indices - strip them to get field name - let field_name = segment.trim_end_matches(|c: char| c.is_numeric() || c == '[' || c == ']'); - - if field_name.is_empty() { - continue; // Pure array index like [0], skip - } - - if let Some(obj) = current_obj.take() { - if let Some(prop) = obj.properties.get(field_name) { - current_type = Some(SchemaType::ObjectProp(prop)); - } - } - - // Process current type - match current_type { - Some(SchemaType::ObjectProp(LexObjectProperty::Array(arr))) => { - // Array - unwrap to item type - current_type = Some(SchemaType::ArrayItem(&arr.items)); - } - Some(SchemaType::ObjectProp(LexObjectProperty::Object(obj))) => { - // Nested object - descend into it - current_obj = Some(obj); - current_type = None; - } - Some(SchemaType::ArrayItem(LexArrayItem::Object(obj))) => { - // Array of objects - descend into object - current_obj = Some(obj); - current_type = None; - } - _ => {} - } - } - - // Check if final type is a string with format - match current_type? { - SchemaType::ObjectProp(LexObjectProperty::String(lex_string)) => lex_string.format, - SchemaType::ArrayItem(LexArrayItem::String(lex_string)) => lex_string.format, - _ => None, - } -} - -#[component] -fn DataView( - data: Data<'static>, - root_data: ReadSignal>, - path: String, - did: String, -) -> Element { - // Try to get validation result from context and get errors exactly at this path - let validation_result = try_use_context::>>(); - - let errors = if let Some(vr_signal) = validation_result { - get_errors_at_exact_path(&*vr_signal.read(), &path) - } else { - Vec::new() - }; - - let has_errors = !errors.is_empty(); - - match &data { - Data::Null => rsx! { - div { class: if has_errors { "record-field field-error" } else { "record-field" }, - PathLabel { path: path.clone() } - span { class: "field-value muted", "null" } - if has_errors { - for error in &errors { - div { class: "field-error-message", "{error}" } - } - } - } - }, - Data::Boolean(b) => rsx! { - div { class: if has_errors { "record-field field-error" } else { "record-field" }, - PathLabel { path: path.clone() } - span { class: "field-value", "{b}" } - if has_errors { - for error in &errors { - div { class: "field-error-message", "{error}" } - } - } - } - }, - Data::Integer(i) => rsx! { - div { class: if has_errors { "record-field field-error" } else { "record-field" }, - PathLabel { path: path.clone() } - span { class: "field-value", "{i}" } - if has_errors { - for error in &errors { - div { class: "field-error-message", "{error}" } - } - } - } - }, - Data::String(s) => { - use jacquard::types::string::AtprotoStr; - use jacquard_lexicon::lexicon::LexStringFormat; - - // Get expected format from schema - let expected_format = get_expected_string_format(&*root_data.read(), &path); - - // Get actual type from data - let actual_type_label = match s { - AtprotoStr::Datetime(_) => "datetime", - AtprotoStr::Language(_) => "language", - AtprotoStr::Tid(_) => "tid", - AtprotoStr::Nsid(_) => "nsid", - AtprotoStr::Did(_) => "did", - AtprotoStr::Handle(_) => "handle", - AtprotoStr::AtIdentifier(_) => "at-identifier", - AtprotoStr::AtUri(_) => "at-uri", - AtprotoStr::Uri(_) => "uri", - AtprotoStr::Cid(_) => "cid", - AtprotoStr::RecordKey(_) => "record-key", - AtprotoStr::String(_) => "string", - }; - - // Prefer schema format if available, otherwise use actual type - let type_label = if let Some(fmt) = expected_format { - match fmt { - LexStringFormat::Datetime => "datetime", - LexStringFormat::Uri => "uri", - LexStringFormat::AtUri => "at-uri", - LexStringFormat::Did => "did", - LexStringFormat::Handle => "handle", - LexStringFormat::AtIdentifier => "at-identifier", - LexStringFormat::Nsid => "nsid", - LexStringFormat::Cid => "cid", - LexStringFormat::Language => "language", - LexStringFormat::Tid => "tid", - LexStringFormat::RecordKey => "record-key", - } - } else { - actual_type_label - }; - - rsx! { - div { class: if has_errors { "record-field field-error" } else { "record-field" }, - PathLabel { path: path.clone() } - span { class: "field-value", - - HighlightedString { string_type: s.clone() } - if type_label != "string" { - span { class: "string-type-tag", " [{type_label}]" } - } - } - if has_errors { - for error in &errors { - div { class: "field-error-message", "{error}" } - } - } - } - } - } - Data::Bytes(b) => { - let hex_string = get_hex_rep(&mut b.to_vec()); - let byte_size = if b.len() > 128 { - format_size(b.len(), humansize::BINARY) - } else { - format!("{} bytes", b.len()) - }; - rsx! { - div { class: if has_errors { "record-field field-error" } else { "record-field" }, - PathLabel { path: path.clone() } - pre { class: "field-value bytes", "{hex_string} [{byte_size}]" } - if has_errors { - for error in &errors { - div { class: "field-error-message", "{error}" } - } - } - } - } - } - Data::CidLink(cid) => rsx! { - div { class: if has_errors { "record-field field-error" } else { "record-field" }, - span { class: "field-label", "{path}" } - span { class: "field-value", "{cid}" } - if has_errors { - for error in &errors { - div { class: "field-error-message", "{error}" } - } - } - } - }, - Data::Array(arr) => { - let label = path.split('.').last().unwrap_or(&path); - rsx! { - div { class: "record-section", - Accordion { - id: "array-{path}", - collapsible: true, - AccordionItem { - default_open: true, - index: 0, - AccordionTrigger { - div { class: "section-label", "{label}" span { class: "array-len", "[{arr.len()}]" } } - } - AccordionContent { - if has_errors { - for error in &errors { - div { class: "field-error-message", "{error}" } - } - } - div { class: "section-content", - for (idx, item) in arr.iter().enumerate() { - { - let item_path = format!("{}[{}]", label, idx); - let is_object = matches!(item, Data::Object(_)); - - if is_object { - rsx! { - div { - class: "array-item", - div { class: "record-section", - div { class: "section-label", "{item_path}" } - div { class: "section-content", - DataView { - data: item.clone(), - root_data, - path: item_path.clone(), - did: did.clone() - } - } - } - } - } - } else { - rsx! { - div { - class: "array-item", - DataView { - data: item.clone(), - root_data, - path: item_path, - did: did.clone() - } - } - } - } - } - } - } - } - } - } - } - } - } - Data::Object(obj) => { - let is_root = path.is_empty(); - let is_array_item = path.split('.').last().unwrap_or(&path).contains('['); - - if is_root || is_array_item { - // Root object or array item: just render children (array items already wrapped) - rsx! { - div { class: if !is_root { "record-section" } else {""}, - if has_errors { - for error in &errors { - div { class: "field-error-message", "{error}" } - } - } - for (key, value) in obj.iter() { - { - let new_path = if is_root { - key.to_string() - } else { - format!("{}.{}", path, key) - }; - let did_clone = did.clone(); - rsx! { - DataView { data: value.clone(), root_data, path: new_path, did: did_clone } - } - } - } - } - } - } else { - // Nested object (not array item): wrap in section - let label = path.split('.').last().unwrap_or(&path); - rsx! { - div { class: "record-section", - Accordion { - id: "object-{path}", - collapsible: true, - AccordionItem { - default_open: true, - index: 0, - AccordionTrigger { - div { class: "section-label", "{label}" } - } - AccordionContent { - if has_errors { - for error in &errors { - div { class: "field-error-message", "{error}" } - } - } - div { class: "section-content", - for (key, value) in obj.iter() { - { - let new_path = format!("{}.{}", path, key); - let did_clone = did.clone(); - rsx! { - DataView { data: value.clone(), root_data, path: new_path, did: did_clone } - } - } - } - } - } - } - } - } - } - } - } - Data::Blob(blob) => { - let is_image = blob.mime_type.starts_with("image/"); - let format = blob.mime_type.strip_prefix("image/").unwrap_or("jpeg"); - let image_url = format!( - "https://cdn.bsky.app/img/feed_fullsize/plain/{}/{}@{}", - did, - blob.cid(), - format - ); - - let blob_size = format_size(blob.size, humansize::BINARY); - rsx! { - div { class: "record-field", - span { class: "field-label", "{path}" } - span { class: "field-value mime", "[mimeType: {blob.mime_type}, size: {blob_size}]" } - if is_image { - img { - src: "{image_url}", - alt: "Blob image", - class: "blob-image", - } - } - } - } - } - } -} - -#[component] -fn HighlightedUri(uri: AtUri<'static>) -> Element { - let s = uri.as_str(); - let link = format!("{}/record/{}", crate::env::WEAVER_APP_DOMAIN, s); - - if let Some(rest) = s.strip_prefix("at://") { - let parts: Vec<&str> = rest.splitn(3, '/').collect(); - return rsx! { - a { - href: link, - class: "uri-link", - span { class: "string-at-uri", - span { class: "aturi-scheme", "at://" } - span { class: "aturi-authority", "{uri.authority()}" } - - if parts.len() > 1 { - span { class: "aturi-slash", "/" } - if let Some(collection) = uri.collection() { - span { class: "aturi-collection", "{collection.as_ref()}" } - } - } - if parts.len() > 2 { - span { class: "aturi-slash", "/" } - if let Some(rkey) = uri.rkey() { - span { class: "aturi-rkey", "{rkey.as_ref()}" } - } - } - } - } - }; - } - - rsx! { a { class: "string-at-uri", href: s } } -} - -#[component] -fn HighlightedString(string_type: AtprotoStr<'static>) -> Element { - use jacquard::types::string::AtprotoStr; - - match &string_type { - AtprotoStr::Nsid(nsid) => { - let parts: Vec<&str> = nsid.as_str().split('.').collect(); - rsx! { - span { class: "string-nsid", - for (i, part) in parts.iter().enumerate() { - span { class: "nsid-segment nsid-segment-{i % 3}", "{part}" } - if i < parts.len() - 1 { - span { class: "nsid-dot", "." } - } - } - } - } - } - AtprotoStr::Did(did) => { - let s = did.as_str(); - if let Some(rest) = s.strip_prefix("did:") { - if let Some((method, identifier)) = rest.split_once(':') { - return rsx! { - span { class: "string-did", - span { class: "did-scheme", "did:" } - span { class: "did-method", "{method}" } - span { class: "did-separator", ":" } - span { class: "did-identifier", "{identifier}" } - } - }; - } - } - rsx! { span { class: "string-did", "{s}" } } - } - AtprotoStr::Handle(handle) => { - let parts: Vec<&str> = handle.as_str().split('.').collect(); - rsx! { - span { class: "string-handle", - for (i, part) in parts.iter().enumerate() { - span { class: "handle-segment handle-segment-{i % 2}", "{part}" } - if i < parts.len() - 1 { - span { class: "handle-dot", "." } - } - } - } - } - } - AtprotoStr::AtUri(uri) => { - rsx! { - HighlightedUri { uri: uri.clone().into_static() } - } - } - AtprotoStr::Uri(uri) => { - let s = uri.as_str(); - if let Ok(at_uri) = AtUri::new(s) { - return rsx! { - HighlightedUri { uri: at_uri.into_static() } - }; - } - - // Try to parse scheme - if let Some((scheme, rest)) = s.split_once("://") { - // Split authority and path - let (authority, path) = if let Some(idx) = rest.find('/') { - (&rest[..idx], &rest[idx..]) - } else { - (rest, "") - }; - - return rsx! { - a { - href: "{s}", - target: "_blank", - rel: "noopener noreferrer", - class: "uri-link", - span { class: "string-uri", - span { class: "uri-scheme", "{scheme}" } - span { class: "uri-separator", "://" } - span { class: "uri-authority", "{authority}" } - if !path.is_empty() { - span { class: "uri-path", "{path}" } - } - } - } - }; - } - - rsx! { span { class: "string-uri", "{s}" } } - } - _ => { - let value = string_type.as_str(); - rsx! { "{value}" } - } - } -} - -#[derive(Props, Clone, PartialEq)] -pub struct CodeViewProps { - #[props(default)] - id: Signal, - #[props(default)] - class: Signal, - code: ReadSignal, - lang: Option, -} - -#[component] -fn JsonEditor( - data: Signal>, - nsid: ReadSignal>, - schema: ReadSignal>>, -) -> Element { - let mut json_text = - use_signal(|| serde_json::to_string_pretty(&*data.read()).unwrap_or_default()); - - let height = use_memo(move || { - let line_count = json_text().lines().count(); - let min_lines = 10; - let lines = line_count.max(min_lines); - // line-height is 1.5, font-size is 0.9rem (approx 14.4px), so each line is ~21.6px - // Add padding (1rem top + 1rem bottom = 2rem = 32px) - format!("{}px", lines * 22 + 32) - }); - - let validation = use_resource(move || { - let text = json_text(); - let nsid_val = nsid(); - let _ = schema(); // Track schema changes - - async move { - // Only validate if we have an NSID - let nsid_str = nsid_val?; - - // Parse JSON to Data - let parsed = match serde_json::from_str::(&text) { - Ok(val) => val.into_static(), - Err(e) => { - return Some((None, Some(e.to_string()))); - } - }; - - // Use global validator (schema already registered) - let validator = jacquard_lexicon::validation::SchemaValidator::global(); - let result = validator.validate_by_nsid(&nsid_str, &parsed); - - Some((Some(result), None)) - } - }); - - rsx! { - div { class: "json-editor", - textarea { - class: "json-textarea", - style: "height: {height};", - value: "{json_text}", - oninput: move |evt| { - json_text.set(evt.value()); - // Update data signal on successful parse - if let Ok(parsed) = serde_json::from_str::(&evt.value()) { - data.set(parsed.into_static()); - } - }, - } - - ValidationPanel { - validation: validation, - } - } - } -} - -#[component] -fn ActionButtons( - on_update: EventHandler<()>, - on_save_new: EventHandler<()>, - on_replace: EventHandler<()>, - on_delete: EventHandler<()>, - on_cancel: EventHandler<()>, -) -> Element { - let mut show_save_dropdown = use_signal(|| false); - let mut show_replace_warning = use_signal(|| false); - let mut show_delete_confirm = use_signal(|| false); - - rsx! { - div { class: "action-buttons-group", - button { - class: "tab-button action-button", - onclick: move |_| on_update.call(()), - "Update" - } - - div { class: "dropdown-wrapper", - button { - class: "tab-button action-button", - onclick: move |_| show_save_dropdown.toggle(), - "Save as New ▼" - } - if show_save_dropdown() { - div { class: "dropdown-menu", - button { - onclick: move |_| { - show_save_dropdown.set(false); - on_save_new.call(()); - }, - "Save as New" - } - button { - onclick: move |_| { - show_save_dropdown.set(false); - show_replace_warning.set(true); - }, - "Replace" - } - } - } - } - - if show_replace_warning() { - div { class: "inline-warning", - "⚠️ This will delete the current record and create a new one with a different rkey. " - button { - onclick: move |_| { - show_replace_warning.set(false); - on_replace.call(()); - }, - "Yes" - } - button { - onclick: move |_| show_replace_warning.set(false), - "No" - } - } - } - - button { - class: "tab-button action-button action-button-danger", - onclick: move |_| show_delete_confirm.set(true), - "Delete" - } - - DialogRoot { - open: Some(show_delete_confirm()), - on_open_change: move |open: bool| { - show_delete_confirm.set(open); - }, - DialogContent { - DialogTitle { "Delete Record?" } - DialogDescription { - "This action cannot be undone." - } - div { class: "dialog-actions", - button { - onclick: move |_| { - show_delete_confirm.set(false); - on_delete.call(()); - }, - "Delete" - } - button { - onclick: move |_| show_delete_confirm.set(false), - "Cancel" - } - } - } - } - - button { - class: "tab-button action-button", - onclick: move |_| on_cancel.call(()), - "Cancel" - } - } - } -} - -#[component] -fn ValidationPanel( - validation: Resource, Option)>>, -) -> Element { - rsx! { - div { class: "validation-panel", - if let Some(Some((result_opt, parse_error_opt))) = validation.read().as_ref() { - if let Some(parse_err) = parse_error_opt { - div { class: "parse-error", - "❌ Invalid JSON: {parse_err}" - } - } - - if let Some(result) = result_opt { - // Structural validity - if result.is_structurally_valid() { - div { class: "validation-success", "✓ Structurally valid" } - } else { - div { class: "parse-error", "❌ Structurally invalid" } - } - - // Overall validity - if result.is_valid() { - div { class: "validation-success", "✓ Fully valid" } - } else { - div { class: "validation-warning", "⚠ Has errors" } - } - - // Show errors if any - if !result.is_valid() { - div { class: "validation-errors", - h4 { "Validation Errors:" } - for error in result.all_errors() { - div { class: "error", "{error}" } - } - } - } - } - } else { - div { "Validating..." } - } - } - } -} - -// ============================================================================ -// Validation Helper Functions -// ============================================================================ - -/// Parse UI path into segments (fields and indices only) -fn parse_ui_path(ui_path: &str) -> Vec { - if ui_path.is_empty() { - return vec![]; - } - - let mut segments = Vec::new(); - let mut current = String::new(); - - for ch in ui_path.chars() { - match ch { - '.' => { - if !current.is_empty() { - segments.push(UiPathSegment::Field(current.clone())); - current.clear(); - } - } - '[' => { - if !current.is_empty() { - segments.push(UiPathSegment::Field(current.clone())); - current.clear(); - } - } - ']' => { - if !current.is_empty() { - if let Ok(idx) = current.parse::() { - segments.push(UiPathSegment::Index(idx)); - } - current.clear(); - } - } - c => current.push(c), - } - } - - if !current.is_empty() { - segments.push(UiPathSegment::Field(current)); - } - - segments -} - -#[derive(Debug, PartialEq)] -enum UiPathSegment { - Field(String), - Index(usize), -} - -/// Get all validation errors at exactly this path (not children) -fn get_errors_at_exact_path( - validation_result: &Option, - ui_path: &str, -) -> Vec { - use jacquard_lexicon::validation::PathSegment; - - if let Some(result) = validation_result { - let ui_segments = parse_ui_path(ui_path); - - result - .all_errors() - .filter_map(|err| { - let validation_path = match &err { - ValidationError::Structural(s) => match s { - StructuralError::TypeMismatch { path, .. } => Some(path), - StructuralError::MissingRequiredField { path, .. } => Some(path), - StructuralError::MissingUnionDiscriminator { path } => Some(path), - StructuralError::UnionNoMatch { path, .. } => Some(path), - StructuralError::UnresolvedRef { path, .. } => Some(path), - StructuralError::RefCycle { path, .. } => Some(path), - StructuralError::MaxDepthExceeded { path, .. } => Some(path), - }, - ValidationError::Constraint(c) => match c { - ConstraintError::MaxLength { path, .. } => Some(path), - ConstraintError::MaxGraphemes { path, .. } => Some(path), - ConstraintError::MinLength { path, .. } => Some(path), - ConstraintError::MinGraphemes { path, .. } => Some(path), - ConstraintError::Maximum { path, .. } => Some(path), - ConstraintError::Minimum { path, .. } => Some(path), - }, - }; - - if let Some(path) = validation_path { - // Convert validation path to UI segments - let validation_ui_segments: Vec<_> = path - .segments() - .iter() - .filter_map(|seg| match seg { - PathSegment::Field(name) => { - Some(UiPathSegment::Field(name.to_string())) - } - PathSegment::Index(idx) => Some(UiPathSegment::Index(*idx)), - PathSegment::UnionVariant(_) => None, - }) - .collect(); - - // Exact match only - if validation_ui_segments == ui_segments { - return Some(err.to_string()); - } - } - None - }) - .collect() - } else { - Vec::new() - } -} - -// ============================================================================ -// Pretty Editor: Helper Functions -// ============================================================================ - -/// Infer Data type from text input -fn infer_data_from_text(text: &str) -> Result, String> { - let trimmed = text.trim(); - - if trimmed == "true" || trimmed == "false" { - Ok(Data::Boolean(trimmed == "true")) - } else if trimmed == "{}" { - use jacquard::types::value::Object; - use std::collections::BTreeMap; - Ok(Data::Object(Object(BTreeMap::new()))) - } else if trimmed == "[]" { - use jacquard::types::value::Array; - Ok(Data::Array(Array(Vec::new()))) - } else if trimmed == "null" { - Ok(Data::Null) - } else if let Ok(num) = trimmed.parse::() { - Ok(Data::Integer(num)) - } else { - // Smart string parsing - use jacquard::types::value::parsing; - Ok(Data::String(parsing::parse_string(trimmed).into_static())) - } -} - -/// Parse text as specific AtprotoStr type, preserving type information -fn try_parse_as_type( - text: &str, - string_type: LexiconStringType, -) -> Result, String> { - use jacquard::types::string::*; - use std::str::FromStr; - - match string_type { - LexiconStringType::Datetime => Datetime::from_str(text) - .map(AtprotoStr::Datetime) - .map_err(|e| format!("Invalid datetime: {}", e)), - LexiconStringType::Did => Did::new(text) - .map(|v| AtprotoStr::Did(v.into_static())) - .map_err(|e| format!("Invalid DID: {}", e)), - LexiconStringType::Handle => Handle::new(text) - .map(|v| AtprotoStr::Handle(v.into_static())) - .map_err(|e| format!("Invalid handle: {}", e)), - LexiconStringType::AtUri => AtUri::new(text) - .map(|v| AtprotoStr::AtUri(v.into_static())) - .map_err(|e| format!("Invalid AT-URI: {}", e)), - LexiconStringType::AtIdentifier => AtIdentifier::new(text) - .map(|v| AtprotoStr::AtIdentifier(v.into_static())) - .map_err(|e| format!("Invalid identifier: {}", e)), - LexiconStringType::Nsid => Nsid::new(text) - .map(|v| AtprotoStr::Nsid(v.into_static())) - .map_err(|e| format!("Invalid NSID: {}", e)), - LexiconStringType::Tid => Tid::new(text) - .map(|v| AtprotoStr::Tid(v.into_static())) - .map_err(|e| format!("Invalid TID: {}", e)), - LexiconStringType::RecordKey => Rkey::new(text) - .map(|rk| AtprotoStr::RecordKey(RecordKey::from(rk))) - .map_err(|e| format!("Invalid record key: {}", e)), - LexiconStringType::Cid => Cid::new(text.as_bytes()) - .map(|v| AtprotoStr::Cid(v.into_static())) - .map_err(|_| "Invalid CID".to_string()), - LexiconStringType::Language => Language::new(text) - .map(AtprotoStr::Language) - .map_err(|e| format!("Invalid language: {}", e)), - LexiconStringType::Uri(_) => Uri::new(text) - .map(|u| AtprotoStr::Uri(u.into_static())) - .map_err(|e| format!("Invalid URI: {}", e)), - LexiconStringType::String => { - // Plain strings: use smart inference - use jacquard::types::value::parsing; - Ok(parsing::parse_string(text).into_static()) - } - } -} - -/// Create default value for new array item by cloning structure of existing items -fn create_array_item_default(arr: &jacquard::types::value::Array) -> Data<'static> { - if let Some(existing) = arr.0.first() { - clone_structure(existing) - } else { - // Empty array, default to null (user can change type) - Data::Null - } -} - -/// Clone structure of Data, setting sensible defaults for leaf values -fn clone_structure(data: &Data) -> Data<'static> { - use jacquard::types::string::*; - use jacquard::types::value::{Array, Object}; - use jacquard::types::{LexiconStringType, blob::*}; - use std::collections::BTreeMap; - - match data { - Data::Object(obj) => { - let mut new_obj = BTreeMap::new(); - for (key, value) in obj.0.iter() { - new_obj.insert(key.clone(), clone_structure(value)); - } - Data::Object(Object(new_obj)) - } - Data::Array(_) => Data::Array(Array(Vec::new())), - Data::String(s) => match s.string_type() { - LexiconStringType::Datetime => { - // Sensible default: now - Data::String(AtprotoStr::Datetime(Datetime::now())) - } - LexiconStringType::Tid => Data::String(AtprotoStr::Tid(Tid::now_0())), - _ => { - // Empty string, type inference will handle it - Data::String(AtprotoStr::String("".into())) - } - }, - Data::Integer(_) => Data::Integer(0), - Data::Boolean(_) => Data::Boolean(false), - Data::Blob(blob) => { - // Placeholder blob - Data::Blob( - Blob { - r#ref: CidLink::str( - "bafkreiaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - ), - mime_type: blob.mime_type.clone(), - size: 0, - } - .into_static(), - ) - } - Data::CidLink(_) => Data::CidLink(Cid::str( - "bafkreiaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - )), - Data::Bytes(_) => Data::Bytes(Bytes::new()), - Data::Null => Data::Null, - } -} - -// ============================================================================ -// Pretty Editor: Component Hierarchy -// ============================================================================ - -/// Main dispatcher - routes to specific field editors based on Data type -#[component] -fn EditableDataView( - root: Signal>, - path: String, - did: String, - #[props(default)] remove_button: Option, -) -> Element { - let path_for_memo = path.clone(); - let root_read = root.read(); - - match root_read - .get_at_path(&path_for_memo) - .map(|d| d.clone().into_static()) - { - Some(Data::Object(_)) => { - rsx! { EditableObjectField { root, path: path.clone(), did, remove_button } } - } - Some(Data::Array(_)) => rsx! { EditableArrayField { root, path: path.clone(), did } }, - Some(Data::String(_)) => { - rsx! { EditableStringField { root, path: path.clone(), remove_button } } - } - Some(Data::Integer(_)) => { - rsx! { EditableIntegerField { root, path: path.clone(), remove_button } } - } - Some(Data::Boolean(_)) => { - rsx! { EditableBooleanField { root, path: path.clone(), remove_button } } - } - Some(Data::Null) => rsx! { EditableNullField { root, path: path.clone(), remove_button } }, - Some(Data::Blob(_)) => { - rsx! { EditableBlobField { root, path: path.clone(), did, remove_button } } - } - Some(Data::Bytes(_)) => { - rsx! { EditableBytesField { root, path: path.clone(), remove_button } } - } - Some(Data::CidLink(_)) => { - rsx! { EditableCidLinkField { root, path: path.clone(), remove_button } } - } - - None => rsx! { div { class: "field-error", "❌ Path not found: {path}" } }, - } -} - -// ============================================================================ -// Primitive Field Editors -// ============================================================================ - -/// String field with type preservation -#[component] -fn EditableStringField( - root: Signal>, - path: String, - #[props(default)] remove_button: Option, -) -> Element { - use jacquard::types::LexiconStringType; - - let path_for_text = path.clone(); - let path_for_type = path.clone(); - - // Get current string value - let current_text = use_memo(move || { - root.read() - .get_at_path(&path_for_text) - .and_then(|d| d.as_str()) - .map(|s| s.to_string()) - .unwrap_or_default() - }); - - // Get string type (Copy, cheap to store) - let string_type = use_memo(move || { - root.read() - .get_at_path(&path_for_type) - .and_then(|d| match d { - Data::String(s) => Some(s.string_type()), - _ => None, - }) - .unwrap_or(LexiconStringType::String) - }); - - // Local state for invalid input - let mut input_text = use_signal(|| current_text()); - let mut parse_error = use_signal(|| None::); - - // Sync input when current changes - use_effect(move || { - input_text.set(current_text()); - }); - - let path_for_mutation = path.clone(); - let handle_input = move |evt: Event| { - let new_text = evt.value(); - input_text.set(new_text.clone()); - - match try_parse_as_type(&new_text, string_type()) { - Ok(new_atproto_str) => { - parse_error.set(None); - let mut new_data = root.read().clone(); - new_data.set_at_path(&path_for_mutation, Data::String(new_atproto_str)); - root.set(new_data); - } - Err(e) => { - parse_error.set(Some(e)); - } - } - }; - - let type_label = format!("{:?}", string_type()).to_lowercase(); - let is_plain_string = string_type() == LexiconStringType::String; - - // Dynamic width based on content length - let input_width = use_memo(move || { - let len = input_text().len(); - let min_width = match string_type() { - LexiconStringType::Cid => 60, - LexiconStringType::Nsid => 40, - LexiconStringType::Did => 50, - LexiconStringType::AtUri => 50, - _ => 20, - }; - format!("{}ch", len.max(min_width)) - }); - - rsx! { - div { class: "record-field", - div { class: "field-header", - PathLabel { path: path.clone() } - if type_label != "string" { - span { class: "string-type-tag", " [{type_label}]" } - } - {remove_button} - } - if is_plain_string { - textarea { - value: "{input_text}", - oninput: handle_input, - class: if parse_error().is_some() { "invalid" } else { "" }, - rows: "1", - } - } else { - input { - r#type: "text", - value: "{input_text}", - style: "width: {input_width}", - oninput: handle_input, - class: if parse_error().is_some() { "invalid" } else { "" }, - } - } - if let Some(err) = parse_error() { - span { class: "field-error", " ❌ {err}" } - } - } - } -} - -/// Integer field with validation -#[component] -fn EditableIntegerField( - root: Signal>, - path: String, - #[props(default)] remove_button: Option, -) -> Element { - let path_for_memo = path.clone(); - let current_value = use_memo(move || { - root.read() - .get_at_path(&path_for_memo) - .and_then(|d| d.as_integer()) - .unwrap_or(0) - }); - - let mut input_text = use_signal(|| current_value().to_string()); - let mut parse_error = use_signal(|| None::); - - use_effect(move || { - input_text.set(current_value().to_string()); - }); - - let path_for_mutation = path.clone(); - - rsx! { - div { class: "record-field", - div { class: "field-header", - PathLabel { path: path.clone() } - {remove_button} - } - input { - r#type: "number", - value: "{input_text}", - oninput: move |evt| { - let text = evt.value(); - input_text.set(text.clone()); - - match text.parse::() { - Ok(num) => { - parse_error.set(None); - let mut data_edit = root.write_unchecked(); - data_edit.set_at_path(&path_for_mutation, Data::Integer(num)); - } - Err(_) => { - parse_error.set(Some("Must be a valid integer".to_string())); - } - } - } - } - if let Some(err) = parse_error() { - span { class: "field-error", " ❌ {err}" } - } - } - } -} - -/// Boolean field (toggle button) -#[component] -fn EditableBooleanField( - root: Signal>, - path: String, - #[props(default)] remove_button: Option, -) -> Element { - let path_for_memo = path.clone(); - let current_value = use_memo(move || { - root.read() - .get_at_path(&path_for_memo) - .and_then(|d| d.as_boolean()) - .unwrap_or(false) - }); - - let path_for_mutation = path.clone(); - rsx! { - div { class: "record-field", - div { class: "field-header", - PathLabel { path: path.clone() } - {remove_button} - } - button { - class: if current_value() { "boolean-toggle boolean-toggle-true" } else { "boolean-toggle boolean-toggle-false" }, - onclick: move |_| { - root.with_mut(|data| { - if let Some(target) = data.get_at_path_mut(path_for_mutation.as_str()) { - if let Some(bool_val) = target.as_boolean() { - *target = Data::Boolean(!bool_val); - } - } - }); - }, - "{current_value()}" - } - } - } -} - -/// Null field with type inference -#[component] -fn EditableNullField( - root: Signal>, - path: String, - #[props(default)] remove_button: Option, -) -> Element { - let mut input_text = use_signal(|| String::new()); - let mut parse_error = use_signal(|| None::); - - let path_for_mutation = path.clone(); - rsx! { - div { class: "record-field", - div { class: "field-header", - PathLabel { path: path.clone() } - span { class: "field-value muted", "null" } - {remove_button} - } - input { - r#type: "text", - placeholder: "Enter value (or {{}}, [], true, 123)...", - value: "{input_text}", - oninput: move |evt| { - input_text.set(evt.value()); - }, - onkeydown: move |evt| { - use dioxus::prelude::keyboard_types::Key; - if evt.key() == Key::Enter { - let text = input_text(); - match infer_data_from_text(&text) { - Ok(new_value) => { - root.with_mut(|data| { - if let Some(target) = data.get_at_path_mut(path_for_mutation.as_str()) { - *target = new_value; - } - }); - input_text.set(String::new()); - parse_error.set(None); - } - Err(e) => { - parse_error.set(Some(e)); - } - } - } - } - } - if let Some(err) = parse_error() { - span { class: "field-error", " ❌ {err}" } - } - } - } -} - -/// Blob field - shows CID, size (editable), mime type (read-only), file upload -#[component] -fn EditableBlobField( - root: Signal>, - path: String, - did: String, - #[props(default)] remove_button: Option, -) -> Element { - let path_for_memo = path.clone(); - let blob_data = use_memo(move || { - root.read() - .get_at_path(&path_for_memo) - .and_then(|d| match d { - Data::Blob(blob) => Some(( - blob.r#ref.to_string(), - blob.size, - blob.mime_type.as_str().to_string(), - )), - _ => None, - }) - }); - - let mut cid_input = use_signal(|| String::new()); - let mut size_input = use_signal(|| String::new()); - let mut cid_error = use_signal(|| None::); - let mut size_error = use_signal(|| None::); - let mut uploading = use_signal(|| false); - let mut upload_error = use_signal(|| None::); - let mut preview_data_url = use_signal(|| None::); - - // Sync inputs when blob data changes - use_effect(move || { - if let Some((cid, size, _)) = blob_data() { - cid_input.set(cid); - size_input.set(size.to_string()); - } - }); - - let fetcher = use_context::(); - let path_for_upload = path.clone(); - let handle_file = move |evt: Event| { - let fetcher = fetcher.clone(); - let path_upload_clone = path_for_upload.clone(); - spawn(async move { - uploading.set(true); - upload_error.set(None); - - let files = evt.files(); - for file_data in files { - match file_data.read_bytes().await { - Ok(bytes_data) => { - // Convert to jacquard Bytes and sniff MIME type - let bytes = jacquard::bytes::Bytes::from(bytes_data.to_vec()); - let mime_str = bytes - .sniff_mime_type() - .unwrap_or("application/octet-stream"); - let mime_type = jacquard::types::blob::MimeType::new_owned(mime_str); - - // Create data URL for immediate preview if it's an image - if mime_str.starts_with("image/") { - let base64_data = base64::Engine::encode( - &base64::engine::general_purpose::STANDARD, - &bytes, - ); - let data_url = format!("data:{};base64,{}", mime_str, base64_data); - preview_data_url.set(Some(data_url.clone())); - - // Try to decode dimensions and populate aspectRatio field - #[cfg(target_arch = "wasm32")] - { - let path_clone = path_upload_clone.clone(); - spawn(async move { - if let Some((width, height)) = - decode_image_dimensions(&data_url).await - { - populate_aspect_ratio( - root, - &path_clone, - width as i64, - height as i64, - ); - } - }); - } - } - - // Upload blob - let client = fetcher.get_client(); - match client.upload_blob(bytes, mime_type).await { - Ok(new_blob) => { - // Update blob in record - let path_ref = path_upload_clone.clone(); - root.with_mut(|record_data| { - if let Some(Data::Blob(blob)) = - record_data.get_at_path_mut(&path_ref) - { - *blob = new_blob; - } - }); - upload_error.set(None); - } - Err(e) => { - upload_error.set(Some(format!("Upload failed: {:?}", e))); - } - } - } - Err(e) => { - upload_error.set(Some(format!("Failed to read file: {}", e))); - } - } - } - - uploading.set(false); - }); - }; - - let path_for_cid = path.clone(); - let handle_cid_change = move |evt: Event| { - let text = evt.value(); - cid_input.set(text.clone()); - - match jacquard::types::cid::CidLink::new_owned(text.as_bytes()) { - Ok(new_cid_link) => { - cid_error.set(None); - root.with_mut(|data| { - if let Some(Data::Blob(blob)) = data.get_at_path_mut(&path_for_cid) { - blob.r#ref = new_cid_link; - } - }); - } - Err(_) => { - cid_error.set(Some("Invalid CID format".to_string())); - } - } - }; - - let path_for_size = path.clone(); - let handle_size_change = move |evt: Event| { - let text = evt.value(); - size_input.set(text.clone()); - - match text.parse::() { - Ok(new_size) => { - size_input.set(format_size(new_size, humansize::BINARY)); - size_error.set(None); - root.with_mut(|data| { - if let Some(Data::Blob(blob)) = data.get_at_path_mut(&path_for_size) { - blob.size = new_size; - } - }); - } - Err(_) => { - size_error.set(Some("Must be a non-negative integer".to_string())); - } - } - }; - - let placeholder_cid = "bafkreiaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - let is_placeholder = blob_data() - .map(|(cid, _, _)| cid == placeholder_cid) - .unwrap_or(true); - let is_image = blob_data() - .map(|(_, _, mime)| mime.starts_with("image/")) - .unwrap_or(false); - - // Use preview data URL if available (fresh upload), otherwise CDN - let image_url = if let Some(data_url) = preview_data_url() { - Some(data_url) - } else if !is_placeholder && is_image { - blob_data().map(|(cid, _, mime)| { - let format = mime.strip_prefix("image/").unwrap_or("jpeg"); - format!( - "https://cdn.bsky.app/img/feed_fullsize/plain/{}/{}@{}", - did, cid, format - ) - }) - } else { - None - }; - - rsx! { - div { class: "record-field blob-field", - div { class: "field-header", - PathLabel { path: path.clone() } - span { class: "string-type-tag", " [blob]" } - {remove_button} - } - div { class: "blob-fields", - div { class: "blob-field-row blob-field-cid", - label { "CID:" } - input { - r#type: "text", - value: "{cid_input}", - oninput: handle_cid_change, - class: if cid_error().is_some() { "invalid" } else { "" }, - } - if let Some(err) = cid_error() { - span { class: "field-error", " ❌ {err}" } - } - } - div { class: "blob-field-row", - label { "Size:" } - input { - r#type: "number", - value: "{size_input}", - oninput: handle_size_change, - class: if size_error().is_some() { "invalid" } else { "" }, - } - if let Some(err) = size_error() { - span { class: "field-error", " ❌ {err}" } - } - } - div { class: "blob-field-row", - label { "MIME Type:" } - span { class: "readonly", - "{blob_data().map(|(_, _, mime)| mime).unwrap_or_default()}" - } - } - if let Some(url) = image_url { - img { - src: "{url}", - alt: "Blob preview", - class: "blob-image", - } - } - div { class: "blob-upload-section", - input { - r#type: "file", - accept: if is_image { "image/*" } else { "*/*" }, - onchange: handle_file, - disabled: uploading(), - } - if uploading() { - span { class: "upload-status", "Uploading..." } - } - if let Some(err) = upload_error() { - div { class: "field-error", "❌ {err}" } - } - } - } - } - } -} - -/// Decode image dimensions from data URL using browser Image API -#[cfg(target_arch = "wasm32")] -async fn decode_image_dimensions(data_url: &str) -> Option<(u32, u32)> { - use wasm_bindgen::JsCast; - use wasm_bindgen::prelude::*; - use wasm_bindgen_futures::JsFuture; - - let window = web_sys::window()?; - let document = window.document()?; - - let img = document.create_element("img").ok()?; - let img = img.dyn_into::().ok()?; - - img.set_src(data_url); - - // Wait for image to load - let promise = js_sys::Promise::new(&mut |resolve, _reject| { - let onload = Closure::wrap(Box::new(move || { - resolve.call0(&JsValue::NULL).ok(); - }) as Box); - - img.set_onload(Some(onload.as_ref().unchecked_ref())); - onload.forget(); - }); - - JsFuture::from(promise).await.ok()?; - - Some((img.natural_width(), img.natural_height())) -} - -/// Find and populate aspectRatio field for a blob -#[allow(unused)] -fn populate_aspect_ratio( - mut root: Signal>, - blob_path: &str, - width: i64, - height: i64, -) { - // Query for all aspectRatio fields and collect the path we want - let aspect_path_to_update = { - let data = root.read(); - let query_result = data.query("...aspectRatio"); - - query_result.multiple().and_then(|matches| { - // Find aspectRatio that's a sibling of our blob - // e.g. blob at "embed.images[0].image" -> look for "embed.images[0].aspectRatio" - let blob_parent = blob_path.rsplit_once('.').map(|(parent, _)| parent); - matches.iter().find_map(|query_match| { - let aspect_parent = query_match.path.rsplit_once('.').map(|(parent, _)| parent); - - // Check if they share the same parent - if blob_parent == aspect_parent { - Some(query_match.path.clone()) - } else { - None - } - }) - }) - }; - - // Update the aspectRatio if we found a matching field - if let Some(aspect_path) = aspect_path_to_update { - let aspect_obj = atproto! {{ - "width": width, - "height": height - }}; - - root.with_mut(|record_data| { - record_data.set_at_path(&aspect_path, aspect_obj); - }); - } -} - -/// Bytes field with hex/base64 auto-detection -#[component] -fn EditableBytesField( - root: Signal>, - path: String, - #[props(default)] remove_button: Option, -) -> Element { - let path_for_memo = path.clone(); - let current_bytes = use_memo(move || { - root.read() - .get_at_path(&path_for_memo) - .and_then(|d| match d { - Data::Bytes(b) => Some(bytes_to_hex(b)), - _ => None, - }) - }); - - let mut input_text = use_signal(|| String::new()); - let mut parse_error = use_signal(|| None::); - let mut detected_format = use_signal(|| None::); - - // Sync input when bytes change - use_effect(move || { - if let Some(hex) = current_bytes() { - input_text.set(hex); - } - }); - - let path_for_mutation = path.clone(); - let handle_input = move |evt: Event| { - let text = evt.value(); - input_text.set(text.clone()); - - match parse_bytes_input(&text) { - Ok((bytes, format)) => { - parse_error.set(None); - detected_format.set(Some(format)); - root.with_mut(|data| { - if let Some(target) = data.get_at_path_mut(&path_for_mutation) { - *target = Data::Bytes(bytes); - } - }); - } - Err(e) => { - parse_error.set(Some(e)); - detected_format.set(None); - } - } - }; - - let byte_count = current_bytes() - .map(|hex| hex.chars().filter(|c| c.is_ascii_hexdigit()).count() / 2) - .unwrap_or(0); - let size_label = if byte_count > 128 { - format_size(byte_count, humansize::BINARY) - } else { - format!("{} bytes", byte_count) - }; - - rsx! { - div { class: "record-field bytes-field", - div { class: "field-header", - PathLabel { path: path.clone() } - span { class: "string-type-tag", " [bytes: {size_label}]" } - if let Some(format) = detected_format() { - span { class: "bytes-format-tag", " ({format})" } - } - {remove_button} - } - textarea { - value: "{input_text}", - placeholder: "Paste hex (1a2b3c...) or base64 (YWJj...)", - oninput: handle_input, - class: if parse_error().is_some() { "invalid" } else { "" }, - rows: "3", - } - if let Some(err) = parse_error() { - span { class: "field-error", " ❌ {err}" } - } - } - } -} - -/// Parse bytes from hex or base64, auto-detecting format -fn parse_bytes_input(text: &str) -> Result<(jacquard::bytes::Bytes, String), String> { - let trimmed = text.trim(); - if trimmed.is_empty() { - return Err("Input is empty".to_string()); - } - - // Remove common whitespace/separators - let cleaned: String = trimmed - .chars() - .filter(|c| !c.is_whitespace() && *c != ':' && *c != '-') - .collect(); - - // Try hex first (more restrictive) - if cleaned.chars().all(|c| c.is_ascii_hexdigit()) { - parse_hex_bytes(&cleaned).map(|b| (b, "hex".to_string())) - } else { - // Try base64 - parse_base64_bytes(&cleaned).map(|b| (b, "base64".to_string())) - } -} - -/// Parse hex string to bytes -fn parse_hex_bytes(hex: &str) -> Result { - if hex.len() % 2 != 0 { - return Err("Hex string must have even length".to_string()); - } - - let mut bytes = Vec::with_capacity(hex.len() / 2); - for chunk in hex.as_bytes().chunks(2) { - let hex_byte = std::str::from_utf8(chunk).map_err(|e| format!("Invalid UTF-8: {}", e))?; - let byte = - u8::from_str_radix(hex_byte, 16).map_err(|e| format!("Invalid hex digit: {}", e))?; - bytes.push(byte); - } - - Ok(jacquard::bytes::Bytes::from(bytes)) -} - -/// Parse base64 string to bytes -fn parse_base64_bytes(b64: &str) -> Result { - use base64::Engine; - let engine = base64::engine::general_purpose::STANDARD; - - engine - .decode(b64) - .map(jacquard::bytes::Bytes::from) - .map_err(|e| format!("Invalid base64: {}", e)) -} - -/// Convert bytes to hex display string (with spacing every 4 chars) -fn bytes_to_hex(bytes: &jacquard::bytes::Bytes) -> String { - bytes - .iter() - .enumerate() - .map(|(i, b)| { - let hex = format!("{:02x}", b); - if i > 0 && i % 2 == 0 { - format!(" {}", hex) - } else { - hex - } - }) - .collect() -} - -/// CidLink field with validation -#[component] -fn EditableCidLinkField( - root: Signal>, - path: String, - #[props(default)] remove_button: Option, -) -> Element { - let path_for_memo = path.clone(); - let current_cid = use_memo(move || { - root.read() - .get_at_path(&path_for_memo) - .map(|d| match d { - Data::CidLink(cid) => cid.to_string(), - _ => String::new(), - }) - .unwrap_or_default() - }); - - let mut input_text = use_signal(|| String::new()); - let mut parse_error = use_signal(|| None::); - - use_effect(move || { - input_text.set(current_cid()); - }); - - let input_width = use_memo(move || { - let len = input_text().len(); - format!("{}ch", len.max(60)) - }); - - let path_for_mutation = path.clone(); - let handle_input = move |evt: Event| { - let text = evt.value(); - input_text.set(text.clone()); - - match jacquard::types::cid::Cid::new_owned(text.as_bytes()) { - Ok(new_cid) => { - parse_error.set(None); - root.with_mut(|data| { - if let Some(target) = data.get_at_path_mut(&path_for_mutation) { - *target = Data::CidLink(new_cid); - } - }); - } - Err(_) => { - parse_error.set(Some("Invalid CID format".to_string())); - } - } - }; - - rsx! { - div { class: "record-field cidlink-field", - div { class: "field-header", - PathLabel { path: path.clone() } - span { class: "string-type-tag", " [cid-link]" } - {remove_button} - } - input { - r#type: "text", - value: "{input_text}", - style: "width: {input_width}", - placeholder: "bafyrei...", - oninput: handle_input, - class: if parse_error().is_some() { "invalid" } else { "" }, - } - if let Some(err) = parse_error() { - span { class: "field-error", " ❌ {err}" } - } - } - } -} - -// ============================================================================ -// Field with Remove Button Wrapper -// ============================================================================ - -/// Wraps a field with an optional remove button in the header -#[component] -fn FieldWithRemove( - root: Signal>, - path: String, - did: String, - is_removable: bool, - parent_path: String, - field_key: String, -) -> Element { - let remove_button = if is_removable { - Some(rsx! { - button { - class: "field-remove-button", - onclick: move |_| { - let mut new_data = root.read().clone(); - if let Some(Data::Object(obj)) = new_data.get_at_path_mut(parent_path.as_str()) { - obj.0.remove(field_key.as_str()); - } - root.set(new_data); - }, - "Remove" - } - }) - } else { - None - }; - - rsx! { - EditableDataView { - root: root, - path: path.clone(), - did: did.clone(), - remove_button: remove_button, - } - } -} - -// ============================================================================ -// Array Field Editor (enables recursion) -// ============================================================================ - -/// Array field - iterates items and renders child EditableDataView for each -#[component] -fn EditableArrayField(root: Signal>, path: String, did: String) -> Element { - let path_for_memo = path.clone(); - let array_len = use_memo(move || { - root.read() - .get_at_path(&path_for_memo) - .and_then(|d| d.as_array()) - .map(|arr| arr.0.len()) - .unwrap_or(0) - }); - - let path_for_add = path.clone(); - - rsx! { - div { class: "record-section array-section", - Accordion { - id: "edit-array-{path}", - collapsible: true, - AccordionItem { - default_open: true, - index: 0, - AccordionTrigger { - div { class: "section-header", - div { class: "section-label", - { - let parts: Vec<&str> = path.split('.').collect(); - let final_part = parts.last().unwrap_or(&""); - rsx! { "{final_part}" } - } - } - span { class: "array-length", "[{array_len}]" } - } - } - AccordionContent { - div { class: "section-content", - for idx in 0..array_len() { - { - let item_path = format!("{}[{}]", path, idx); - let path_for_remove = path.clone(); - - rsx! { - div { - class: "array-item", - key: "{item_path}", - - EditableDataView { - root: root, - path: item_path.clone(), - did: did.clone(), - remove_button: rsx! { - button { - class: "field-remove-button", - onclick: move |_| { - root.with_mut(|data| { - if let Some(Data::Array(arr)) = data.get_at_path_mut(&path_for_remove) { - arr.0.remove(idx); - } - }); - }, - "Remove" - } - } - } - } - } - } - } - div { - class: "array-item", - div { - class: "add-field-widget", - button { - onclick: move |_| { - root.with_mut(|data| { - if let Some(Data::Array(arr)) = data.get_at_path_mut(&path_for_add) { - let new_item = create_array_item_default(arr); - arr.0.push(new_item); - } - }); - }, - "+ Add Item" - } - } - } - } - } - } - } - } - } -} - -// ============================================================================ -// Object Field Editor (enables recursion) -// ============================================================================ - -/// Object field - iterates fields and renders child EditableDataView for each -#[component] -fn EditableObjectField( - root: Signal>, - path: String, - did: String, - #[props(default)] remove_button: Option, -) -> Element { - let path_for_memo = path.clone(); - let field_keys = use_memo(move || { - root.read() - .get_at_path(&path_for_memo) - .and_then(|d| d.as_object()) - .map(|obj| obj.0.keys().cloned().collect::>()) - .unwrap_or_default() - }); - - let is_root = path.is_empty(); - - rsx! { - if !is_root { - div { class: "record-section object-section", - Accordion { - id: "edit-object-{path}", - collapsible: true, - AccordionItem { - default_open: true, - index: 0, - AccordionTrigger { - div { class: "section-header", - div { class: "section-label", - { - let parts: Vec<&str> = path.split('.').collect(); - let final_part = parts.last().unwrap_or(&""); - rsx! { "{final_part}" } - } - } - {remove_button} - } - } - AccordionContent { - div { class: "section-content", - for key in field_keys() { - { - let field_path = if path.is_empty() { - key.to_string() - } else { - format!("{}.{}", path, key) - }; - let is_type_field = key == "$type"; - - rsx! { - FieldWithRemove { - key: "{field_path}", - root: root, - path: field_path.clone(), - did: did.clone(), - is_removable: !is_type_field, - parent_path: path.clone(), - field_key: key.clone(), - } - } - } - } - - AddFieldWidget { root: root, path: path.clone() } - } - } - } - } - } - } else { - for key in field_keys() { - { - let field_path = key.to_string(); - let is_type_field = key == "$type"; - - rsx! { - FieldWithRemove { - key: "{field_path}", - root: root, - path: field_path.clone(), - did: did.clone(), - is_removable: !is_type_field, - parent_path: path.clone(), - field_key: key.clone(), - } - } - } - } - - AddFieldWidget { root: root, path: path.clone() } - } - } -} - -/// Widget for adding new fields to objects -#[component] -fn AddFieldWidget(root: Signal>, path: String) -> Element { - let mut field_name = use_signal(|| String::new()); - let mut field_value = use_signal(|| String::new()); - let mut error = use_signal(|| None::); - let mut show_form = use_signal(|| false); - - let path_for_enter = path.clone(); - let path_for_button = path.clone(); - - rsx! { - div { class: "add-field-widget", - if !show_form() { - button { - class: "add-button", - onclick: move |_| show_form.set(true), - "+ Add Field" - } - } else { - div { class: "add-field-form", - input { - r#type: "text", - placeholder: "Field name", - value: "{field_name}", - oninput: move |evt| field_name.set(evt.value()), - } - input { - r#type: "text", - placeholder: r#"Value: {{}}, [], true, 123, "text""#, - value: "{field_value}", - oninput: move |evt| field_value.set(evt.value()), - onkeydown: move |evt| { - use dioxus::prelude::keyboard_types::Key; - if evt.key() == Key::Enter { - let name = field_name(); - let value_text = field_value(); - - if name.is_empty() { - error.set(Some("Field name required".to_string())); - return; - } - - let new_value = match infer_data_from_text(&value_text) { - Ok(data) => data, - Err(e) => { - error.set(Some(e)); - return; - } - }; - - let mut new_data = root.read().clone(); - if let Some(Data::Object(obj)) = new_data.get_at_path_mut(path_for_enter.as_str()) { - obj.0.insert(name.into(), new_value); - } - root.set(new_data); - - // Reset form - field_name.set(String::new()); - field_value.set(String::new()); - show_form.set(false); - error.set(None); - } - } - } - button { - class: "add-field-widget-edit", - onclick: move |_| { - let name = field_name(); - let value_text = field_value(); - - if name.is_empty() { - error.set(Some("Field name required".to_string())); - return; - } - - let new_value = match infer_data_from_text(&value_text) { - Ok(data) => data, - Err(e) => { - error.set(Some(e)); - return; - } - }; - - let mut new_data = root.read().clone(); - if let Some(Data::Object(obj)) = new_data.get_at_path_mut(path_for_button.as_str()) { - obj.0.insert(name.into(), new_value); - } - root.set(new_data); - - // Reset form - field_name.set(String::new()); - field_value.set(String::new()); - show_form.set(false); - error.set(None); - }, - "Add" - } - button { - class: "add-field-widget-edit", - onclick: move |_| { - show_form.set(false); - field_name.set(String::new()); - field_value.set(String::new()); - error.set(None); - }, - "Cancel" - } - if let Some(err) = error() { - div { class: "field-error", "❌ {err}" } - } - } - } - } - } -} - -/// Layout component for record view - handles header, metadata, and wraps children -#[component] -fn RecordViewLayout( - uri: AtUri<'static>, - cid: Option>, - schema: ReadSignal>>, - record_value: Data<'static>, - children: Element, -) -> Element { - // Validate the record if schema is available - let validation_status = use_memo(move || { - let _schema_doc = schema()?; - let nsid_str = record_value.type_discriminator()?; - - let validator = jacquard_lexicon::validation::SchemaValidator::global(); - let result = validator.validate_by_nsid(nsid_str, &record_value); - - Some(result.is_valid()) - }); - - rsx! { - div { - class: "record-metadata", - div { class: "metadata-row", - span { class: "metadata-label", "URI" } - span { class: "metadata-value", - HighlightedUri { uri: uri.clone() } - } - } - if let Some(cid) = cid { - div { class: "metadata-row", - span { class: "metadata-label", "CID" } - code { class: "metadata-value", "{cid}" } - } - } - if let Some(is_valid) = validation_status() { - div { class: "metadata-row", - span { class: "metadata-label", "Schema" } - span { - class: if is_valid { "metadata-value schema-valid" } else { "metadata-value schema-invalid" }, - if is_valid { "Valid" } else { "Invalid" } - } - } - } - } - - {children} - - } -} - -/// Render some text as markdown. -#[component] -fn EditableRecordContent( - record_value: Data<'static>, - uri: ReadSignal>, - view_mode: Signal, - edit_mode: Signal, - record_resource: Resource, AgentError>>, - schema: ReadSignal>>, -) -> Element { - let mut edit_data = use_signal(use_reactive!(|record_value| record_value.clone())); - let nsid = use_memo(move || edit_data().type_discriminator().map(|s| s.to_string())); - let navigator = use_navigator(); - let fetcher = use_context::(); - - // Validate edit_data whenever it changes and provide via context - let mut validation_result = use_signal(|| None); - use_effect(move || { - let _ = schema(); // Track schema changes - if let Some(nsid_str) = nsid() { - let data = edit_data(); - let validator = jacquard_lexicon::validation::SchemaValidator::global(); - let result = validator.validate_by_nsid(&nsid_str, &data); - validation_result.set(Some(result)); - } - }); - use_context_provider(|| validation_result); - - let update_fetcher = fetcher.clone(); - let create_fetcher = fetcher.clone(); - let replace_fetcher = fetcher.clone(); - let delete_fetcher = fetcher.clone(); - - rsx! { - div { - class: "tab-bar", - button { - class: if view_mode() == ViewMode::Pretty { "tab-button active" } else { "tab-button" }, - onclick: move |_| view_mode.set(ViewMode::Pretty), - "View" - } - button { - class: if view_mode() == ViewMode::Json { "tab-button active" } else { "tab-button" }, - onclick: move |_| view_mode.set(ViewMode::Json), - "JSON" - } - button { - class: if view_mode() == ViewMode::Schema { "tab-button active" } else { "tab-button" }, - onclick: move |_| view_mode.set(ViewMode::Schema), - "Schema" - } - ActionButtons { - on_update: move |_| { - let fetcher = update_fetcher.clone(); - let uri = uri(); - let data = edit_data(); - spawn(async move { - if let Some((did, _)) = fetcher.session_info().await { - if let (Some(collection_str), Some(rkey)) = (uri.collection(), uri.rkey()) { - let collection = Nsid::new(collection_str.as_str()).ok(); - if let Some(collection) = collection { - let request = PutRecord::new() - .repo(AtIdentifier::Did(did)) - .collection(collection) - .rkey(rkey.clone()) - .record(data.clone()) - .build(); - - match fetcher.send(request).await { - Ok(output) => { - if output.status() == StatusCode::OK { - tracing::info!("Record updated successfully"); - edit_data.set(data.clone()); - edit_mode.set(false); - } else { - tracing::error!("Unexpected status code: {:?}", output.status()); - } - } - Err(e) => { - tracing::error!("Failed to update record: {:?}", e); - } - } - } - } - } - }); - }, - on_save_new: move |_| { - let fetcher = create_fetcher.clone(); - let data = edit_data(); - let nav = navigator.clone(); - spawn(async move { - if let Some((did, _)) = fetcher.session_info().await { - if let Some(collection_str) = data.type_discriminator() { - let collection = Nsid::new(collection_str).ok(); - if let Some(collection) = collection { - let request = CreateRecord::new() - .repo(AtIdentifier::Did(did)) - .collection(collection) - .record(data.clone()) - .build(); - - match fetcher.send(request).await { - Ok(response) => { - if let Ok(output) = response.into_output() { - tracing::info!("Record created: {}", output.uri); - let link = format!("{}/record/{}", crate::env::WEAVER_APP_DOMAIN, output.uri); - nav.push(link); - } - } - Err(e) => { - tracing::error!("Failed to create record: {:?}", e); - } - } - } - } - } - }); - }, - on_replace: move |_| { - let fetcher = replace_fetcher.clone(); - let uri = uri(); - let data = edit_data(); - let nav = navigator.clone(); - spawn(async move { - if let Some((did, _)) = fetcher.session_info().await { - if let Some(new_collection_str) = data.type_discriminator() { - let new_collection = Nsid::new(new_collection_str).ok(); - if let Some(new_collection) = new_collection { - // Create new record - let create_req = CreateRecord::new() - .repo(AtIdentifier::Did(did.clone())) - .collection(new_collection) - .record(data.clone()) - .build(); - - match fetcher.send(create_req).await { - Ok(response) => { - if let Ok(create_output) = response.into_output() { - // Delete old record - if let (Some(old_collection_str), Some(old_rkey)) = (uri.collection(), uri.rkey()) { - let old_collection = Nsid::new(old_collection_str.as_str()).ok(); - if let Some(old_collection) = old_collection { - let delete_req = DeleteRecord::new() - .repo(AtIdentifier::Did(did)) - .collection(old_collection) - .rkey(old_rkey.clone()) - .build(); - - if let Err(e) = fetcher.send(delete_req).await { - tracing::warn!("Created new record but failed to delete old: {:?}", e); - } - } - } - - tracing::info!("Record replaced: {}", create_output.uri); - let link = format!("{}/record/{}", crate::env::WEAVER_APP_DOMAIN, create_output.uri); - nav.push(link); - } - } - Err(e) => { - tracing::error!("Failed to replace record: {:?}", e); - } - } - } - } - } - }); - }, - on_delete: move |_| { - let fetcher = delete_fetcher.clone(); - let uri = uri(); - let nav = navigator.clone(); - spawn(async move { - if let Some((did, _)) = fetcher.session_info().await { - if let (Some(collection_str), Some(rkey)) = (uri.collection(), uri.rkey()) { - let collection = Nsid::new(collection_str.as_str()).ok(); - if let Some(collection) = collection { - let request = DeleteRecord::new() - .repo(AtIdentifier::Did(did)) - .collection(collection) - .rkey(rkey.clone()) - .build(); - - match fetcher.send(request).await { - Ok(_) => { - tracing::info!("Record deleted"); - nav.push(Route::Home {}); - } - Err(e) => { - tracing::error!("Failed to delete record: {:?}", e); - } - } - } - } - } - }); - }, - on_cancel: move |_| { - edit_data.set(record_value.clone()); - edit_mode.set(false); - }, - } - } - div { - class: "tab-content", - match view_mode() { - ViewMode::Pretty => rsx! { - div { class: "pretty-record", - EditableDataView { - root: edit_data, - path: String::new(), - did: uri().authority().to_string(), - } - } - }, - ViewMode::Json => rsx! { - JsonEditor { data: edit_data, nsid, schema } - }, - ViewMode::Schema => rsx! { - SchemaView { schema } - }, - } - } - } -} - -#[component] -pub fn CodeView(props: CodeViewProps) -> Element { - let code = &*props.code.read(); - - let mut html_buf = String::new(); - highlight_code(props.lang.as_deref(), code, &mut html_buf).unwrap(); - - rsx! { - document::Style { {generate_default_css().unwrap()}} - div { - id: "{&*props.id.read()}", - class: "{&*props.class.read()}", - dangerous_inner_html: "{html_buf}" - } - } -} diff --git a/crates/weaver-cli/Cargo.toml b/crates/weaver-cli/Cargo.toml index e8cfc05..54a30bd 100644 --- a/crates/weaver-cli/Cargo.toml +++ b/crates/weaver-cli/Cargo.toml @@ -17,7 +17,6 @@ weaver-api = { path = "../weaver-api" } miette = { workspace = true, features = ["fancy"] } jacquard = { workspace = true, features = ["loopback", "dns"] } -jacquard-api = { workspace = true, features = ["sh_weaver"] } markdown-weaver = { workspace = true } markdown-weaver-escape = { workspace = true } diff --git a/crates/weaver-common/Cargo.toml b/crates/weaver-common/Cargo.toml index 90f50f0..7ca74ca 100644 --- a/crates/weaver-common/Cargo.toml +++ b/crates/weaver-common/Cargo.toml @@ -14,44 +14,30 @@ native = ["jacquard/dns"] n0-future = { workspace = true } weaver-api = { version = "0.1", path = "../weaver-api" } markdown-weaver = { workspace = true } -#libsqlite3-sys = { version = "0.30.1", features = ["bundled"] } - http = "1.3.1" - jacquard = { workspace = true } jacquard-common = { workspace = true } trait-variant = "0.1" - serde = { workspace = true } serde_json = { version = "1.0.140", features = ["preserve_order", "raw_value"] } -serde_ipld_dagcbor = { version = "0.6.1", features = ["codec"] } -serde_html_form = "0.2.7" -serde_bytes = "0.11.17" tokio = { version = "1.44", features = ["sync"] } - -minijinja = { workspace = true, features = [ - "builtins", - "debug", - "deserialization", - "macros", - "multi_template", - "adjacent_loop_items", - "std_collections", - "serde", -] } - - miette = { workspace = true } -owo-colors = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } -reqwest = "0.12.15" -regex = "1.11.1" +reqwest = { version = "0.12", default-features = false, features = [ + "json", + "rustls-tls", +] } + markdown-weaver-escape = { workspace = true, features = ["std"] } mime-sniffer = "^0.1" +[target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dependencies] +regex = "1.11.1" + # wasm-in-browser dependencies [target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies] +regex-lite = { version = "0.1" } futures-util = "0.3" js-sys = "0.3" pin-project = "1" diff --git a/crates/weaver-common/src/agent.rs b/crates/weaver-common/src/agent.rs index ccf039e..90612af 100644 --- a/crates/weaver-common/src/agent.rs +++ b/crates/weaver-common/src/agent.rs @@ -624,7 +624,7 @@ pub trait WeaverExt: AgentSessionExt + XrpcExt { .map(|blob| { let cid = blob.blob().cid(); jacquard::types::string::Uri::new_owned(format!( - "https://cdn.bsky.app/img/avatar/plain/{}/{}", + "https://cdn.bsky.app/img/avatar/plain/{}/{}@jpeg", did, cid )) }) @@ -639,7 +639,7 @@ pub trait WeaverExt: AgentSessionExt + XrpcExt { .map(|blob| { let cid = blob.blob().cid(); jacquard::types::string::Uri::new_owned(format!( - "https://cdn.bsky.app/img/banner/plain/{}/{}", + "https://cdn.bsky.app/img/banner/plain/{}/{}@jpeg", did, cid )) }) @@ -728,7 +728,7 @@ pub trait WeaverExt: AgentSessionExt + XrpcExt { .map(|blob| { let cid = blob.blob().cid(); jacquard::types::string::Uri::new_owned(format!( - "https://cdn.bsky.app/img/avatar/plain/{}/{}", + "https://cdn.bsky.app/img/avatar/plain/{}/{}@jpeg", did, cid )) }) @@ -743,7 +743,7 @@ pub trait WeaverExt: AgentSessionExt + XrpcExt { .map(|blob| { let cid = blob.blob().cid(); jacquard::types::string::Uri::new_owned(format!( - "https://cdn.bsky.app/img/feed_fullsize/plain/{}/{}", + "https://cdn.bsky.app/img/banner/plain/{}/{}@jpeg", did, cid )) }) diff --git a/crates/weaver-common/src/error.rs b/crates/weaver-common/src/error.rs index d025911..c72001a 100644 --- a/crates/weaver-common/src/error.rs +++ b/crates/weaver-common/src/error.rs @@ -103,8 +103,6 @@ impl ParseError { pub enum ParseErrorKind { #[error(transparent)] SerdeError(#[from] SerDeError), - #[error(transparent)] - MiniJinjaError(#[from] minijinja::Error), #[error("error in markdown parsing or rendering: {0}")] MarkdownError(markdown_weaver::CowStr<'static>), } @@ -116,10 +114,6 @@ pub enum SerDeError { #[error(transparent)] #[diagnostic_source] Json(#[from] serde_json::Error), - #[error(transparent)] - DagCbor(#[from] serde_ipld_dagcbor::error::CodecError), - #[error(transparent)] - HtmlForm(#[from] serde_html_form::ser::Error), } impl From for ParseError { diff --git a/crates/weaver-index/Cargo.toml b/crates/weaver-index/Cargo.toml index 4feab39..d9b0477 100644 --- a/crates/weaver-index/Cargo.toml +++ b/crates/weaver-index/Cargo.toml @@ -24,7 +24,6 @@ serde_json = "1.0.128" uuid = { version = "1.8.0", features = ["v7", "serde"] } jacquard = { workspace = true, features = ["websocket", "zstd"] } -jacquard-api = { workspace = true, features = ["sh_weaver"] } jacquard-axum = { workspace = true } axum = "0.8" diff --git a/crates/weaver-renderer/Cargo.toml b/crates/weaver-renderer/Cargo.toml index 47e703e..17d96f2 100644 --- a/crates/weaver-renderer/Cargo.toml +++ b/crates/weaver-renderer/Cargo.toml @@ -11,23 +11,19 @@ weaver-common = { path = "../weaver-common" } weaver-api = { path = "../weaver-api" } jacquard.workspace = true markdown-weaver = { workspace = true } -compact_string = "0.1.0" http = "1.3.1" url = "2.5.4" syntect = { workspace = true, default-features = false, features = ["default-fancy"]} markdown-weaver-escape = { workspace = true, features = ["std"] } thiserror.workspace = true +tracing.workspace = true miette.workspace = true unicode-normalization = "0.1.24" yaml-rust2 = { version = "0.10.2" } bitflags = "2.9.1" -tracing = "0.1" - dashmap = "6.1.0" -regex = "1.11.1" pin-utils = "0.1.0" pin-project = "1.1.10" -dynosaur = "0.2.0" smol_str = { version = "0.3", features = ["serde"] } mime-sniffer = "0.1.3" reqwest = { version = "0.12.7", default-features = false, features = [ @@ -36,9 +32,13 @@ reqwest = { version = "0.12.7", default-features = false, features = [ ] } [target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dependencies] +regex = { version = "1.12" } tokio = { version = "1.28", features = ["rt", "time"] } tokio-util = { version = "0.7.14", features = ["rt"] } ignore = "0.4.23" +[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies] +regex-lite = { version = "0.1" } + [dev-dependencies] insta = { version = "1.40", features = ["yaml"] } diff --git a/crates/weaver-renderer/src/atproto/client.rs b/crates/weaver-renderer/src/atproto/client.rs index 1e8922b..8dd661d 100644 --- a/crates/weaver-renderer/src/atproto/client.rs +++ b/crates/weaver-renderer/src/atproto/client.rs @@ -203,6 +203,7 @@ impl<'a, R: EmbedResolver> ClientContext<'a, R> { if let Some(embeds) = &entry.embeds { if let Some(images) = &embeds.images { for img in &images.images { + tracing::info!("Image: {:?}", img); if let Some(name) = &img.name { let blob_name = BlobName::from_filename(name.as_ref()); map.insert(blob_name, img.image.blob().cid().clone().into_static()); @@ -227,13 +228,13 @@ impl<'a, R: EmbedResolver> ClientContext<'a, R> { /// - Bluesky list: `at://{actor}/app.bsky.graph.list/{rkey}` → `https://bsky.app/profile/{actor}/lists/{rkey}` /// - Bluesky feed: `at://{actor}/app.bsky.feed.generator/{rkey}` → `https://bsky.app/profile/{actor}/feed/{rkey}` /// - Bluesky starterpack: `at://{actor}/app.bsky.graph.starterpack/{rkey}` → `https://bsky.app/starter-pack/{actor}/{rkey}` -/// - Weaver/other: `at://{actor}/{collection}/{rkey}` → `https://weaver.sh/{actor}/{collection}/{rkey}` +/// - Weaver/other: `at://{actor}/{collection}/{rkey}` → `https://weaver.sh/record/{at_uri}` fn at_uri_to_web_url(at_uri: &AtUri<'_>) -> String { let authority = at_uri.authority().as_ref(); // Profile-only link (no collection/rkey) if at_uri.collection().is_none() && at_uri.rkey().is_none() { - return format!("https://weaver.sh/{}", authority); + return format!("https://alpha.weaver.sh/{}", authority); } // Record link @@ -257,15 +258,12 @@ fn at_uri_to_web_url(at_uri: &AtUri<'_>) -> String { } // Weaver records and unknown collections go to weaver.sh _ => { - format!( - "https://weaver.sh/{}/{}/{}", - authority, collection_str, rkey_str - ) + format!("https://alpha.weaver.sh/record/{}", at_uri) } } } else { // Fallback for malformed URIs - format!("https://weaver.sh/{}", authority) + format!("https://alpha.weaver.sh/{}", authority) } } diff --git a/crates/weaver-renderer/src/atproto/writer.rs b/crates/weaver-renderer/src/atproto/writer.rs index ff1523c..c0be02c 100644 --- a/crates/weaver-renderer/src/atproto/writer.rs +++ b/crates/weaver-renderer/src/atproto/writer.rs @@ -50,7 +50,9 @@ enum TableState { Body, } -impl<'a, I: Iterator>, W: StrWrite, E: EmbedContentProvider> ClientWriter<'a, I, W, E> { +impl<'a, I: Iterator>, W: StrWrite, E: EmbedContentProvider> + ClientWriter<'a, I, W, E> +{ pub fn new(events: I, writer: W) -> Self { Self { events, @@ -382,7 +384,7 @@ impl<'a, I: Iterator>, W: StrWrite, E: EmbedContentProvider> Cl Tag::Superscript => self.write(""), Tag::Emphasis => self.write(""), Tag::Strong => self.write(""), - Tag::Strikethrough => self.write(""), + Tag::Strikethrough => self.write(""), Tag::Link { link_type: LinkType::Email, dest_url, @@ -551,7 +553,7 @@ impl<'a, I: Iterator>, W: StrWrite, E: EmbedContentProvider> Cl TagEnd::Superscript => self.write(""), TagEnd::Subscript => self.write(""), TagEnd::Strong => self.write(""), - TagEnd::Strikethrough => self.write(""), + TagEnd::Strikethrough => self.write(""), TagEnd::Link => self.write(""), TagEnd::Image => Ok(()), // No-op: raw_text() already consumed the End(Image) event TagEnd::Embed => Ok(()), @@ -568,7 +570,9 @@ impl<'a, I: Iterator>, W: StrWrite, E: EmbedContentProvider> Cl } } -impl<'a, I: Iterator>, W: StrWrite, E: EmbedContentProvider> ClientWriter<'a, I, W, E> { +impl<'a, I: Iterator>, W: StrWrite, E: EmbedContentProvider> + ClientWriter<'a, I, W, E> +{ fn write_embed( &mut self, embed_type: EmbedType, diff --git a/crates/weaver-renderer/src/lib.rs b/crates/weaver-renderer/src/lib.rs index 12e0d46..cf5876f 100644 --- a/crates/weaver-renderer/src/lib.rs +++ b/crates/weaver-renderer/src/lib.rs @@ -10,7 +10,11 @@ use n0_future::Stream; use yaml_rust2::Yaml; use yaml_rust2::YamlLoader; +#[cfg(not(all(target_family = "wasm", target_os = "unknown")))] use regex::Regex; +#[cfg(all(target_family = "wasm", target_os = "unknown"))] +use regex_lite::Regex; + use std::iter::Iterator; use std::path::PathBuf; use std::pin::Pin; diff --git a/crates/weaver-renderer/src/types.rs b/crates/weaver-renderer/src/types.rs index 576c2ad..51cf894 100644 --- a/crates/weaver-renderer/src/types.rs +++ b/crates/weaver-renderer/src/types.rs @@ -1,5 +1,5 @@ -use compact_string::CompactString; use http::Uri; +use smol_str::SmolStr; use weaver_common::jacquard::types::string::{Cid, Did}; pub struct Link<'a> { @@ -7,7 +7,7 @@ pub struct Link<'a> { pub blob: BlobLink<'a>, } -pub type MimeType = CompactString; +pub type MimeType = SmolStr; pub enum BlobLink<'a> { PDS { diff --git a/crates/weaver-renderer/src/utils.rs b/crates/weaver-renderer/src/utils.rs index 33bec33..f3a35b0 100644 --- a/crates/weaver-renderer/src/utils.rs +++ b/crates/weaver-renderer/src/utils.rs @@ -2,7 +2,12 @@ use std::{path::Path, sync::OnceLock}; use markdown_weaver::{CodeBlockKind, CowStr, Event, Tag}; use miette::IntoDiagnostic; use n0_future::TryFutureExt; + +#[cfg(not(all(target_family = "wasm", target_os = "unknown")))] use regex::Regex; +#[cfg(all(target_family = "wasm", target_os = "unknown"))] +use regex_lite::Regex; + use markdown_weaver::BrokenLink; use std::path::PathBuf; use std::sync::Arc; diff --git a/flake.nix b/flake.nix index dfe4d2a..0d18471 100644 --- a/flake.nix +++ b/flake.nix @@ -267,6 +267,7 @@ dioxus-cli wasm-bindgen-cli wasm-pack + twiggy binaryen.out ]; }; diff --git a/weaver_notes/.obsidian/workspace.json b/weaver_notes/.obsidian/workspace.json index 3fcb4ed..9dfaa4c 100644 --- a/weaver_notes/.obsidian/workspace.json +++ b/weaver_notes/.obsidian/workspace.json @@ -41,12 +41,12 @@ "state": { "type": "markdown", "state": { - "file": "bug notes.md", + "file": "Why I rewrote pdsls in Rust (tm).md", "mode": "source", "source": false }, "icon": "lucide-file", - "title": "bug notes" + "title": "Why I rewrote pdsls in Rust (tm)" } } ], @@ -201,6 +201,9 @@ }, "active": "6029beecc3d03bce", "lastOpenFiles": [ + "light_mode_excerpt.png", + "notebook_entry_preview.png", + "xkcd_345_excerpt.png", "bug notes.md", "Why I rewrote pdsls in Rust (tm).md", "meta.png", @@ -212,8 +215,6 @@ "pretty_editor.png", "Arch.md", "Weaver - Long-form writing.md", - "weaver_photo_med.jpg", - "xkcd_345_excerpt.png", - "light_mode_excerpt.png" + "weaver_photo_med.jpg" ] } \ No newline at end of file diff --git a/weaver_notes/bug notes.md b/weaver_notes/bug notes.md deleted file mode 100644 index 825dea4..0000000 --- a/weaver_notes/bug notes.md +++ /dev/null @@ -1,3 +0,0 @@ -- renderer/processor doesn't add http(s):// to urls without them (or handle autolinks currently) -- mobile view doesn't narrow correctly -- \ No newline at end of file diff --git a/weaver_notes/light_mode_excerpt.png b/weaver_notes/light_mode_excerpt.png new file mode 100644 index 0000000000000000000000000000000000000000..f9e9bb108e67a6e95eb6196cfcf4dc24642e3c2f GIT binary patch literal 130900 zcmeAS@N?(olHy`uVBq!ia0y~yU`}9QV4BFm#=yW}qbPZ!6KiaBrQvd#&) zDt?^*E(q{my2QcJ!J)KJ+(XlaBSd z8vd;R*FELKBSYJ&Gxq#)my-X*L);m-$jVT27V~rMvN<>2)GXAz@{=n*$z%88zsW}~ zYzv3@W|7^+dyLM{yk*y?`}Rzz3xJ4DSh)jw2hYefF|tlxCgIH+kyp@zVj+qEryvHR5azqhRJ4EtF5O6eC z{P}aYJ>$zMT^8}xRr}tAXqJl|uilt^Y{`y^0pEx8xB@UG^cyC6{UT=?EY zJ6Be3z4WMLrsgb5*{}8ctd4fe&gLr$%X)BUs=uZGzX0v}GOyVx3*StgVH;|6a?>os zO?j}yH}Rt-^E8V%nO|FD|1OWKe|cW=%ZHDfO`G`R_h@CeOgT{e_+72k-mKMs;uM^- zUPbJiar=Cl@64WE;rhPzMQS)qYuTzxK9 z1)AVHU$l60CjGWwW?i>F+(3P+e)Nrx-yhYwXMUOWwr~Ib{J?Hw*-n}I1^MA`}~-xMN7Q=n#I;G)>S`eSzGq=pztT=x}3=NAay?nSO5ujT*yAq7q`o8{`pAH z?{YRJd&>Slblz2J-_aMp^PJmH&B*7iXU&wj%75Gvdt>SIx^K=RZu7j$f7$spl0T_T zv2+buHPfE`Y57Jo&dS5AyZJA+Enfce#?sXy&Bit-l%}4Z*qJ4I;^mi)B#R|?kDn|0 z^EbQQZOiK~th4r1^3Q$#In`s{j9Fsm_4y`9PD%T>xlla!NwGrm6x~px{zx6;f}C(U z8@>JE$;-7|ovQhKFF*R3!D^jQ{^R%RAHR8TB!)qg&kLWHvIBKd`%XTrIhwRwh1=RN z{N0_U+2!v}W{3a$zni~lbKK6$OBT#7e}DPn1YQe=UrRbwUQg0-tl{F%ySv+8-Z$1M zecj!Hs=pUnwLfkCbD%IQc9nvue&m6V_iFd;&wsvt*<1Cwn=Qig?@zt{;KbhO?UPde z#WV`-U)X)!J5$c0>f@>@|BkK+c9gMTo|Am~<7U76ztiqzy^2_?Miy+eI= ze`z(HY{8vs5nmuC-7>{P?%C_uUGHb)^v}OPY3=NtwfhhMbXvy4$9K)*?47y>zu3Kc z?;jeU^?FiXdCvY?&Qh*BdM9t5bg@)7DtNHpcegCxjeDEL=g!ej+257sD{|@n(#rjYDW(Ub^IQ*vJ&`O_q1{=;5%)Z3>^#42IXQ!-818j<;CW^!I zNQO%U`)3}x$y9+P5XEiioYEDVfiU%t!DJMAmuW(*wkXHa4pBBn^ULU z|Gd=S_r89F^8WdIiegXkB_HiMdG3DT@#ha~YOk)DwukRusypwwS?YRs_UQgk|8()^ zraJcHoLk`4)5K1W*A0oM3#af)J#E`uE<0)dl8=vrzJ;7z>F_HgL_7W)$5z(5MHT+* zQXd_Q4dR<&TUr!(zSSq-iK5H?pB_d3y0h7>djwV zbYA`#S`T`a9NWfR_TiA|&8Y&>bDPUwOB7QCDl)djgS7qUvvs~OCWY%yirgoUW zzgt{&zOT6L{o`Nf<9565E4sF2BGFz zzQaGeULQ34ByLl@=KPVLUw*E6dZqrEQPuSI&-Y0$7xX)~{>_qmY3jz_r&2;p{mruC z_i5}6+`Q(oart}u$_SG(5tBJPCx_=<_*XoAPT%(S+uy|P?mZ4j+i+=yT+ZHaA2h3e zyg2!hZ_fTFUw=o;XFIn^IPKp$S-)6bz0F5zmzi45X;jns6$fp+F!jFWk=PV*^>f

5J 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

CxPR@_~C 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 literal 0 HcmV?d00001