hmmm

Orual 94b6302a 23946661

+4464 -3873
+94 -148
Cargo.lock
··· 131 131 132 132 [[package]] 133 133 name = "anstyle-query" 134 - version = "1.1.4" 134 + version = "1.1.5" 135 135 source = "registry+https://github.com/rust-lang/crates.io-index" 136 - checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" 136 + checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" 137 137 dependencies = [ 138 - "windows-sys 0.60.2", 138 + "windows-sys 0.61.2", 139 139 ] 140 140 141 141 [[package]] 142 142 name = "anstyle-wincon" 143 - version = "3.0.10" 143 + version = "3.0.11" 144 144 source = "registry+https://github.com/rust-lang/crates.io-index" 145 - checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" 145 + checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" 146 146 dependencies = [ 147 147 "anstyle", 148 148 "once_cell_polyfill", 149 - "windows-sys 0.60.2", 149 + "windows-sys 0.61.2", 150 150 ] 151 151 152 152 [[package]] ··· 357 357 358 358 [[package]] 359 359 name = "axum" 360 - version = "0.8.6" 360 + version = "0.8.7" 361 361 source = "registry+https://github.com/rust-lang/crates.io-index" 362 - checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" 362 + checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425" 363 363 dependencies = [ 364 364 "axum-core", 365 365 "axum-macros", ··· 710 710 711 711 [[package]] 712 712 name = "bytes" 713 - version = "1.10.1" 713 + version = "1.11.0" 714 714 source = "registry+https://github.com/rust-lang/crates.io-index" 715 - checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 715 + checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" 716 716 dependencies = [ 717 717 "serde", 718 718 ] ··· 763 763 764 764 [[package]] 765 765 name = "cc" 766 - version = "1.2.45" 766 + version = "1.2.46" 767 767 source = "registry+https://github.com/rust-lang/crates.io-index" 768 - checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe" 768 + checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" 769 769 dependencies = [ 770 770 "find-msvc-tools", 771 771 "jobserver", ··· 894 894 895 895 [[package]] 896 896 name = "clap" 897 - version = "4.5.51" 897 + version = "4.5.53" 898 898 source = "registry+https://github.com/rust-lang/crates.io-index" 899 - checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" 899 + checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" 900 900 dependencies = [ 901 901 "clap_builder", 902 902 "clap_derive", ··· 904 904 905 905 [[package]] 906 906 name = "clap_builder" 907 - version = "4.5.51" 907 + version = "4.5.53" 908 908 source = "registry+https://github.com/rust-lang/crates.io-index" 909 - checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" 909 + checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" 910 910 dependencies = [ 911 911 "anstream", 912 912 "anstyle", ··· 986 986 dependencies = [ 987 987 "bytes", 988 988 "memchr", 989 - ] 990 - 991 - [[package]] 992 - name = "compact_string" 993 - version = "0.1.0" 994 - source = "registry+https://github.com/rust-lang/crates.io-index" 995 - checksum = "5255b88d8ea09573f588088dea17fbea682b4442abea6761a15d1da2c3a76c5c" 996 - dependencies = [ 997 - "serde", 998 - "thiserror 1.0.69", 999 989 ] 1000 990 1001 991 [[package]] ··· 1865 1855 ] 1866 1856 1867 1857 [[package]] 1868 - name = "dioxus-free-icons" 1869 - version = "0.10.0" 1870 - source = "registry+https://github.com/rust-lang/crates.io-index" 1871 - checksum = "28d356e0f9edad0930bc1cc76744360c0ecca020cb943acaadf42cb774f28284" 1872 - dependencies = [ 1873 - "dioxus", 1874 - ] 1875 - 1876 - [[package]] 1877 1858 name = "dioxus-fullstack" 1878 1859 version = "0.7.1" 1879 1860 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2109 2090 [[package]] 2110 2091 name = "dioxus-primitives" 2111 2092 version = "0.0.1" 2112 - source = "git+https://github.com/DioxusLabs/components#98067ce2da493651b0c089db91e9903714c211f7" 2093 + source = "git+https://github.com/DioxusLabs/components#a15f329d1dc2bd76cd4030b27ecd70edb9fd8c6b" 2113 2094 dependencies = [ 2114 2095 "dioxus", 2115 2096 "dioxus-time", ··· 2491 2472 checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" 2492 2473 2493 2474 [[package]] 2494 - name = "dynosaur" 2495 - version = "0.2.0" 2496 - source = "registry+https://github.com/rust-lang/crates.io-index" 2497 - checksum = "277b2cb52d2df4acece06bb16bc0bb0a006970c7bf504eac2d310927a6f65890" 2498 - dependencies = [ 2499 - "dynosaur_derive", 2500 - "trait-variant", 2501 - ] 2502 - 2503 - [[package]] 2504 - name = "dynosaur_derive" 2505 - version = "0.2.0" 2506 - source = "registry+https://github.com/rust-lang/crates.io-index" 2507 - checksum = "7a4102713839a8c01c77c165bc38ef2e83948f6397fa1e1dcfacec0f07b149d3" 2508 - dependencies = [ 2509 - "proc-macro2", 2510 - "quote", 2511 - "syn 2.0.110", 2512 - ] 2513 - 2514 - [[package]] 2515 2475 name = "ecdsa" 2516 2476 version = "0.16.9" 2517 2477 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2777 2737 2778 2738 [[package]] 2779 2739 name = "find-msvc-tools" 2780 - version = "0.1.4" 2740 + version = "0.1.5" 2781 2741 source = "registry+https://github.com/rust-lang/crates.io-index" 2782 - checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" 2742 + checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" 2783 2743 2784 2744 [[package]] 2785 2745 name = "flate2" ··· 3463 3423 "futures-core", 3464 3424 "futures-sink", 3465 3425 "http", 3466 - "indexmap 2.12.0", 3426 + "indexmap 2.12.1", 3467 3427 "slab", 3468 3428 "tokio", 3469 3429 "tokio-util", ··· 3520 3480 3521 3481 [[package]] 3522 3482 name = "hashbrown" 3523 - version = "0.16.0" 3483 + version = "0.16.1" 3524 3484 source = "registry+https://github.com/rust-lang/crates.io-index" 3525 - checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" 3485 + checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" 3526 3486 dependencies = [ 3527 3487 "allocator-api2", 3528 3488 "equivalent", ··· 3762 3722 3763 3723 [[package]] 3764 3724 name = "hyper" 3765 - version = "1.8.0" 3725 + version = "1.8.1" 3766 3726 source = "registry+https://github.com/rust-lang/crates.io-index" 3767 - checksum = "1744436df46f0bde35af3eda22aeaba453aada65d8f1c171cd8a5f59030bd69f" 3727 + checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" 3768 3728 dependencies = [ 3769 3729 "atomic-waker", 3770 3730 "bytes", ··· 3801 3761 ] 3802 3762 3803 3763 [[package]] 3804 - name = "hyper-tls" 3805 - version = "0.6.0" 3806 - source = "registry+https://github.com/rust-lang/crates.io-index" 3807 - checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" 3808 - dependencies = [ 3809 - "bytes", 3810 - "http-body-util", 3811 - "hyper", 3812 - "hyper-util", 3813 - "native-tls", 3814 - "tokio", 3815 - "tokio-native-tls", 3816 - "tower-service", 3817 - ] 3818 - 3819 - [[package]] 3820 3764 name = "hyper-util" 3821 - version = "0.1.17" 3765 + version = "0.1.18" 3822 3766 source = "registry+https://github.com/rust-lang/crates.io-index" 3823 - checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" 3767 + checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" 3824 3768 dependencies = [ 3825 3769 "base64 0.22.1", 3826 3770 "bytes", ··· 4003 3947 4004 3948 [[package]] 4005 3949 name = "indexmap" 4006 - version = "2.12.0" 3950 + version = "2.12.1" 4007 3951 source = "registry+https://github.com/rust-lang/crates.io-index" 4008 - checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" 3952 + checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" 4009 3953 dependencies = [ 4010 3954 "equivalent", 4011 - "hashbrown 0.16.0", 3955 + "hashbrown 0.16.1", 4012 3956 "serde", 4013 3957 "serde_core", 4014 3958 ] ··· 4033 3977 4034 3978 [[package]] 4035 3979 name = "insta" 4036 - version = "1.43.2" 3980 + version = "1.44.1" 4037 3981 source = "registry+https://github.com/rust-lang/crates.io-index" 4038 - checksum = "46fdb647ebde000f43b5b53f773c30cf9b0cb4300453208713fa38b2c70935a0" 3982 + checksum = "e8732d3774162a0851e3f2b150eb98f31a9885dd75985099421d393385a01dfd" 4039 3983 dependencies = [ 4040 3984 "console", 4041 3985 "once_cell", ··· 4123 4067 4124 4068 [[package]] 4125 4069 name = "jacquard" 4126 - version = "0.9.0" 4070 + version = "0.9.4" 4127 4071 dependencies = [ 4128 4072 "bytes", 4129 4073 "getrandom 0.2.16", ··· 4137 4081 "jose-jwk", 4138 4082 "miette 7.6.0", 4139 4083 "regex", 4084 + "regex-lite", 4140 4085 "reqwest", 4141 4086 "serde", 4142 4087 "serde_html_form", ··· 4152 4097 4153 4098 [[package]] 4154 4099 name = "jacquard-api" 4155 - version = "0.9.0" 4100 + version = "0.9.2" 4156 4101 dependencies = [ 4157 4102 "bon", 4158 4103 "bytes", ··· 4169 4114 4170 4115 [[package]] 4171 4116 name = "jacquard-axum" 4172 - version = "0.9.0" 4117 + version = "0.9.2" 4173 4118 dependencies = [ 4174 4119 "axum", 4175 4120 "bytes", ··· 4190 4135 4191 4136 [[package]] 4192 4137 name = "jacquard-common" 4193 - version = "0.9.0" 4138 + version = "0.9.2" 4194 4139 dependencies = [ 4195 4140 "base64 0.22.1", 4196 4141 "bon", ··· 4213 4158 "p256", 4214 4159 "rand 0.9.2", 4215 4160 "regex", 4161 + "regex-lite", 4216 4162 "reqwest", 4217 4163 "serde", 4218 4164 "serde_html_form", ··· 4232 4178 4233 4179 [[package]] 4234 4180 name = "jacquard-derive" 4235 - version = "0.9.0" 4181 + version = "0.9.4" 4236 4182 dependencies = [ 4237 4183 "heck 0.5.0", 4238 4184 "jacquard-lexicon", ··· 4243 4189 4244 4190 [[package]] 4245 4191 name = "jacquard-identity" 4246 - version = "0.9.1" 4192 + version = "0.9.2" 4247 4193 dependencies = [ 4248 4194 "bon", 4249 4195 "bytes", ··· 4253 4199 "jacquard-common", 4254 4200 "jacquard-lexicon", 4255 4201 "miette 7.6.0", 4256 - "mini-moka", 4202 + "mini-moka 0.10.99", 4257 4203 "percent-encoding", 4258 4204 "reqwest", 4259 4205 "serde", ··· 4269 4215 4270 4216 [[package]] 4271 4217 name = "jacquard-lexicon" 4272 - version = "0.9.1" 4218 + version = "0.9.2" 4273 4219 dependencies = [ 4274 4220 "cid", 4275 4221 "dashmap", ··· 4294 4240 4295 4241 [[package]] 4296 4242 name = "jacquard-oauth" 4297 - version = "0.9.0" 4243 + version = "0.9.2" 4298 4244 dependencies = [ 4299 4245 "base64 0.22.1", 4300 4246 "bytes", ··· 4499 4445 dependencies = [ 4500 4446 "cssparser", 4501 4447 "html5ever 0.29.1", 4502 - "indexmap 2.12.0", 4448 + "indexmap 2.12.1", 4503 4449 "selectors", 4504 4450 ] 4505 4451 ··· 4705 4651 source = "registry+https://github.com/rust-lang/crates.io-index" 4706 4652 checksum = "96051b46fc183dc9cd4a223960ef37b9af631b55191852a8274bfef064cda20f" 4707 4653 dependencies = [ 4708 - "hashbrown 0.16.0", 4654 + "hashbrown 0.16.1", 4709 4655 ] 4710 4656 4711 4657 [[package]] ··· 5032 4978 5033 4979 [[package]] 5034 4980 name = "mini-moka" 4981 + version = "0.10.99" 4982 + dependencies = [ 4983 + "crossbeam-channel", 4984 + "crossbeam-utils", 4985 + "dashmap", 4986 + "smallvec", 4987 + "tagptr", 4988 + "triomphe", 4989 + "web-time", 4990 + ] 4991 + 4992 + [[package]] 4993 + name = "mini-moka" 5035 4994 version = "0.11.0" 5036 4995 source = "git+https://github.com/moka-rs/mini-moka?rev=da864e849f5d034f32e02197fee9bb5d5af36d3d#da864e849f5d034f32e02197fee9bb5d5af36d3d" 5037 4996 dependencies = [ ··· 5944 5903 checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" 5945 5904 dependencies = [ 5946 5905 "base64 0.22.1", 5947 - "indexmap 2.12.0", 5906 + "indexmap 2.12.1", 5948 5907 "quick-xml 0.38.4", 5949 5908 "serde", 5950 5909 "time", ··· 6423 6382 ] 6424 6383 6425 6384 [[package]] 6385 + name = "regex-lite" 6386 + version = "0.1.8" 6387 + source = "registry+https://github.com/rust-lang/crates.io-index" 6388 + checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" 6389 + 6390 + [[package]] 6426 6391 name = "regex-syntax" 6427 6392 version = "0.8.8" 6428 6393 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 6448 6413 "http-body-util", 6449 6414 "hyper", 6450 6415 "hyper-rustls", 6451 - "hyper-tls", 6452 6416 "hyper-util", 6453 6417 "js-sys", 6454 6418 "log", 6455 6419 "mime", 6456 6420 "mime_guess", 6457 - "native-tls", 6458 6421 "percent-encoding", 6459 6422 "pin-project-lite", 6460 6423 "quinn", ··· 6465 6428 "serde_urlencoded", 6466 6429 "sync_wrapper", 6467 6430 "tokio", 6468 - "tokio-native-tls", 6469 6431 "tokio-rustls", 6470 6432 "tokio-util", 6471 6433 "tower", ··· 6481 6443 6482 6444 [[package]] 6483 6445 name = "resolv-conf" 6484 - version = "0.7.5" 6446 + version = "0.7.6" 6485 6447 source = "registry+https://github.com/rust-lang/crates.io-index" 6486 - checksum = "6b3789b30bd25ba102de4beabd95d21ac45b69b1be7d14522bab988c526d6799" 6448 + checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" 6487 6449 6488 6450 [[package]] 6489 6451 name = "rfc6979" ··· 6559 6521 6560 6522 [[package]] 6561 6523 name = "rsa" 6562 - version = "0.9.8" 6524 + version = "0.9.9" 6563 6525 source = "registry+https://github.com/rust-lang/crates.io-index" 6564 - checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" 6526 + checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" 6565 6527 dependencies = [ 6566 6528 "const-oid", 6567 6529 "digest", ··· 6924 6886 checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" 6925 6887 dependencies = [ 6926 6888 "form_urlencoded", 6927 - "indexmap 2.12.0", 6889 + "indexmap 2.12.1", 6928 6890 "itoa", 6929 6891 "ryu", 6930 6892 "serde_core", ··· 6948 6910 source = "registry+https://github.com/rust-lang/crates.io-index" 6949 6911 checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" 6950 6912 dependencies = [ 6951 - "indexmap 2.12.0", 6913 + "indexmap 2.12.1", 6952 6914 "itoa", 6953 6915 "memchr", 6954 6916 "ryu", ··· 7021 6983 7022 6984 [[package]] 7023 6985 name = "serde_with" 7024 - version = "3.15.1" 6986 + version = "3.16.0" 7025 6987 source = "registry+https://github.com/rust-lang/crates.io-index" 7026 - checksum = "aa66c845eee442168b2c8134fec70ac50dc20e760769c8ba0ad1319ca1959b04" 6988 + checksum = "10574371d41b0d9b2cff89418eda27da52bcaff2cc8741db26382a77c29131f1" 7027 6989 dependencies = [ 7028 6990 "base64 0.22.1", 7029 6991 "chrono", 7030 6992 "hex", 7031 6993 "indexmap 1.9.3", 7032 - "indexmap 2.12.0", 6994 + "indexmap 2.12.1", 7033 6995 "schemars 0.9.0", 7034 6996 "schemars 1.1.0", 7035 6997 "serde_core", ··· 7040 7002 7041 7003 [[package]] 7042 7004 name = "serde_with_macros" 7043 - version = "3.15.1" 7005 + version = "3.16.0" 7044 7006 source = "registry+https://github.com/rust-lang/crates.io-index" 7045 - checksum = "b91a903660542fced4e99881aa481bdbaec1634568ee02e0b8bd57c64cb38955" 7007 + checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b" 7046 7008 dependencies = [ 7047 7009 "darling", 7048 7010 "proc-macro2", ··· 8001 7963 ] 8002 7964 8003 7965 [[package]] 8004 - name = "tokio-native-tls" 8005 - version = "0.3.1" 8006 - source = "registry+https://github.com/rust-lang/crates.io-index" 8007 - checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 8008 - dependencies = [ 8009 - "native-tls", 8010 - "tokio", 8011 - ] 8012 - 8013 - [[package]] 8014 7966 name = "tokio-rustls" 8015 7967 version = "0.26.4" 8016 7968 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 8112 8064 source = "registry+https://github.com/rust-lang/crates.io-index" 8113 8065 checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" 8114 8066 dependencies = [ 8115 - "indexmap 2.12.0", 8067 + "indexmap 2.12.1", 8116 8068 "serde", 8117 8069 "serde_spanned 0.6.9", 8118 8070 "toml_datetime 0.6.11", ··· 8156 8108 source = "registry+https://github.com/rust-lang/crates.io-index" 8157 8109 checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" 8158 8110 dependencies = [ 8159 - "indexmap 2.12.0", 8111 + "indexmap 2.12.1", 8160 8112 "toml_datetime 0.6.11", 8161 8113 "winnow 0.5.40", 8162 8114 ] ··· 8167 8119 source = "registry+https://github.com/rust-lang/crates.io-index" 8168 8120 checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" 8169 8121 dependencies = [ 8170 - "indexmap 2.12.0", 8122 + "indexmap 2.12.1", 8171 8123 "toml_datetime 0.6.11", 8172 8124 "winnow 0.5.40", 8173 8125 ] ··· 8178 8130 source = "registry+https://github.com/rust-lang/crates.io-index" 8179 8131 checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" 8180 8132 dependencies = [ 8181 - "indexmap 2.12.0", 8133 + "indexmap 2.12.1", 8182 8134 "serde", 8183 8135 "serde_spanned 0.6.9", 8184 8136 "toml_datetime 0.6.11", ··· 8192 8144 source = "registry+https://github.com/rust-lang/crates.io-index" 8193 8145 checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" 8194 8146 dependencies = [ 8195 - "indexmap 2.12.0", 8147 + "indexmap 2.12.1", 8196 8148 "toml_datetime 0.7.3", 8197 8149 "toml_parser", 8198 8150 "winnow 0.7.13", ··· 8945 8897 "console_error_panic_hook", 8946 8898 "dashmap", 8947 8899 "dioxus", 8948 - "dioxus-free-icons", 8949 8900 "dioxus-primitives", 8950 8901 "dotenvy", 8951 8902 "gloo-storage", ··· 8953 8904 "humansize", 8954 8905 "jacquard", 8955 8906 "jacquard-axum", 8907 + "jacquard-identity", 8956 8908 "jacquard-lexicon", 8957 8909 "js-sys", 8958 8910 "lol_alloc", 8959 8911 "markdown-weaver", 8960 8912 "mime-sniffer", 8961 - "mini-moka", 8913 + "mini-moka 0.11.0", 8962 8914 "n0-future", 8963 8915 "reqwest", 8964 8916 "serde", 8965 8917 "serde_html_form", 8918 + "serde_ipld_dagcbor", 8966 8919 "serde_json", 8967 8920 "time", 8968 8921 "tokio", ··· 8983 8936 "clap", 8984 8937 "dirs", 8985 8938 "jacquard", 8986 - "jacquard-api", 8987 8939 "kdl", 8988 8940 "markdown-weaver", 8989 8941 "markdown-weaver-escape", ··· 9010 8962 "markdown-weaver-escape", 9011 8963 "miette 7.6.0", 9012 8964 "mime-sniffer", 9013 - "minijinja", 9014 8965 "n0-future", 9015 - "owo-colors", 9016 8966 "pin-project", 9017 8967 "regex", 8968 + "regex-lite", 9018 8969 "reqwest", 9019 8970 "send_wrapper", 9020 8971 "serde", 9021 - "serde_bytes", 9022 - "serde_html_form", 9023 - "serde_ipld_dagcbor", 9024 8972 "serde_json", 9025 8973 "thiserror 2.0.17", 9026 8974 "tokio", ··· 9049 8997 "futures-util", 9050 8998 "hyper", 9051 8999 "jacquard", 9052 - "jacquard-api", 9053 9000 "jacquard-axum", 9054 9001 "jose", 9055 9002 "jose-jwk", ··· 9087 9034 version = "0.1.0" 9088 9035 dependencies = [ 9089 9036 "bitflags 2.10.0", 9090 - "compact_string", 9091 9037 "dashmap", 9092 - "dynosaur", 9093 9038 "http", 9094 9039 "ignore", 9095 9040 "insta", ··· 9102 9047 "pin-project", 9103 9048 "pin-utils", 9104 9049 "regex", 9050 + "regex-lite", 9105 9051 "reqwest", 9106 9052 "smol_str", 9107 9053 "syntect", ··· 9395 9341 9396 9342 [[package]] 9397 9343 name = "windows-registry" 9398 - version = "0.5.3" 9344 + version = "0.6.1" 9399 9345 source = "registry+https://github.com/rust-lang/crates.io-index" 9400 - checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" 9346 + checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" 9401 9347 dependencies = [ 9402 - "windows-link 0.1.3", 9403 - "windows-result 0.3.4", 9404 - "windows-strings 0.4.2", 9348 + "windows-link 0.2.1", 9349 + "windows-result 0.4.1", 9350 + "windows-strings 0.5.1", 9405 9351 ] 9406 9352 9407 9353 [[package]] ··· 10006 9952 10007 9953 [[package]] 10008 9954 name = "zerocopy" 10009 - version = "0.8.27" 9955 + version = "0.8.28" 10010 9956 source = "registry+https://github.com/rust-lang/crates.io-index" 10011 - checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" 9957 + checksum = "43fa6694ed34d6e57407afbccdeecfa268c470a7d2a5b0cf49ce9fcc345afb90" 10012 9958 dependencies = [ 10013 9959 "zerocopy-derive", 10014 9960 ] 10015 9961 10016 9962 [[package]] 10017 9963 name = "zerocopy-derive" 10018 - version = "0.8.27" 9964 + version = "0.8.28" 10019 9965 source = "registry+https://github.com/rust-lang/crates.io-index" 10020 - checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" 9966 + checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26" 10021 9967 dependencies = [ 10022 9968 "proc-macro2", 10023 9969 "quote",
+11 -11
Cargo.toml
··· 21 21 22 22 23 23 [workspace.dependencies] 24 - 25 24 serde = { version = "1.0", features = ["derive"] } 26 25 bytes = "1.10" 27 - minijinja = { version = "2.9.0", default-features = false } 28 - minijinja-contrib = { version = "2.9.0", default-features = false } 29 26 miette = { version = "7.6" } 30 - owo-colors = { version = "4.2.0" } 31 27 thiserror = "2.0" 32 28 syntect = { version = "5.2.0", default-features = false } 33 - jane-eyre = "0.6.12" 34 29 n0-future = "=0.1.3" 35 30 tracing = { version = "0.1.41", default-features = false, features = ["std"] } 36 - lexicon_cid = { package = "cid", version = "0.10.1", features = [ 37 - "serde-codec", 38 - ] } 39 - 40 31 markdown-weaver = { git = "https://github.com/rsform/markdown-weaver" } 41 32 markdown-weaver-escape = { git = "https://github.com/rsform/markdown-weaver" } 42 33 43 34 # jacquard = { git = "https://tangled.org/@nonbinary.computer/jacquard", default-features = false, features = ["derive", "api_bluesky", "tracing"] } 44 - # jacquard-api = { git = "https://tangled.org/@nonbinary.computer/jacquard" } 35 + # jacquard-identity = { git = "https://tangled.org/@nonbinary.computer/jacquard", features = ["cache"] } 45 36 # jacquard-common = { git = "https://tangled.org/@nonbinary.computer/jacquard" } 46 37 # jacquard-axum = { git = "https://tangled.org/@nonbinary.computer/jacquard" } 47 38 # jacquard-derive = { git = "https://tangled.org/@nonbinary.computer/jacquard" } 48 39 # jacquard-lexicon = { git = "https://tangled.org/@nonbinary.computer/jacquard", default-features = false } 49 40 50 - jacquard = { path = "../jacquard/crates/jacquard", default-features = false, features = ["derive", "api_bluesky", "tracing"] } 41 + jacquard = { path = "../jacquard/crates/jacquard", default-features = false, features = ["derive", "api_bluesky", "tracing", "cache"] } 42 + jacquard-identity = { path = "../jacquard/crates/jacquard-identity", features = ["cache"] } 51 43 jacquard-api = { path = "../jacquard/crates/jacquard-api" } 52 44 jacquard-common = { path = "../jacquard/crates/jacquard-common" } 53 45 jacquard-axum = {path = "../jacquard/crates/jacquard-axum" } 54 46 jacquard-derive = { path = "../jacquard/crates/jacquard-derive" } 55 47 jacquard-lexicon = { path = "../jacquard/crates/jacquard-lexicon", default-features = false } 48 + 49 + # jacquard = { path = "../jacquard-facet/crates/jacquard", default-features = false, features = ["derive", "api_bluesky", "tracing", "serde"] } 50 + # jacquard-identity = { path = "../jacquard-facet/crates/jacquard-identity", features = ["cache"] } 51 + # jacquard-api = { path = "../jacquard-facet/crates/jacquard-api" } 52 + # jacquard-common = { path = "../jacquard-facet/crates/jacquard-common" } 53 + # jacquard-axum = {path = "../jacquard-facet/crates/jacquard-axum" } 54 + # jacquard-derive = { path = "../jacquard-facet/crates/jacquard-derive" } 55 + # jacquard-lexicon = { path = "../jacquard-facet/crates/jacquard-lexicon", default-features = false } 56 56 57 57 [profile] 58 58
+5 -3
crates/weaver-api/Cargo.toml
··· 6 6 authors.workspace = true 7 7 8 8 [dependencies] 9 - bytes = { workspace = true, features = ["serde"] } 9 + bytes = { workspace = true } 10 10 jacquard-common = { workspace = true } 11 11 jacquard-derive = { workspace = true } 12 12 jacquard-lexicon = { workspace = true } 13 13 miette.workspace = true 14 14 rustversion = "1.0" 15 15 serde.workspace = true 16 - serde_ipld_dagcbor = "0.6" 16 + serde_ipld_dagcbor = { version = "0.6", optional = true } 17 17 thiserror.workspace = true 18 18 unicode-segmentation = "1.12" 19 19 ··· 23 23 non_snake_case = "allow" 24 24 25 25 [features] 26 - default = ["sh_weaver", "com_atproto"] 26 + default = ["sh_weaver", "com_atproto", "serde"] 27 27 streaming = ["jacquard-common/websocket"] 28 + serde = ["bytes/serde", "dep:serde_ipld_dagcbor"] 29 + 28 30 29 31 # --- generated --- 30 32 # Generated namespace features
+15
crates/weaver-api/lexicons/tools_ozone_moderation_defs.json
··· 326 326 "type": "string", 327 327 "description": "The content of the email sent to the user." 328 328 }, 329 + "isDelivered": { 330 + "type": "boolean", 331 + "description": "Indicates whether the email was successfully delivered to the user's inbox." 332 + }, 329 333 "policies": { 330 334 "type": "array", 331 335 "description": "Names/Keywords of the policies that necessitated the email.", ··· 555 559 "type": "string", 556 560 "description": "When the strike should expire. If not provided, the strike never expires.", 557 561 "format": "datetime" 562 + }, 563 + "targetServices": { 564 + "type": "array", 565 + "description": "List of services where the takedown should be applied. If empty or not provided, takedown is applied on all configured services.", 566 + "items": { 567 + "type": "string", 568 + "knownValues": [ 569 + "appview", 570 + "pds" 571 + ] 572 + } 558 573 } 559 574 } 560 575 },
+7 -1
crates/weaver-api/lexicons/tools_ozone_moderation_queryStatuses.json
··· 149 149 }, 150 150 "reviewState": { 151 151 "type": "string", 152 - "description": "Specify when fetching subjects in a certain state" 152 + "description": "Specify when fetching subjects in a certain state", 153 + "knownValues": [ 154 + "tools.ozone.moderation.defs#reviewOpen", 155 + "tools.ozone.moderation.defs#reviewClosed", 156 + "tools.ozone.moderation.defs#reviewEscalated", 157 + "tools.ozone.moderation.defs#reviewNone" 158 + ] 153 159 }, 154 160 "reviewedAfter": { 155 161 "type": "string",
+21
crates/weaver-api/lexicons/tools_ozone_moderation_scheduleAction.json
··· 133 133 "type": "integer", 134 134 "description": "Indicates how long the takedown should be in effect before automatically expiring." 135 135 }, 136 + "emailContent": { 137 + "type": "string", 138 + "description": "Email content to be sent to the user upon takedown." 139 + }, 140 + "emailSubject": { 141 + "type": "string", 142 + "description": "Subject of the email to be sent to the user upon takedown." 143 + }, 136 144 "policies": { 137 145 "type": "array", 138 146 "description": "Names/Keywords of the policies that drove the decision.", ··· 140 148 "type": "string" 141 149 }, 142 150 "maxLength": 5 151 + }, 152 + "severityLevel": { 153 + "type": "string", 154 + "description": "Severity level of the violation (e.g., 'sev-0', 'sev-1', 'sev-2', etc.)." 155 + }, 156 + "strikeCount": { 157 + "type": "integer", 158 + "description": "Number of strikes to assign to the user when takedown is applied." 159 + }, 160 + "strikeExpiresAt": { 161 + "type": "string", 162 + "description": "When the strike should expire. If not provided, the strike never expires.", 163 + "format": "datetime" 143 164 } 144 165 } 145 166 }
+52 -50
crates/weaver-api/src/app_bsky/actor.rs
··· 3502 3502 /// Groups of users to apply the muted word to. If undefined, applies to all users. 3503 3503 #[serde(skip_serializing_if = "std::option::Option::is_none")] 3504 3504 #[serde(borrow)] 3505 - pub actor_target: Option<jacquard_common::CowStr<'a>>, 3505 + pub actor_target: std::option::Option<jacquard_common::CowStr<'a>>, 3506 3506 /// The date and time at which the muted word will expire and no longer be applied. 3507 3507 #[serde(skip_serializing_if = "std::option::Option::is_none")] 3508 - pub expires_at: Option<jacquard_common::types::string::Datetime>, 3508 + pub expires_at: std::option::Option<jacquard_common::types::string::Datetime>, 3509 3509 #[serde(skip_serializing_if = "std::option::Option::is_none")] 3510 3510 #[serde(borrow)] 3511 - pub id: Option<jacquard_common::CowStr<'a>>, 3511 + pub id: std::option::Option<jacquard_common::CowStr<'a>>, 3512 3512 /// The intended targets of the muted word. 3513 3513 #[serde(borrow)] 3514 3514 pub targets: Vec<crate::app_bsky::actor::MutedWordTarget<'a>>, ··· 3998 3998 /// Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters. 3999 3999 #[serde(skip_serializing_if = "std::option::Option::is_none")] 4000 4000 #[serde(borrow)] 4001 - pub data: Option<jacquard_common::CowStr<'a>>, 4001 + pub data: std::option::Option<jacquard_common::CowStr<'a>>, 4002 4002 /// The date and time at which the NUX will expire and should be considered completed. 4003 4003 #[serde(skip_serializing_if = "std::option::Option::is_none")] 4004 - pub expires_at: Option<jacquard_common::types::string::Datetime>, 4004 + pub expires_at: std::option::Option<jacquard_common::types::string::Datetime>, 4005 4005 #[serde(borrow)] 4006 4006 pub id: jacquard_common::CowStr<'a>, 4007 4007 } ··· 4551 4551 pub struct ProfileView<'a> { 4552 4552 #[serde(skip_serializing_if = "std::option::Option::is_none")] 4553 4553 #[serde(borrow)] 4554 - pub associated: Option<crate::app_bsky::actor::ProfileAssociated<'a>>, 4554 + pub associated: std::option::Option<crate::app_bsky::actor::ProfileAssociated<'a>>, 4555 4555 #[serde(skip_serializing_if = "std::option::Option::is_none")] 4556 4556 #[serde(borrow)] 4557 - pub avatar: Option<jacquard_common::types::string::Uri<'a>>, 4557 + pub avatar: std::option::Option<jacquard_common::types::string::Uri<'a>>, 4558 4558 #[serde(skip_serializing_if = "std::option::Option::is_none")] 4559 - pub created_at: Option<jacquard_common::types::string::Datetime>, 4559 + pub created_at: std::option::Option<jacquard_common::types::string::Datetime>, 4560 4560 /// Debug information for internal development 4561 4561 #[serde(skip_serializing_if = "std::option::Option::is_none")] 4562 4562 #[serde(borrow)] 4563 - pub debug: Option<jacquard_common::types::value::Data<'a>>, 4563 + pub debug: std::option::Option<jacquard_common::types::value::Data<'a>>, 4564 4564 #[serde(skip_serializing_if = "std::option::Option::is_none")] 4565 4565 #[serde(borrow)] 4566 - pub description: Option<jacquard_common::CowStr<'a>>, 4566 + pub description: std::option::Option<jacquard_common::CowStr<'a>>, 4567 4567 #[serde(borrow)] 4568 4568 pub did: jacquard_common::types::string::Did<'a>, 4569 4569 #[serde(skip_serializing_if = "std::option::Option::is_none")] 4570 4570 #[serde(borrow)] 4571 - pub display_name: Option<jacquard_common::CowStr<'a>>, 4571 + pub display_name: std::option::Option<jacquard_common::CowStr<'a>>, 4572 4572 #[serde(borrow)] 4573 4573 pub handle: jacquard_common::types::string::Handle<'a>, 4574 4574 #[serde(skip_serializing_if = "std::option::Option::is_none")] 4575 - pub indexed_at: Option<jacquard_common::types::string::Datetime>, 4575 + pub indexed_at: std::option::Option<jacquard_common::types::string::Datetime>, 4576 4576 #[serde(skip_serializing_if = "std::option::Option::is_none")] 4577 4577 #[serde(borrow)] 4578 - pub labels: Option<Vec<crate::com_atproto::label::Label<'a>>>, 4578 + pub labels: std::option::Option<Vec<crate::com_atproto::label::Label<'a>>>, 4579 4579 #[serde(skip_serializing_if = "std::option::Option::is_none")] 4580 4580 #[serde(borrow)] 4581 - pub pronouns: Option<jacquard_common::CowStr<'a>>, 4581 + pub pronouns: std::option::Option<jacquard_common::CowStr<'a>>, 4582 4582 #[serde(skip_serializing_if = "std::option::Option::is_none")] 4583 4583 #[serde(borrow)] 4584 - pub status: Option<crate::app_bsky::actor::StatusView<'a>>, 4584 + pub status: std::option::Option<crate::app_bsky::actor::StatusView<'a>>, 4585 4585 #[serde(skip_serializing_if = "std::option::Option::is_none")] 4586 4586 #[serde(borrow)] 4587 - pub verification: Option<crate::app_bsky::actor::VerificationState<'a>>, 4587 + pub verification: std::option::Option<crate::app_bsky::actor::VerificationState<'a>>, 4588 4588 #[serde(skip_serializing_if = "std::option::Option::is_none")] 4589 4589 #[serde(borrow)] 4590 - pub viewer: Option<crate::app_bsky::actor::ViewerState<'a>>, 4590 + pub viewer: std::option::Option<crate::app_bsky::actor::ViewerState<'a>>, 4591 4591 } 4592 4592 4593 4593 pub mod profile_view_state { ··· 5097 5097 pub struct ProfileViewBasic<'a> { 5098 5098 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5099 5099 #[serde(borrow)] 5100 - pub associated: Option<crate::app_bsky::actor::ProfileAssociated<'a>>, 5100 + pub associated: std::option::Option<crate::app_bsky::actor::ProfileAssociated<'a>>, 5101 5101 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5102 5102 #[serde(borrow)] 5103 - pub avatar: Option<jacquard_common::types::string::Uri<'a>>, 5103 + pub avatar: std::option::Option<jacquard_common::types::string::Uri<'a>>, 5104 5104 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5105 - pub created_at: Option<jacquard_common::types::string::Datetime>, 5105 + pub created_at: std::option::Option<jacquard_common::types::string::Datetime>, 5106 5106 /// Debug information for internal development 5107 5107 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5108 5108 #[serde(borrow)] 5109 - pub debug: Option<jacquard_common::types::value::Data<'a>>, 5109 + pub debug: std::option::Option<jacquard_common::types::value::Data<'a>>, 5110 5110 #[serde(borrow)] 5111 5111 pub did: jacquard_common::types::string::Did<'a>, 5112 5112 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5113 5113 #[serde(borrow)] 5114 - pub display_name: Option<jacquard_common::CowStr<'a>>, 5114 + pub display_name: std::option::Option<jacquard_common::CowStr<'a>>, 5115 5115 #[serde(borrow)] 5116 5116 pub handle: jacquard_common::types::string::Handle<'a>, 5117 5117 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5118 5118 #[serde(borrow)] 5119 - pub labels: Option<Vec<crate::com_atproto::label::Label<'a>>>, 5119 + pub labels: std::option::Option<Vec<crate::com_atproto::label::Label<'a>>>, 5120 5120 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5121 5121 #[serde(borrow)] 5122 - pub pronouns: Option<jacquard_common::CowStr<'a>>, 5122 + pub pronouns: std::option::Option<jacquard_common::CowStr<'a>>, 5123 5123 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5124 5124 #[serde(borrow)] 5125 - pub status: Option<crate::app_bsky::actor::StatusView<'a>>, 5125 + pub status: std::option::Option<crate::app_bsky::actor::StatusView<'a>>, 5126 5126 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5127 5127 #[serde(borrow)] 5128 - pub verification: Option<crate::app_bsky::actor::VerificationState<'a>>, 5128 + pub verification: std::option::Option<crate::app_bsky::actor::VerificationState<'a>>, 5129 5129 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5130 5130 #[serde(borrow)] 5131 - pub viewer: Option<crate::app_bsky::actor::ViewerState<'a>>, 5131 + pub viewer: std::option::Option<crate::app_bsky::actor::ViewerState<'a>>, 5132 5132 } 5133 5133 5134 5134 pub mod profile_view_basic_state { ··· 5562 5562 pub struct ProfileViewDetailed<'a> { 5563 5563 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5564 5564 #[serde(borrow)] 5565 - pub associated: Option<crate::app_bsky::actor::ProfileAssociated<'a>>, 5565 + pub associated: std::option::Option<crate::app_bsky::actor::ProfileAssociated<'a>>, 5566 5566 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5567 5567 #[serde(borrow)] 5568 - pub avatar: Option<jacquard_common::types::string::Uri<'a>>, 5568 + pub avatar: std::option::Option<jacquard_common::types::string::Uri<'a>>, 5569 5569 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5570 5570 #[serde(borrow)] 5571 - pub banner: Option<jacquard_common::types::string::Uri<'a>>, 5571 + pub banner: std::option::Option<jacquard_common::types::string::Uri<'a>>, 5572 5572 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5573 - pub created_at: Option<jacquard_common::types::string::Datetime>, 5573 + pub created_at: std::option::Option<jacquard_common::types::string::Datetime>, 5574 5574 /// Debug information for internal development 5575 5575 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5576 5576 #[serde(borrow)] 5577 - pub debug: Option<jacquard_common::types::value::Data<'a>>, 5577 + pub debug: std::option::Option<jacquard_common::types::value::Data<'a>>, 5578 5578 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5579 5579 #[serde(borrow)] 5580 - pub description: Option<jacquard_common::CowStr<'a>>, 5580 + pub description: std::option::Option<jacquard_common::CowStr<'a>>, 5581 5581 #[serde(borrow)] 5582 5582 pub did: jacquard_common::types::string::Did<'a>, 5583 5583 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5584 5584 #[serde(borrow)] 5585 - pub display_name: Option<jacquard_common::CowStr<'a>>, 5585 + pub display_name: std::option::Option<jacquard_common::CowStr<'a>>, 5586 5586 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5587 - pub followers_count: Option<i64>, 5587 + pub followers_count: std::option::Option<i64>, 5588 5588 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5589 - pub follows_count: Option<i64>, 5589 + pub follows_count: std::option::Option<i64>, 5590 5590 #[serde(borrow)] 5591 5591 pub handle: jacquard_common::types::string::Handle<'a>, 5592 5592 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5593 - pub indexed_at: Option<jacquard_common::types::string::Datetime>, 5593 + pub indexed_at: std::option::Option<jacquard_common::types::string::Datetime>, 5594 5594 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5595 5595 #[serde(borrow)] 5596 - pub joined_via_starter_pack: Option< 5596 + pub joined_via_starter_pack: std::option::Option< 5597 5597 crate::app_bsky::graph::StarterPackViewBasic<'a>, 5598 5598 >, 5599 5599 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5600 5600 #[serde(borrow)] 5601 - pub labels: Option<Vec<crate::com_atproto::label::Label<'a>>>, 5601 + pub labels: std::option::Option<Vec<crate::com_atproto::label::Label<'a>>>, 5602 5602 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5603 5603 #[serde(borrow)] 5604 - pub pinned_post: Option<crate::com_atproto::repo::strong_ref::StrongRef<'a>>, 5604 + pub pinned_post: std::option::Option< 5605 + crate::com_atproto::repo::strong_ref::StrongRef<'a>, 5606 + >, 5605 5607 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5606 - pub posts_count: Option<i64>, 5608 + pub posts_count: std::option::Option<i64>, 5607 5609 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5608 5610 #[serde(borrow)] 5609 - pub pronouns: Option<jacquard_common::CowStr<'a>>, 5611 + pub pronouns: std::option::Option<jacquard_common::CowStr<'a>>, 5610 5612 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5611 5613 #[serde(borrow)] 5612 - pub status: Option<crate::app_bsky::actor::StatusView<'a>>, 5614 + pub status: std::option::Option<crate::app_bsky::actor::StatusView<'a>>, 5613 5615 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5614 5616 #[serde(borrow)] 5615 - pub verification: Option<crate::app_bsky::actor::VerificationState<'a>>, 5617 + pub verification: std::option::Option<crate::app_bsky::actor::VerificationState<'a>>, 5616 5618 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5617 5619 #[serde(borrow)] 5618 - pub viewer: Option<crate::app_bsky::actor::ViewerState<'a>>, 5620 + pub viewer: std::option::Option<crate::app_bsky::actor::ViewerState<'a>>, 5619 5621 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5620 5622 #[serde(borrow)] 5621 - pub website: Option<jacquard_common::types::string::Uri<'a>>, 5623 + pub website: std::option::Option<jacquard_common::types::string::Uri<'a>>, 5622 5624 } 5623 5625 5624 5626 pub mod profile_view_detailed_state { ··· 6528 6530 #[serde(borrow)] 6529 6531 pub saved: Vec<jacquard_common::types::string::AtUri<'a>>, 6530 6532 #[serde(skip_serializing_if = "std::option::Option::is_none")] 6531 - pub timeline_index: Option<i64>, 6533 + pub timeline_index: std::option::Option<i64>, 6532 6534 } 6533 6535 6534 6536 pub mod saved_feeds_pref_state { ··· 6857 6859 /// An optional embed associated with the status. 6858 6860 #[serde(skip_serializing_if = "std::option::Option::is_none")] 6859 6861 #[serde(borrow)] 6860 - pub embed: Option<crate::app_bsky::embed::external::View<'a>>, 6862 + pub embed: std::option::Option<crate::app_bsky::embed::external::View<'a>>, 6861 6863 /// The date when this status will expire. The application might choose to no longer return the status after expiration. 6862 6864 #[serde(skip_serializing_if = "std::option::Option::is_none")] 6863 - pub expires_at: Option<jacquard_common::types::string::Datetime>, 6865 + pub expires_at: std::option::Option<jacquard_common::types::string::Datetime>, 6864 6866 /// True if the status is not expired, false if it is expired. Only present if expiration was set. 6865 6867 #[serde(skip_serializing_if = "std::option::Option::is_none")] 6866 - pub is_active: Option<bool>, 6868 + pub is_active: std::option::Option<bool>, 6867 6869 #[serde(borrow)] 6868 6870 pub record: jacquard_common::types::value::Data<'a>, 6869 6871 /// The status for the account.
+12 -10
crates/weaver-api/src/app_bsky/actor/profile.rs
··· 21 21 /// Small image to be displayed next to posts from account. AKA, 'profile picture' 22 22 #[serde(skip_serializing_if = "std::option::Option::is_none")] 23 23 #[serde(borrow)] 24 - pub avatar: Option<jacquard_common::types::blob::BlobRef<'a>>, 24 + pub avatar: std::option::Option<jacquard_common::types::blob::BlobRef<'a>>, 25 25 /// Larger horizontal image to display behind profile view. 26 26 #[serde(skip_serializing_if = "std::option::Option::is_none")] 27 27 #[serde(borrow)] 28 - pub banner: Option<jacquard_common::types::blob::BlobRef<'a>>, 28 + pub banner: std::option::Option<jacquard_common::types::blob::BlobRef<'a>>, 29 29 #[serde(skip_serializing_if = "std::option::Option::is_none")] 30 - pub created_at: Option<jacquard_common::types::string::Datetime>, 30 + pub created_at: std::option::Option<jacquard_common::types::string::Datetime>, 31 31 /// Free-form profile description text. 32 32 #[serde(skip_serializing_if = "std::option::Option::is_none")] 33 33 #[serde(borrow)] 34 - pub description: Option<jacquard_common::CowStr<'a>>, 34 + pub description: std::option::Option<jacquard_common::CowStr<'a>>, 35 35 #[serde(skip_serializing_if = "std::option::Option::is_none")] 36 36 #[serde(borrow)] 37 - pub display_name: Option<jacquard_common::CowStr<'a>>, 37 + pub display_name: std::option::Option<jacquard_common::CowStr<'a>>, 38 38 #[serde(skip_serializing_if = "std::option::Option::is_none")] 39 39 #[serde(borrow)] 40 - pub joined_via_starter_pack: Option< 40 + pub joined_via_starter_pack: std::option::Option< 41 41 crate::com_atproto::repo::strong_ref::StrongRef<'a>, 42 42 >, 43 43 /// Self-label values, specific to the Bluesky application, on the overall account. 44 44 #[serde(skip_serializing_if = "std::option::Option::is_none")] 45 45 #[serde(borrow)] 46 - pub labels: Option<crate::com_atproto::label::SelfLabels<'a>>, 46 + pub labels: std::option::Option<crate::com_atproto::label::SelfLabels<'a>>, 47 47 #[serde(skip_serializing_if = "std::option::Option::is_none")] 48 48 #[serde(borrow)] 49 - pub pinned_post: Option<crate::com_atproto::repo::strong_ref::StrongRef<'a>>, 49 + pub pinned_post: std::option::Option< 50 + crate::com_atproto::repo::strong_ref::StrongRef<'a>, 51 + >, 50 52 /// Free-form pronouns text. 51 53 #[serde(skip_serializing_if = "std::option::Option::is_none")] 52 54 #[serde(borrow)] 53 - pub pronouns: Option<jacquard_common::CowStr<'a>>, 55 + pub pronouns: std::option::Option<jacquard_common::CowStr<'a>>, 54 56 #[serde(skip_serializing_if = "std::option::Option::is_none")] 55 57 #[serde(borrow)] 56 - pub website: Option<jacquard_common::types::string::Uri<'a>>, 58 + pub website: std::option::Option<jacquard_common::types::string::Uri<'a>>, 57 59 } 58 60 59 61 pub mod profile_state {
+2 -2
crates/weaver-api/src/app_bsky/actor/status.rs
··· 39 39 pub created_at: jacquard_common::types::string::Datetime, 40 40 /// The duration of the status in minutes. Applications can choose to impose minimum and maximum limits. 41 41 #[serde(skip_serializing_if = "std::option::Option::is_none")] 42 - pub duration_minutes: Option<i64>, 42 + pub duration_minutes: std::option::Option<i64>, 43 43 /// An optional embed associated with the status. 44 44 #[serde(skip_serializing_if = "std::option::Option::is_none")] 45 45 #[serde(borrow)] 46 - pub embed: Option<crate::app_bsky::embed::external::ExternalRecord<'a>>, 46 + pub embed: std::option::Option<crate::app_bsky::embed::external::ExternalRecord<'a>>, 47 47 /// The status for the account. 48 48 #[serde(borrow)] 49 49 pub status: jacquard_common::CowStr<'a>,
+1 -1
crates/weaver-api/src/app_bsky/bookmark.rs
··· 263 263 #[serde(rename_all = "camelCase")] 264 264 pub struct BookmarkView<'a> { 265 265 #[serde(skip_serializing_if = "std::option::Option::is_none")] 266 - pub created_at: Option<jacquard_common::types::string::Datetime>, 266 + pub created_at: std::option::Option<jacquard_common::types::string::Datetime>, 267 267 #[serde(borrow)] 268 268 pub item: BookmarkViewItem<'a>, 269 269 /// A strong ref to the bookmarked record.
+2 -2
crates/weaver-api/src/app_bsky/embed/external.rs
··· 21 21 pub description: jacquard_common::CowStr<'a>, 22 22 #[serde(skip_serializing_if = "std::option::Option::is_none")] 23 23 #[serde(borrow)] 24 - pub thumb: Option<jacquard_common::types::blob::BlobRef<'a>>, 24 + pub thumb: std::option::Option<jacquard_common::types::blob::BlobRef<'a>>, 25 25 #[serde(borrow)] 26 26 pub title: jacquard_common::CowStr<'a>, 27 27 #[serde(borrow)] ··· 767 767 pub description: jacquard_common::CowStr<'a>, 768 768 #[serde(skip_serializing_if = "std::option::Option::is_none")] 769 769 #[serde(borrow)] 770 - pub thumb: Option<jacquard_common::types::string::Uri<'a>>, 770 + pub thumb: std::option::Option<jacquard_common::types::string::Uri<'a>>, 771 771 #[serde(borrow)] 772 772 pub title: jacquard_common::CowStr<'a>, 773 773 #[serde(borrow)]
+2 -2
crates/weaver-api/src/app_bsky/embed/images.rs
··· 22 22 pub alt: jacquard_common::CowStr<'a>, 23 23 #[serde(skip_serializing_if = "std::option::Option::is_none")] 24 24 #[serde(borrow)] 25 - pub aspect_ratio: Option<crate::app_bsky::embed::AspectRatio<'a>>, 25 + pub aspect_ratio: std::option::Option<crate::app_bsky::embed::AspectRatio<'a>>, 26 26 #[serde(borrow)] 27 27 pub image: jacquard_common::types::blob::BlobRef<'a>, 28 28 } ··· 741 741 pub alt: jacquard_common::CowStr<'a>, 742 742 #[serde(skip_serializing_if = "std::option::Option::is_none")] 743 743 #[serde(borrow)] 744 - pub aspect_ratio: Option<crate::app_bsky::embed::AspectRatio<'a>>, 744 + pub aspect_ratio: std::option::Option<crate::app_bsky::embed::AspectRatio<'a>>, 745 745 /// 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. 746 746 #[serde(borrow)] 747 747 pub fullsize: jacquard_common::types::string::Uri<'a>,
+6 -6
crates/weaver-api/src/app_bsky/embed/record.rs
··· 1273 1273 pub cid: jacquard_common::types::string::Cid<'a>, 1274 1274 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1275 1275 #[serde(borrow)] 1276 - pub embeds: Option<Vec<ViewRecordEmbedsItem<'a>>>, 1276 + pub embeds: std::option::Option<Vec<ViewRecordEmbedsItem<'a>>>, 1277 1277 pub indexed_at: jacquard_common::types::string::Datetime, 1278 1278 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1279 1279 #[serde(borrow)] 1280 - pub labels: Option<Vec<crate::com_atproto::label::Label<'a>>>, 1280 + pub labels: std::option::Option<Vec<crate::com_atproto::label::Label<'a>>>, 1281 1281 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1282 - pub like_count: Option<i64>, 1282 + pub like_count: std::option::Option<i64>, 1283 1283 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1284 - pub quote_count: Option<i64>, 1284 + pub quote_count: std::option::Option<i64>, 1285 1285 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1286 - pub reply_count: Option<i64>, 1286 + pub reply_count: std::option::Option<i64>, 1287 1287 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1288 - pub repost_count: Option<i64>, 1288 + pub repost_count: std::option::Option<i64>, 1289 1289 #[serde(borrow)] 1290 1290 pub uri: jacquard_common::types::string::AtUri<'a>, 1291 1291 /// The record data itself.
+6 -6
crates/weaver-api/src/app_bsky/embed/video.rs
··· 414 414 /// Alt text description of the video, for accessibility. 415 415 #[serde(skip_serializing_if = "std::option::Option::is_none")] 416 416 #[serde(borrow)] 417 - pub alt: Option<jacquard_common::CowStr<'a>>, 417 + pub alt: std::option::Option<jacquard_common::CowStr<'a>>, 418 418 #[serde(skip_serializing_if = "std::option::Option::is_none")] 419 419 #[serde(borrow)] 420 - pub aspect_ratio: Option<crate::app_bsky::embed::AspectRatio<'a>>, 420 + pub aspect_ratio: std::option::Option<crate::app_bsky::embed::AspectRatio<'a>>, 421 421 #[serde(skip_serializing_if = "std::option::Option::is_none")] 422 422 #[serde(borrow)] 423 - pub captions: Option<Vec<crate::app_bsky::embed::video::Caption<'a>>>, 423 + pub captions: std::option::Option<Vec<crate::app_bsky::embed::video::Caption<'a>>>, 424 424 /// The mp4 video file. May be up to 100mb, formerly limited to 50mb. 425 425 #[serde(borrow)] 426 426 pub video: jacquard_common::types::blob::BlobRef<'a>, ··· 664 664 pub struct View<'a> { 665 665 #[serde(skip_serializing_if = "std::option::Option::is_none")] 666 666 #[serde(borrow)] 667 - pub alt: Option<jacquard_common::CowStr<'a>>, 667 + pub alt: std::option::Option<jacquard_common::CowStr<'a>>, 668 668 #[serde(skip_serializing_if = "std::option::Option::is_none")] 669 669 #[serde(borrow)] 670 - pub aspect_ratio: Option<crate::app_bsky::embed::AspectRatio<'a>>, 670 + pub aspect_ratio: std::option::Option<crate::app_bsky::embed::AspectRatio<'a>>, 671 671 #[serde(borrow)] 672 672 pub cid: jacquard_common::types::string::Cid<'a>, 673 673 #[serde(borrow)] 674 674 pub playlist: jacquard_common::types::string::Uri<'a>, 675 675 #[serde(skip_serializing_if = "std::option::Option::is_none")] 676 676 #[serde(borrow)] 677 - pub thumbnail: Option<jacquard_common::types::string::Uri<'a>>, 677 + pub thumbnail: std::option::Option<jacquard_common::types::string::Uri<'a>>, 678 678 } 679 679 680 680 pub mod view_state {
+35 -31
crates/weaver-api/src/app_bsky/feed.rs
··· 46 46 pub did: jacquard_common::types::string::Did<'a>, 47 47 #[serde(skip_serializing_if = "std::option::Option::is_none")] 48 48 #[serde(borrow)] 49 - pub viewer: Option<crate::app_bsky::actor::ViewerState<'a>>, 49 + pub viewer: std::option::Option<crate::app_bsky::actor::ViewerState<'a>>, 50 50 } 51 51 52 52 pub mod blocked_author_state { ··· 1916 1916 /// Context provided by feed generator that may be passed back alongside interactions. 1917 1917 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1918 1918 #[serde(borrow)] 1919 - pub feed_context: Option<jacquard_common::CowStr<'a>>, 1919 + pub feed_context: std::option::Option<jacquard_common::CowStr<'a>>, 1920 1920 #[serde(borrow)] 1921 1921 pub post: crate::app_bsky::feed::PostView<'a>, 1922 1922 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1923 1923 #[serde(borrow)] 1924 - pub reason: Option<FeedViewPostReason<'a>>, 1924 + pub reason: std::option::Option<FeedViewPostReason<'a>>, 1925 1925 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1926 1926 #[serde(borrow)] 1927 - pub reply: Option<crate::app_bsky::feed::ReplyRef<'a>>, 1927 + pub reply: std::option::Option<crate::app_bsky::feed::ReplyRef<'a>>, 1928 1928 /// Unique identifier per request that may be passed back alongside interactions. 1929 1929 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1930 1930 #[serde(borrow)] 1931 - pub req_id: Option<jacquard_common::CowStr<'a>>, 1931 + pub req_id: std::option::Option<jacquard_common::CowStr<'a>>, 1932 1932 } 1933 1933 1934 1934 pub mod feed_view_post_state { ··· 2188 2188 #[serde(rename_all = "camelCase")] 2189 2189 pub struct GeneratorView<'a> { 2190 2190 #[serde(skip_serializing_if = "std::option::Option::is_none")] 2191 - pub accepts_interactions: Option<bool>, 2191 + pub accepts_interactions: std::option::Option<bool>, 2192 2192 #[serde(skip_serializing_if = "std::option::Option::is_none")] 2193 2193 #[serde(borrow)] 2194 - pub avatar: Option<jacquard_common::types::string::Uri<'a>>, 2194 + pub avatar: std::option::Option<jacquard_common::types::string::Uri<'a>>, 2195 2195 #[serde(borrow)] 2196 2196 pub cid: jacquard_common::types::string::Cid<'a>, 2197 2197 #[serde(skip_serializing_if = "std::option::Option::is_none")] 2198 2198 #[serde(borrow)] 2199 - pub content_mode: Option<jacquard_common::CowStr<'a>>, 2199 + pub content_mode: std::option::Option<jacquard_common::CowStr<'a>>, 2200 2200 #[serde(borrow)] 2201 2201 pub creator: crate::app_bsky::actor::ProfileView<'a>, 2202 2202 #[serde(skip_serializing_if = "std::option::Option::is_none")] 2203 2203 #[serde(borrow)] 2204 - pub description: Option<jacquard_common::CowStr<'a>>, 2204 + pub description: std::option::Option<jacquard_common::CowStr<'a>>, 2205 2205 #[serde(skip_serializing_if = "std::option::Option::is_none")] 2206 2206 #[serde(borrow)] 2207 - pub description_facets: Option<Vec<crate::app_bsky::richtext::facet::Facet<'a>>>, 2207 + pub description_facets: std::option::Option< 2208 + Vec<crate::app_bsky::richtext::facet::Facet<'a>>, 2209 + >, 2208 2210 #[serde(borrow)] 2209 2211 pub did: jacquard_common::types::string::Did<'a>, 2210 2212 #[serde(borrow)] ··· 2212 2214 pub indexed_at: jacquard_common::types::string::Datetime, 2213 2215 #[serde(skip_serializing_if = "std::option::Option::is_none")] 2214 2216 #[serde(borrow)] 2215 - pub labels: Option<Vec<crate::com_atproto::label::Label<'a>>>, 2217 + pub labels: std::option::Option<Vec<crate::com_atproto::label::Label<'a>>>, 2216 2218 #[serde(skip_serializing_if = "std::option::Option::is_none")] 2217 - pub like_count: Option<i64>, 2219 + pub like_count: std::option::Option<i64>, 2218 2220 #[serde(borrow)] 2219 2221 pub uri: jacquard_common::types::string::AtUri<'a>, 2220 2222 #[serde(skip_serializing_if = "std::option::Option::is_none")] 2221 2223 #[serde(borrow)] 2222 - pub viewer: Option<crate::app_bsky::feed::GeneratorViewerState<'a>>, 2224 + pub viewer: std::option::Option<crate::app_bsky::feed::GeneratorViewerState<'a>>, 2223 2225 } 2224 2226 2225 2227 pub mod generator_view_state { ··· 3161 3163 #[serde(borrow)] 3162 3164 pub author: crate::app_bsky::actor::ProfileViewBasic<'a>, 3163 3165 #[serde(skip_serializing_if = "std::option::Option::is_none")] 3164 - pub bookmark_count: Option<i64>, 3166 + pub bookmark_count: std::option::Option<i64>, 3165 3167 #[serde(borrow)] 3166 3168 pub cid: jacquard_common::types::string::Cid<'a>, 3167 3169 /// Debug information for internal development 3168 3170 #[serde(skip_serializing_if = "std::option::Option::is_none")] 3169 3171 #[serde(borrow)] 3170 - pub debug: Option<jacquard_common::types::value::Data<'a>>, 3172 + pub debug: std::option::Option<jacquard_common::types::value::Data<'a>>, 3171 3173 #[serde(skip_serializing_if = "std::option::Option::is_none")] 3172 3174 #[serde(borrow)] 3173 - pub embed: Option<PostViewEmbed<'a>>, 3175 + pub embed: std::option::Option<PostViewEmbed<'a>>, 3174 3176 pub indexed_at: jacquard_common::types::string::Datetime, 3175 3177 #[serde(skip_serializing_if = "std::option::Option::is_none")] 3176 3178 #[serde(borrow)] 3177 - pub labels: Option<Vec<crate::com_atproto::label::Label<'a>>>, 3179 + pub labels: std::option::Option<Vec<crate::com_atproto::label::Label<'a>>>, 3178 3180 #[serde(skip_serializing_if = "std::option::Option::is_none")] 3179 - pub like_count: Option<i64>, 3181 + pub like_count: std::option::Option<i64>, 3180 3182 #[serde(skip_serializing_if = "std::option::Option::is_none")] 3181 - pub quote_count: Option<i64>, 3183 + pub quote_count: std::option::Option<i64>, 3182 3184 #[serde(borrow)] 3183 3185 pub record: jacquard_common::types::value::Data<'a>, 3184 3186 #[serde(skip_serializing_if = "std::option::Option::is_none")] 3185 - pub reply_count: Option<i64>, 3187 + pub reply_count: std::option::Option<i64>, 3186 3188 #[serde(skip_serializing_if = "std::option::Option::is_none")] 3187 - pub repost_count: Option<i64>, 3189 + pub repost_count: std::option::Option<i64>, 3188 3190 #[serde(skip_serializing_if = "std::option::Option::is_none")] 3189 3191 #[serde(borrow)] 3190 - pub threadgate: Option<crate::app_bsky::feed::ThreadgateView<'a>>, 3192 + pub threadgate: std::option::Option<crate::app_bsky::feed::ThreadgateView<'a>>, 3191 3193 #[serde(borrow)] 3192 3194 pub uri: jacquard_common::types::string::AtUri<'a>, 3193 3195 #[serde(skip_serializing_if = "std::option::Option::is_none")] 3194 3196 #[serde(borrow)] 3195 - pub viewer: Option<crate::app_bsky::feed::ViewerState<'a>>, 3197 + pub viewer: std::option::Option<crate::app_bsky::feed::ViewerState<'a>>, 3196 3198 } 3197 3199 3198 3200 pub mod post_view_state { ··· 3740 3742 pub by: crate::app_bsky::actor::ProfileViewBasic<'a>, 3741 3743 #[serde(skip_serializing_if = "std::option::Option::is_none")] 3742 3744 #[serde(borrow)] 3743 - pub cid: Option<jacquard_common::types::string::Cid<'a>>, 3745 + pub cid: std::option::Option<jacquard_common::types::string::Cid<'a>>, 3744 3746 pub indexed_at: jacquard_common::types::string::Datetime, 3745 3747 #[serde(skip_serializing_if = "std::option::Option::is_none")] 3746 3748 #[serde(borrow)] 3747 - pub uri: Option<jacquard_common::types::string::AtUri<'a>>, 3749 + pub uri: std::option::Option<jacquard_common::types::string::AtUri<'a>>, 3748 3750 } 3749 3751 3750 3752 pub mod reason_repost_state { ··· 3963 3965 /// When parent is a reply to another post, this is the author of that post. 3964 3966 #[serde(skip_serializing_if = "std::option::Option::is_none")] 3965 3967 #[serde(borrow)] 3966 - pub grandparent_author: Option<crate::app_bsky::actor::ProfileViewBasic<'a>>, 3968 + pub grandparent_author: std::option::Option< 3969 + crate::app_bsky::actor::ProfileViewBasic<'a>, 3970 + >, 3967 3971 #[serde(borrow)] 3968 3972 pub parent: ReplyRefParent<'a>, 3969 3973 #[serde(borrow)] ··· 4242 4246 /// Context that will be passed through to client and may be passed to feed generator back alongside interactions. 4243 4247 #[serde(skip_serializing_if = "std::option::Option::is_none")] 4244 4248 #[serde(borrow)] 4245 - pub feed_context: Option<jacquard_common::CowStr<'a>>, 4249 + pub feed_context: std::option::Option<jacquard_common::CowStr<'a>>, 4246 4250 #[serde(borrow)] 4247 4251 pub post: jacquard_common::types::string::AtUri<'a>, 4248 4252 #[serde(skip_serializing_if = "std::option::Option::is_none")] 4249 4253 #[serde(borrow)] 4250 - pub reason: Option<SkeletonFeedPostReason<'a>>, 4254 + pub reason: std::option::Option<SkeletonFeedPostReason<'a>>, 4251 4255 } 4252 4256 4253 4257 pub mod skeleton_feed_post_state { ··· 4665 4669 pub struct ThreadViewPost<'a> { 4666 4670 #[serde(skip_serializing_if = "std::option::Option::is_none")] 4667 4671 #[serde(borrow)] 4668 - pub parent: Option<ThreadViewPostParent<'a>>, 4672 + pub parent: std::option::Option<ThreadViewPostParent<'a>>, 4669 4673 #[serde(borrow)] 4670 4674 pub post: crate::app_bsky::feed::PostView<'a>, 4671 4675 #[serde(skip_serializing_if = "std::option::Option::is_none")] 4672 4676 #[serde(borrow)] 4673 - pub replies: Option<Vec<ThreadViewPostRepliesItem<'a>>>, 4677 + pub replies: std::option::Option<Vec<ThreadViewPostRepliesItem<'a>>>, 4674 4678 #[serde(skip_serializing_if = "std::option::Option::is_none")] 4675 4679 #[serde(borrow)] 4676 - pub thread_context: Option<crate::app_bsky::feed::ThreadContext<'a>>, 4680 + pub thread_context: std::option::Option<crate::app_bsky::feed::ThreadContext<'a>>, 4677 4681 } 4678 4682 4679 4683 pub mod thread_view_post_state {
+8 -6
crates/weaver-api/src/app_bsky/feed/generator.rs
··· 20 20 pub struct Generator<'a> { 21 21 /// Declaration that a feed accepts feedback interactions from a client through app.bsky.feed.sendInteractions 22 22 #[serde(skip_serializing_if = "std::option::Option::is_none")] 23 - pub accepts_interactions: Option<bool>, 23 + pub accepts_interactions: std::option::Option<bool>, 24 24 #[serde(skip_serializing_if = "std::option::Option::is_none")] 25 25 #[serde(borrow)] 26 - pub avatar: Option<jacquard_common::types::blob::BlobRef<'a>>, 26 + pub avatar: std::option::Option<jacquard_common::types::blob::BlobRef<'a>>, 27 27 #[serde(skip_serializing_if = "std::option::Option::is_none")] 28 28 #[serde(borrow)] 29 - pub content_mode: Option<jacquard_common::CowStr<'a>>, 29 + pub content_mode: std::option::Option<jacquard_common::CowStr<'a>>, 30 30 pub created_at: jacquard_common::types::string::Datetime, 31 31 #[serde(skip_serializing_if = "std::option::Option::is_none")] 32 32 #[serde(borrow)] 33 - pub description: Option<jacquard_common::CowStr<'a>>, 33 + pub description: std::option::Option<jacquard_common::CowStr<'a>>, 34 34 #[serde(skip_serializing_if = "std::option::Option::is_none")] 35 35 #[serde(borrow)] 36 - pub description_facets: Option<Vec<crate::app_bsky::richtext::facet::Facet<'a>>>, 36 + pub description_facets: std::option::Option< 37 + Vec<crate::app_bsky::richtext::facet::Facet<'a>>, 38 + >, 37 39 #[serde(borrow)] 38 40 pub did: jacquard_common::types::string::Did<'a>, 39 41 #[serde(borrow)] ··· 41 43 /// Self-label values 42 44 #[serde(skip_serializing_if = "std::option::Option::is_none")] 43 45 #[serde(borrow)] 44 - pub labels: Option<crate::com_atproto::label::SelfLabels<'a>>, 46 + pub labels: std::option::Option<crate::com_atproto::label::SelfLabels<'a>>, 45 47 } 46 48 47 49 pub mod generator_state {
+1 -1
crates/weaver-api/src/app_bsky/feed/like.rs
··· 23 23 pub subject: crate::com_atproto::repo::strong_ref::StrongRef<'a>, 24 24 #[serde(skip_serializing_if = "std::option::Option::is_none")] 25 25 #[serde(borrow)] 26 - pub via: Option<crate::com_atproto::repo::strong_ref::StrongRef<'a>>, 26 + pub via: std::option::Option<crate::com_atproto::repo::strong_ref::StrongRef<'a>>, 27 27 } 28 28 29 29 pub mod like_state {
+7 -7
crates/weaver-api/src/app_bsky/feed/post.rs
··· 584 584 pub created_at: jacquard_common::types::string::Datetime, 585 585 #[serde(skip_serializing_if = "std::option::Option::is_none")] 586 586 #[serde(borrow)] 587 - pub embed: Option<PostEmbed<'a>>, 587 + pub embed: std::option::Option<PostEmbed<'a>>, 588 588 /// DEPRECATED: replaced by app.bsky.richtext.facet. 589 589 #[serde(skip_serializing_if = "std::option::Option::is_none")] 590 590 #[serde(borrow)] 591 - pub entities: Option<Vec<crate::app_bsky::feed::post::Entity<'a>>>, 591 + pub entities: std::option::Option<Vec<crate::app_bsky::feed::post::Entity<'a>>>, 592 592 /// Annotations of text (mentions, URLs, hashtags, etc) 593 593 #[serde(skip_serializing_if = "std::option::Option::is_none")] 594 594 #[serde(borrow)] 595 - pub facets: Option<Vec<crate::app_bsky::richtext::facet::Facet<'a>>>, 595 + pub facets: std::option::Option<Vec<crate::app_bsky::richtext::facet::Facet<'a>>>, 596 596 /// Self-label values for this post. Effectively content warnings. 597 597 #[serde(skip_serializing_if = "std::option::Option::is_none")] 598 598 #[serde(borrow)] 599 - pub labels: Option<crate::com_atproto::label::SelfLabels<'a>>, 599 + pub labels: std::option::Option<crate::com_atproto::label::SelfLabels<'a>>, 600 600 /// Indicates human language of post primary text content. 601 601 #[serde(skip_serializing_if = "std::option::Option::is_none")] 602 - pub langs: Option<Vec<jacquard_common::types::string::Language>>, 602 + pub langs: std::option::Option<Vec<jacquard_common::types::string::Language>>, 603 603 #[serde(skip_serializing_if = "std::option::Option::is_none")] 604 604 #[serde(borrow)] 605 - pub reply: Option<crate::app_bsky::feed::post::ReplyRef<'a>>, 605 + pub reply: std::option::Option<crate::app_bsky::feed::post::ReplyRef<'a>>, 606 606 /// Additional hashtags, in addition to any included in post text and facets. 607 607 #[serde(skip_serializing_if = "std::option::Option::is_none")] 608 608 #[serde(borrow)] 609 - pub tags: Option<Vec<jacquard_common::CowStr<'a>>>, 609 + pub tags: std::option::Option<Vec<jacquard_common::CowStr<'a>>>, 610 610 /// The primary post content. May be an empty string, if there are embeds. 611 611 #[serde(borrow)] 612 612 pub text: jacquard_common::CowStr<'a>,
+6 -2
crates/weaver-api/src/app_bsky/feed/postgate.rs
··· 200 200 /// List of AT-URIs embedding this post that the author has detached from. 201 201 #[serde(skip_serializing_if = "std::option::Option::is_none")] 202 202 #[serde(borrow)] 203 - pub detached_embedding_uris: Option<Vec<jacquard_common::types::string::AtUri<'a>>>, 203 + pub detached_embedding_uris: std::option::Option< 204 + Vec<jacquard_common::types::string::AtUri<'a>>, 205 + >, 204 206 /// 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. 205 207 #[serde(skip_serializing_if = "std::option::Option::is_none")] 206 208 #[serde(borrow)] 207 - pub embedding_rules: Option<Vec<crate::app_bsky::feed::postgate::DisableRule<'a>>>, 209 + pub embedding_rules: std::option::Option< 210 + Vec<crate::app_bsky::feed::postgate::DisableRule<'a>>, 211 + >, 208 212 /// Reference (AT-URI) to the post record. 209 213 #[serde(borrow)] 210 214 pub post: jacquard_common::types::string::AtUri<'a>,
+1 -1
crates/weaver-api/src/app_bsky/feed/repost.rs
··· 23 23 pub subject: crate::com_atproto::repo::strong_ref::StrongRef<'a>, 24 24 #[serde(skip_serializing_if = "std::option::Option::is_none")] 25 25 #[serde(borrow)] 26 - pub via: Option<crate::com_atproto::repo::strong_ref::StrongRef<'a>>, 26 + pub via: std::option::Option<crate::com_atproto::repo::strong_ref::StrongRef<'a>>, 27 27 } 28 28 29 29 pub mod repost_state {
+4 -2
crates/weaver-api/src/app_bsky/feed/threadgate.rs
··· 440 440 /// 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. 441 441 #[serde(skip_serializing_if = "std::option::Option::is_none")] 442 442 #[serde(borrow)] 443 - pub allow: Option<Vec<ThreadgateAllowItem<'a>>>, 443 + pub allow: std::option::Option<Vec<ThreadgateAllowItem<'a>>>, 444 444 pub created_at: jacquard_common::types::string::Datetime, 445 445 /// List of hidden reply URIs. 446 446 #[serde(skip_serializing_if = "std::option::Option::is_none")] 447 447 #[serde(borrow)] 448 - pub hidden_replies: Option<Vec<jacquard_common::types::string::AtUri<'a>>>, 448 + pub hidden_replies: std::option::Option< 449 + Vec<jacquard_common::types::string::AtUri<'a>>, 450 + >, 449 451 /// Reference (AT-URI) to the post record. 450 452 #[serde(borrow)] 451 453 pub post: jacquard_common::types::string::AtUri<'a>,
+27 -23
crates/weaver-api/src/app_bsky/graph.rs
··· 1237 1237 pub struct ListView<'a> { 1238 1238 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1239 1239 #[serde(borrow)] 1240 - pub avatar: Option<jacquard_common::types::string::Uri<'a>>, 1240 + pub avatar: std::option::Option<jacquard_common::types::string::Uri<'a>>, 1241 1241 #[serde(borrow)] 1242 1242 pub cid: jacquard_common::types::string::Cid<'a>, 1243 1243 #[serde(borrow)] 1244 1244 pub creator: crate::app_bsky::actor::ProfileView<'a>, 1245 1245 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1246 1246 #[serde(borrow)] 1247 - pub description: Option<jacquard_common::CowStr<'a>>, 1247 + pub description: std::option::Option<jacquard_common::CowStr<'a>>, 1248 1248 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1249 1249 #[serde(borrow)] 1250 - pub description_facets: Option<Vec<crate::app_bsky::richtext::facet::Facet<'a>>>, 1250 + pub description_facets: std::option::Option< 1251 + Vec<crate::app_bsky::richtext::facet::Facet<'a>>, 1252 + >, 1251 1253 pub indexed_at: jacquard_common::types::string::Datetime, 1252 1254 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1253 1255 #[serde(borrow)] 1254 - pub labels: Option<Vec<crate::com_atproto::label::Label<'a>>>, 1256 + pub labels: std::option::Option<Vec<crate::com_atproto::label::Label<'a>>>, 1255 1257 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1256 - pub list_item_count: Option<i64>, 1258 + pub list_item_count: std::option::Option<i64>, 1257 1259 #[serde(borrow)] 1258 1260 pub name: jacquard_common::CowStr<'a>, 1259 1261 #[serde(borrow)] ··· 1262 1264 pub uri: jacquard_common::types::string::AtUri<'a>, 1263 1265 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1264 1266 #[serde(borrow)] 1265 - pub viewer: Option<crate::app_bsky::graph::ListViewerState<'a>>, 1267 + pub viewer: std::option::Option<crate::app_bsky::graph::ListViewerState<'a>>, 1266 1268 } 1267 1269 1268 1270 pub mod list_view_state { ··· 1802 1804 pub struct ListViewBasic<'a> { 1803 1805 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1804 1806 #[serde(borrow)] 1805 - pub avatar: Option<jacquard_common::types::string::Uri<'a>>, 1807 + pub avatar: std::option::Option<jacquard_common::types::string::Uri<'a>>, 1806 1808 #[serde(borrow)] 1807 1809 pub cid: jacquard_common::types::string::Cid<'a>, 1808 1810 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1809 - pub indexed_at: Option<jacquard_common::types::string::Datetime>, 1811 + pub indexed_at: std::option::Option<jacquard_common::types::string::Datetime>, 1810 1812 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1811 1813 #[serde(borrow)] 1812 - pub labels: Option<Vec<crate::com_atproto::label::Label<'a>>>, 1814 + pub labels: std::option::Option<Vec<crate::com_atproto::label::Label<'a>>>, 1813 1815 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1814 - pub list_item_count: Option<i64>, 1816 + pub list_item_count: std::option::Option<i64>, 1815 1817 #[serde(borrow)] 1816 1818 pub name: jacquard_common::CowStr<'a>, 1817 1819 #[serde(borrow)] ··· 1820 1822 pub uri: jacquard_common::types::string::AtUri<'a>, 1821 1823 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1822 1824 #[serde(borrow)] 1823 - pub viewer: Option<crate::app_bsky::graph::ListViewerState<'a>>, 1825 + pub viewer: std::option::Option<crate::app_bsky::graph::ListViewerState<'a>>, 1824 1826 } 1825 1827 1826 1828 pub mod list_view_basic_state { ··· 2473 2475 /// if the actor is followed by this DID, contains the AT-URI of the follow record 2474 2476 #[serde(skip_serializing_if = "std::option::Option::is_none")] 2475 2477 #[serde(borrow)] 2476 - pub followed_by: Option<jacquard_common::types::string::AtUri<'a>>, 2478 + pub followed_by: std::option::Option<jacquard_common::types::string::AtUri<'a>>, 2477 2479 /// if the actor follows this DID, this is the AT-URI of the follow record 2478 2480 #[serde(skip_serializing_if = "std::option::Option::is_none")] 2479 2481 #[serde(borrow)] 2480 - pub following: Option<jacquard_common::types::string::AtUri<'a>>, 2482 + pub following: std::option::Option<jacquard_common::types::string::AtUri<'a>>, 2481 2483 } 2482 2484 2483 2485 pub mod relationship_state { ··· 2664 2666 pub creator: crate::app_bsky::actor::ProfileViewBasic<'a>, 2665 2667 #[serde(skip_serializing_if = "std::option::Option::is_none")] 2666 2668 #[serde(borrow)] 2667 - pub feeds: Option<Vec<crate::app_bsky::feed::GeneratorView<'a>>>, 2669 + pub feeds: std::option::Option<Vec<crate::app_bsky::feed::GeneratorView<'a>>>, 2668 2670 pub indexed_at: jacquard_common::types::string::Datetime, 2669 2671 #[serde(skip_serializing_if = "std::option::Option::is_none")] 2670 - pub joined_all_time_count: Option<i64>, 2672 + pub joined_all_time_count: std::option::Option<i64>, 2671 2673 #[serde(skip_serializing_if = "std::option::Option::is_none")] 2672 - pub joined_week_count: Option<i64>, 2674 + pub joined_week_count: std::option::Option<i64>, 2673 2675 #[serde(skip_serializing_if = "std::option::Option::is_none")] 2674 2676 #[serde(borrow)] 2675 - pub labels: Option<Vec<crate::com_atproto::label::Label<'a>>>, 2677 + pub labels: std::option::Option<Vec<crate::com_atproto::label::Label<'a>>>, 2676 2678 #[serde(skip_serializing_if = "std::option::Option::is_none")] 2677 2679 #[serde(borrow)] 2678 - pub list: Option<crate::app_bsky::graph::ListViewBasic<'a>>, 2680 + pub list: std::option::Option<crate::app_bsky::graph::ListViewBasic<'a>>, 2679 2681 #[serde(skip_serializing_if = "std::option::Option::is_none")] 2680 2682 #[serde(borrow)] 2681 - pub list_items_sample: Option<Vec<crate::app_bsky::graph::ListItemView<'a>>>, 2683 + pub list_items_sample: std::option::Option< 2684 + Vec<crate::app_bsky::graph::ListItemView<'a>>, 2685 + >, 2682 2686 #[serde(borrow)] 2683 2687 pub record: jacquard_common::types::value::Data<'a>, 2684 2688 #[serde(borrow)] ··· 3155 3159 pub creator: crate::app_bsky::actor::ProfileViewBasic<'a>, 3156 3160 pub indexed_at: jacquard_common::types::string::Datetime, 3157 3161 #[serde(skip_serializing_if = "std::option::Option::is_none")] 3158 - pub joined_all_time_count: Option<i64>, 3162 + pub joined_all_time_count: std::option::Option<i64>, 3159 3163 #[serde(skip_serializing_if = "std::option::Option::is_none")] 3160 - pub joined_week_count: Option<i64>, 3164 + pub joined_week_count: std::option::Option<i64>, 3161 3165 #[serde(skip_serializing_if = "std::option::Option::is_none")] 3162 3166 #[serde(borrow)] 3163 - pub labels: Option<Vec<crate::com_atproto::label::Label<'a>>>, 3167 + pub labels: std::option::Option<Vec<crate::com_atproto::label::Label<'a>>>, 3164 3168 #[serde(skip_serializing_if = "std::option::Option::is_none")] 3165 - pub list_item_count: Option<i64>, 3169 + pub list_item_count: std::option::Option<i64>, 3166 3170 #[serde(borrow)] 3167 3171 pub record: jacquard_common::types::value::Data<'a>, 3168 3172 #[serde(borrow)]
+1 -1
crates/weaver-api/src/app_bsky/graph/follow.rs
··· 23 23 pub subject: jacquard_common::types::string::Did<'a>, 24 24 #[serde(skip_serializing_if = "std::option::Option::is_none")] 25 25 #[serde(borrow)] 26 - pub via: Option<crate::com_atproto::repo::strong_ref::StrongRef<'a>>, 26 + pub via: std::option::Option<crate::com_atproto::repo::strong_ref::StrongRef<'a>>, 27 27 } 28 28 29 29 pub mod follow_state {
+1 -1
crates/weaver-api/src/app_bsky/graph/get_lists_with_membership.rs
··· 22 22 pub list: crate::app_bsky::graph::ListView<'a>, 23 23 #[serde(skip_serializing_if = "std::option::Option::is_none")] 24 24 #[serde(borrow)] 25 - pub list_item: Option<crate::app_bsky::graph::ListItemView<'a>>, 25 + pub list_item: std::option::Option<crate::app_bsky::graph::ListItemView<'a>>, 26 26 } 27 27 28 28 pub mod list_with_membership_state {
+1 -1
crates/weaver-api/src/app_bsky/graph/get_starter_packs_with_membership.rs
··· 234 234 pub struct StarterPackWithMembership<'a> { 235 235 #[serde(skip_serializing_if = "std::option::Option::is_none")] 236 236 #[serde(borrow)] 237 - pub list_item: Option<crate::app_bsky::graph::ListItemView<'a>>, 237 + pub list_item: std::option::Option<crate::app_bsky::graph::ListItemView<'a>>, 238 238 #[serde(borrow)] 239 239 pub starter_pack: crate::app_bsky::graph::StarterPackView<'a>, 240 240 }
+6 -4
crates/weaver-api/src/app_bsky/graph/list.rs
··· 20 20 pub struct List<'a> { 21 21 #[serde(skip_serializing_if = "std::option::Option::is_none")] 22 22 #[serde(borrow)] 23 - pub avatar: Option<jacquard_common::types::blob::BlobRef<'a>>, 23 + pub avatar: std::option::Option<jacquard_common::types::blob::BlobRef<'a>>, 24 24 pub created_at: jacquard_common::types::string::Datetime, 25 25 #[serde(skip_serializing_if = "std::option::Option::is_none")] 26 26 #[serde(borrow)] 27 - pub description: Option<jacquard_common::CowStr<'a>>, 27 + pub description: std::option::Option<jacquard_common::CowStr<'a>>, 28 28 #[serde(skip_serializing_if = "std::option::Option::is_none")] 29 29 #[serde(borrow)] 30 - pub description_facets: Option<Vec<crate::app_bsky::richtext::facet::Facet<'a>>>, 30 + pub description_facets: std::option::Option< 31 + Vec<crate::app_bsky::richtext::facet::Facet<'a>>, 32 + >, 31 33 #[serde(skip_serializing_if = "std::option::Option::is_none")] 32 34 #[serde(borrow)] 33 - pub labels: Option<crate::com_atproto::label::SelfLabels<'a>>, 35 + pub labels: std::option::Option<crate::com_atproto::label::SelfLabels<'a>>, 34 36 /// Display name for list; can not be empty. 35 37 #[serde(borrow)] 36 38 pub name: jacquard_common::CowStr<'a>,
+7 -3
crates/weaver-api/src/app_bsky/graph/starterpack.rs
··· 337 337 pub created_at: jacquard_common::types::string::Datetime, 338 338 #[serde(skip_serializing_if = "std::option::Option::is_none")] 339 339 #[serde(borrow)] 340 - pub description: Option<jacquard_common::CowStr<'a>>, 340 + pub description: std::option::Option<jacquard_common::CowStr<'a>>, 341 341 #[serde(skip_serializing_if = "std::option::Option::is_none")] 342 342 #[serde(borrow)] 343 - pub description_facets: Option<Vec<crate::app_bsky::richtext::facet::Facet<'a>>>, 343 + pub description_facets: std::option::Option< 344 + Vec<crate::app_bsky::richtext::facet::Facet<'a>>, 345 + >, 344 346 #[serde(skip_serializing_if = "std::option::Option::is_none")] 345 347 #[serde(borrow)] 346 - pub feeds: Option<Vec<crate::app_bsky::graph::starterpack::FeedItem<'a>>>, 348 + pub feeds: std::option::Option< 349 + Vec<crate::app_bsky::graph::starterpack::FeedItem<'a>>, 350 + >, 347 351 /// Reference (AT-URI) to the list record. 348 352 #[serde(borrow)] 349 353 pub list: jacquard_common::types::string::AtUri<'a>,
+16 -10
crates/weaver-api/src/app_bsky/labeler.rs
··· 23 23 /// Label values created by this labeler and scoped exclusively to it. Labels defined here will override global label definitions for this labeler. 24 24 #[serde(skip_serializing_if = "std::option::Option::is_none")] 25 25 #[serde(borrow)] 26 - pub label_value_definitions: Option< 26 + pub label_value_definitions: std::option::Option< 27 27 Vec<crate::com_atproto::label::LabelValueDefinition<'a>>, 28 28 >, 29 29 /// The label values which this labeler publishes. May include global or custom labels. ··· 612 612 pub indexed_at: jacquard_common::types::string::Datetime, 613 613 #[serde(skip_serializing_if = "std::option::Option::is_none")] 614 614 #[serde(borrow)] 615 - pub labels: Option<Vec<crate::com_atproto::label::Label<'a>>>, 615 + pub labels: std::option::Option<Vec<crate::com_atproto::label::Label<'a>>>, 616 616 #[serde(skip_serializing_if = "std::option::Option::is_none")] 617 - pub like_count: Option<i64>, 617 + pub like_count: std::option::Option<i64>, 618 618 #[serde(borrow)] 619 619 pub uri: jacquard_common::types::string::AtUri<'a>, 620 620 #[serde(skip_serializing_if = "std::option::Option::is_none")] 621 621 #[serde(borrow)] 622 - pub viewer: Option<crate::app_bsky::labeler::LabelerViewerState<'a>>, 622 + pub viewer: std::option::Option<crate::app_bsky::labeler::LabelerViewerState<'a>>, 623 623 } 624 624 625 625 pub mod labeler_view_state { ··· 945 945 pub indexed_at: jacquard_common::types::string::Datetime, 946 946 #[serde(skip_serializing_if = "std::option::Option::is_none")] 947 947 #[serde(borrow)] 948 - pub labels: Option<Vec<crate::com_atproto::label::Label<'a>>>, 948 + pub labels: std::option::Option<Vec<crate::com_atproto::label::Label<'a>>>, 949 949 #[serde(skip_serializing_if = "std::option::Option::is_none")] 950 - pub like_count: Option<i64>, 950 + pub like_count: std::option::Option<i64>, 951 951 #[serde(borrow)] 952 952 pub policies: crate::app_bsky::labeler::LabelerPolicies<'a>, 953 953 /// 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. 954 954 #[serde(skip_serializing_if = "std::option::Option::is_none")] 955 955 #[serde(borrow)] 956 - pub reason_types: Option<Vec<crate::com_atproto::moderation::ReasonType<'a>>>, 956 + pub reason_types: std::option::Option< 957 + Vec<crate::com_atproto::moderation::ReasonType<'a>>, 958 + >, 957 959 /// 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. 958 960 #[serde(skip_serializing_if = "std::option::Option::is_none")] 959 961 #[serde(borrow)] 960 - pub subject_collections: Option<Vec<jacquard_common::types::string::Nsid<'a>>>, 962 + pub subject_collections: std::option::Option< 963 + Vec<jacquard_common::types::string::Nsid<'a>>, 964 + >, 961 965 /// The set of subject types (account, record, etc) this service accepts reports on. 962 966 #[serde(skip_serializing_if = "std::option::Option::is_none")] 963 967 #[serde(borrow)] 964 - pub subject_types: Option<Vec<crate::com_atproto::moderation::SubjectType<'a>>>, 968 + pub subject_types: std::option::Option< 969 + Vec<crate::com_atproto::moderation::SubjectType<'a>>, 970 + >, 965 971 #[serde(borrow)] 966 972 pub uri: jacquard_common::types::string::AtUri<'a>, 967 973 #[serde(skip_serializing_if = "std::option::Option::is_none")] 968 974 #[serde(borrow)] 969 - pub viewer: Option<crate::app_bsky::labeler::LabelerViewerState<'a>>, 975 + pub viewer: std::option::Option<crate::app_bsky::labeler::LabelerViewerState<'a>>, 970 976 } 971 977 972 978 pub mod labeler_view_detailed_state {
+10 -4
crates/weaver-api/src/app_bsky/labeler/service.rs
··· 21 21 pub created_at: jacquard_common::types::string::Datetime, 22 22 #[serde(skip_serializing_if = "std::option::Option::is_none")] 23 23 #[serde(borrow)] 24 - pub labels: Option<crate::com_atproto::label::SelfLabels<'a>>, 24 + pub labels: std::option::Option<crate::com_atproto::label::SelfLabels<'a>>, 25 25 #[serde(borrow)] 26 26 pub policies: crate::app_bsky::labeler::LabelerPolicies<'a>, 27 27 /// 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. 28 28 #[serde(skip_serializing_if = "std::option::Option::is_none")] 29 29 #[serde(borrow)] 30 - pub reason_types: Option<Vec<crate::com_atproto::moderation::ReasonType<'a>>>, 30 + pub reason_types: std::option::Option< 31 + Vec<crate::com_atproto::moderation::ReasonType<'a>>, 32 + >, 31 33 /// 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. 32 34 #[serde(skip_serializing_if = "std::option::Option::is_none")] 33 35 #[serde(borrow)] 34 - pub subject_collections: Option<Vec<jacquard_common::types::string::Nsid<'a>>>, 36 + pub subject_collections: std::option::Option< 37 + Vec<jacquard_common::types::string::Nsid<'a>>, 38 + >, 35 39 /// The set of subject types (account, record, etc) this service accepts reports on. 36 40 #[serde(skip_serializing_if = "std::option::Option::is_none")] 37 41 #[serde(borrow)] 38 - pub subject_types: Option<Vec<crate::com_atproto::moderation::SubjectType<'a>>>, 42 + pub subject_types: std::option::Option< 43 + Vec<crate::com_atproto::moderation::SubjectType<'a>>, 44 + >, 39 45 } 40 46 41 47 pub mod service_state {
+2 -2
crates/weaver-api/src/app_bsky/notification/list_notifications.rs
··· 248 248 pub is_read: bool, 249 249 #[serde(skip_serializing_if = "std::option::Option::is_none")] 250 250 #[serde(borrow)] 251 - pub labels: Option<Vec<crate::com_atproto::label::Label<'a>>>, 251 + pub labels: std::option::Option<Vec<crate::com_atproto::label::Label<'a>>>, 252 252 /// The reason why this notification was delivered - e.g. your post was liked, or you received a new follower. 253 253 #[serde(borrow)] 254 254 pub reason: jacquard_common::CowStr<'a>, 255 255 #[serde(skip_serializing_if = "std::option::Option::is_none")] 256 256 #[serde(borrow)] 257 - pub reason_subject: Option<jacquard_common::types::string::AtUri<'a>>, 257 + pub reason_subject: std::option::Option<jacquard_common::types::string::AtUri<'a>>, 258 258 #[serde(borrow)] 259 259 pub record: jacquard_common::types::value::Data<'a>, 260 260 #[serde(borrow)]
+1 -1
crates/weaver-api/src/app_bsky/notification/register_push.rs
··· 19 19 pub struct RegisterPush<'a> { 20 20 /// Set to true when the actor is age restricted 21 21 #[serde(skip_serializing_if = "std::option::Option::is_none")] 22 - pub age_restricted: Option<bool>, 22 + pub age_restricted: std::option::Option<bool>, 23 23 #[serde(borrow)] 24 24 pub app_id: jacquard_common::CowStr<'a>, 25 25 #[serde(borrow)]
+9 -9
crates/weaver-api/src/app_bsky/unspecced.rs
··· 47 47 /// The IP address used when completing the AA flow. 48 48 #[serde(skip_serializing_if = "std::option::Option::is_none")] 49 49 #[serde(borrow)] 50 - pub complete_ip: Option<jacquard_common::CowStr<'a>>, 50 + pub complete_ip: std::option::Option<jacquard_common::CowStr<'a>>, 51 51 /// The user agent used when completing the AA flow. 52 52 #[serde(skip_serializing_if = "std::option::Option::is_none")] 53 53 #[serde(borrow)] 54 - pub complete_ua: Option<jacquard_common::CowStr<'a>>, 54 + pub complete_ua: std::option::Option<jacquard_common::CowStr<'a>>, 55 55 /// The date and time of this write operation. 56 56 pub created_at: jacquard_common::types::string::Datetime, 57 57 /// The email used for AA. 58 58 #[serde(skip_serializing_if = "std::option::Option::is_none")] 59 59 #[serde(borrow)] 60 - pub email: Option<jacquard_common::CowStr<'a>>, 60 + pub email: std::option::Option<jacquard_common::CowStr<'a>>, 61 61 /// The IP address used when initiating the AA flow. 62 62 #[serde(skip_serializing_if = "std::option::Option::is_none")] 63 63 #[serde(borrow)] 64 - pub init_ip: Option<jacquard_common::CowStr<'a>>, 64 + pub init_ip: std::option::Option<jacquard_common::CowStr<'a>>, 65 65 /// The user agent used when initiating the AA flow. 66 66 #[serde(skip_serializing_if = "std::option::Option::is_none")] 67 67 #[serde(borrow)] 68 - pub init_ua: Option<jacquard_common::CowStr<'a>>, 68 + pub init_ua: std::option::Option<jacquard_common::CowStr<'a>>, 69 69 /// The status of the age assurance process. 70 70 #[serde(borrow)] 71 71 pub status: jacquard_common::CowStr<'a>, ··· 1717 1717 pub struct SkeletonTrend<'a> { 1718 1718 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1719 1719 #[serde(borrow)] 1720 - pub category: Option<jacquard_common::CowStr<'a>>, 1720 + pub category: std::option::Option<jacquard_common::CowStr<'a>>, 1721 1721 #[serde(borrow)] 1722 1722 pub dids: Vec<jacquard_common::types::string::Did<'a>>, 1723 1723 #[serde(borrow)] ··· 1728 1728 pub started_at: jacquard_common::types::string::Datetime, 1729 1729 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1730 1730 #[serde(borrow)] 1731 - pub status: Option<jacquard_common::CowStr<'a>>, 1731 + pub status: std::option::Option<jacquard_common::CowStr<'a>>, 1732 1732 #[serde(borrow)] 1733 1733 pub topic: jacquard_common::CowStr<'a>, 1734 1734 } ··· 2645 2645 pub actors: Vec<crate::app_bsky::actor::ProfileViewBasic<'a>>, 2646 2646 #[serde(skip_serializing_if = "std::option::Option::is_none")] 2647 2647 #[serde(borrow)] 2648 - pub category: Option<jacquard_common::CowStr<'a>>, 2648 + pub category: std::option::Option<jacquard_common::CowStr<'a>>, 2649 2649 #[serde(borrow)] 2650 2650 pub display_name: jacquard_common::CowStr<'a>, 2651 2651 #[serde(borrow)] ··· 2654 2654 pub started_at: jacquard_common::types::string::Datetime, 2655 2655 #[serde(skip_serializing_if = "std::option::Option::is_none")] 2656 2656 #[serde(borrow)] 2657 - pub status: Option<jacquard_common::CowStr<'a>>, 2657 + pub status: std::option::Option<jacquard_common::CowStr<'a>>, 2658 2658 #[serde(borrow)] 2659 2659 pub topic: jacquard_common::CowStr<'a>, 2660 2660 }
+4 -4
crates/weaver-api/src/app_bsky/video.rs
··· 23 23 pub struct JobStatus<'a> { 24 24 #[serde(skip_serializing_if = "std::option::Option::is_none")] 25 25 #[serde(borrow)] 26 - pub blob: Option<jacquard_common::types::blob::BlobRef<'a>>, 26 + pub blob: std::option::Option<jacquard_common::types::blob::BlobRef<'a>>, 27 27 #[serde(borrow)] 28 28 pub did: jacquard_common::types::string::Did<'a>, 29 29 #[serde(skip_serializing_if = "std::option::Option::is_none")] 30 30 #[serde(borrow)] 31 - pub error: Option<jacquard_common::CowStr<'a>>, 31 + pub error: std::option::Option<jacquard_common::CowStr<'a>>, 32 32 #[serde(borrow)] 33 33 pub job_id: jacquard_common::CowStr<'a>, 34 34 #[serde(skip_serializing_if = "std::option::Option::is_none")] 35 35 #[serde(borrow)] 36 - pub message: Option<jacquard_common::CowStr<'a>>, 36 + pub message: std::option::Option<jacquard_common::CowStr<'a>>, 37 37 /// Progress within the current processing state. 38 38 #[serde(skip_serializing_if = "std::option::Option::is_none")] 39 - pub progress: Option<i64>, 39 + pub progress: std::option::Option<i64>, 40 40 /// The state of the video processing job. All values not listed as a known value indicate that the job is in process. 41 41 #[serde(borrow)] 42 42 pub state: jacquard_common::CowStr<'a>,
+7 -7
crates/weaver-api/src/chat_bsky/actor.rs
··· 23 23 pub struct ProfileViewBasic<'a> { 24 24 #[serde(skip_serializing_if = "std::option::Option::is_none")] 25 25 #[serde(borrow)] 26 - pub associated: Option<crate::app_bsky::actor::ProfileAssociated<'a>>, 26 + pub associated: std::option::Option<crate::app_bsky::actor::ProfileAssociated<'a>>, 27 27 #[serde(skip_serializing_if = "std::option::Option::is_none")] 28 28 #[serde(borrow)] 29 - pub avatar: Option<jacquard_common::types::string::Uri<'a>>, 29 + pub avatar: std::option::Option<jacquard_common::types::string::Uri<'a>>, 30 30 /// Set to true when the actor cannot actively participate in conversations 31 31 #[serde(skip_serializing_if = "std::option::Option::is_none")] 32 - pub chat_disabled: Option<bool>, 32 + pub chat_disabled: std::option::Option<bool>, 33 33 #[serde(borrow)] 34 34 pub did: jacquard_common::types::string::Did<'a>, 35 35 #[serde(skip_serializing_if = "std::option::Option::is_none")] 36 36 #[serde(borrow)] 37 - pub display_name: Option<jacquard_common::CowStr<'a>>, 37 + pub display_name: std::option::Option<jacquard_common::CowStr<'a>>, 38 38 #[serde(borrow)] 39 39 pub handle: jacquard_common::types::string::Handle<'a>, 40 40 #[serde(skip_serializing_if = "std::option::Option::is_none")] 41 41 #[serde(borrow)] 42 - pub labels: Option<Vec<crate::com_atproto::label::Label<'a>>>, 42 + pub labels: std::option::Option<Vec<crate::com_atproto::label::Label<'a>>>, 43 43 #[serde(skip_serializing_if = "std::option::Option::is_none")] 44 44 #[serde(borrow)] 45 - pub verification: Option<crate::app_bsky::actor::VerificationState<'a>>, 45 + pub verification: std::option::Option<crate::app_bsky::actor::VerificationState<'a>>, 46 46 #[serde(skip_serializing_if = "std::option::Option::is_none")] 47 47 #[serde(borrow)] 48 - pub viewer: Option<crate::app_bsky::actor::ViewerState<'a>>, 48 + pub viewer: std::option::Option<crate::app_bsky::actor::ViewerState<'a>>, 49 49 } 50 50 51 51 pub mod profile_view_basic_state {
+8 -6
crates/weaver-api/src/chat_bsky/convo.rs
··· 39 39 pub id: jacquard_common::CowStr<'a>, 40 40 #[serde(skip_serializing_if = "std::option::Option::is_none")] 41 41 #[serde(borrow)] 42 - pub last_message: Option<ConvoViewLastMessage<'a>>, 42 + pub last_message: std::option::Option<ConvoViewLastMessage<'a>>, 43 43 #[serde(skip_serializing_if = "std::option::Option::is_none")] 44 44 #[serde(borrow)] 45 - pub last_reaction: Option<crate::chat_bsky::convo::MessageAndReactionView<'a>>, 45 + pub last_reaction: std::option::Option< 46 + crate::chat_bsky::convo::MessageAndReactionView<'a>, 47 + >, 46 48 #[serde(borrow)] 47 49 pub members: Vec<crate::chat_bsky::actor::ProfileViewBasic<'a>>, 48 50 pub muted: bool, ··· 50 52 pub rev: jacquard_common::CowStr<'a>, 51 53 #[serde(skip_serializing_if = "std::option::Option::is_none")] 52 54 #[serde(borrow)] 53 - pub status: Option<jacquard_common::CowStr<'a>>, 55 + pub status: std::option::Option<jacquard_common::CowStr<'a>>, 54 56 pub unread_count: i64, 55 57 } 56 58 ··· 3781 3783 pub struct MessageView<'a> { 3782 3784 #[serde(skip_serializing_if = "std::option::Option::is_none")] 3783 3785 #[serde(borrow)] 3784 - pub embed: Option<crate::app_bsky::embed::record::View<'a>>, 3786 + pub embed: std::option::Option<crate::app_bsky::embed::record::View<'a>>, 3785 3787 /// Annotations of text (mentions, URLs, hashtags, etc) 3786 3788 #[serde(skip_serializing_if = "std::option::Option::is_none")] 3787 3789 #[serde(borrow)] 3788 - pub facets: Option<Vec<crate::app_bsky::richtext::facet::Facet<'a>>>, 3790 + pub facets: std::option::Option<Vec<crate::app_bsky::richtext::facet::Facet<'a>>>, 3789 3791 #[serde(borrow)] 3790 3792 pub id: jacquard_common::CowStr<'a>, 3791 3793 /// Reactions to this message, in ascending order of creation time. 3792 3794 #[serde(skip_serializing_if = "std::option::Option::is_none")] 3793 3795 #[serde(borrow)] 3794 - pub reactions: Option<Vec<crate::chat_bsky::convo::ReactionView<'a>>>, 3796 + pub reactions: std::option::Option<Vec<crate::chat_bsky::convo::ReactionView<'a>>>, 3795 3797 #[serde(borrow)] 3796 3798 pub rev: jacquard_common::CowStr<'a>, 3797 3799 #[serde(borrow)]
+1 -1
crates/weaver-api/src/chat_bsky/moderation/update_actor_access.rs
··· 22 22 pub allow_access: bool, 23 23 #[serde(skip_serializing_if = "std::option::Option::is_none")] 24 24 #[serde(borrow)] 25 - pub r#ref: Option<jacquard_common::CowStr<'a>>, 25 + pub r#ref: std::option::Option<jacquard_common::CowStr<'a>>, 26 26 } 27 27 28 28 pub mod update_actor_access_state {
+17 -11
crates/weaver-api/src/com_atproto/admin.rs
··· 34 34 #[serde(rename_all = "camelCase")] 35 35 pub struct AccountView<'a> { 36 36 #[serde(skip_serializing_if = "std::option::Option::is_none")] 37 - pub deactivated_at: Option<jacquard_common::types::string::Datetime>, 37 + pub deactivated_at: std::option::Option<jacquard_common::types::string::Datetime>, 38 38 #[serde(borrow)] 39 39 pub did: jacquard_common::types::string::Did<'a>, 40 40 #[serde(skip_serializing_if = "std::option::Option::is_none")] 41 41 #[serde(borrow)] 42 - pub email: Option<jacquard_common::CowStr<'a>>, 42 + pub email: std::option::Option<jacquard_common::CowStr<'a>>, 43 43 #[serde(skip_serializing_if = "std::option::Option::is_none")] 44 - pub email_confirmed_at: Option<jacquard_common::types::string::Datetime>, 44 + pub email_confirmed_at: std::option::Option< 45 + jacquard_common::types::string::Datetime, 46 + >, 45 47 #[serde(borrow)] 46 48 pub handle: jacquard_common::types::string::Handle<'a>, 47 49 pub indexed_at: jacquard_common::types::string::Datetime, 48 50 #[serde(skip_serializing_if = "std::option::Option::is_none")] 49 51 #[serde(borrow)] 50 - pub invite_note: Option<jacquard_common::CowStr<'a>>, 52 + pub invite_note: std::option::Option<jacquard_common::CowStr<'a>>, 51 53 #[serde(skip_serializing_if = "std::option::Option::is_none")] 52 54 #[serde(borrow)] 53 - pub invited_by: Option<crate::com_atproto::server::InviteCode<'a>>, 55 + pub invited_by: std::option::Option<crate::com_atproto::server::InviteCode<'a>>, 54 56 #[serde(skip_serializing_if = "std::option::Option::is_none")] 55 57 #[serde(borrow)] 56 - pub invites: Option<Vec<crate::com_atproto::server::InviteCode<'a>>>, 58 + pub invites: std::option::Option<Vec<crate::com_atproto::server::InviteCode<'a>>>, 57 59 #[serde(skip_serializing_if = "std::option::Option::is_none")] 58 - pub invites_disabled: Option<bool>, 60 + pub invites_disabled: std::option::Option<bool>, 59 61 #[serde(skip_serializing_if = "std::option::Option::is_none")] 60 62 #[serde(borrow)] 61 - pub related_records: Option<Vec<jacquard_common::types::value::Data<'a>>>, 63 + pub related_records: std::option::Option< 64 + Vec<jacquard_common::types::value::Data<'a>>, 65 + >, 62 66 #[serde(skip_serializing_if = "std::option::Option::is_none")] 63 67 #[serde(borrow)] 64 - pub threat_signatures: Option<Vec<crate::com_atproto::admin::ThreatSignature<'a>>>, 68 + pub threat_signatures: std::option::Option< 69 + Vec<crate::com_atproto::admin::ThreatSignature<'a>>, 70 + >, 65 71 } 66 72 67 73 pub mod account_view_state { ··· 888 894 pub did: jacquard_common::types::string::Did<'a>, 889 895 #[serde(skip_serializing_if = "std::option::Option::is_none")] 890 896 #[serde(borrow)] 891 - pub record_uri: Option<jacquard_common::types::string::AtUri<'a>>, 897 + pub record_uri: std::option::Option<jacquard_common::types::string::AtUri<'a>>, 892 898 } 893 899 894 900 pub mod repo_blob_ref_state { ··· 1223 1229 pub applied: bool, 1224 1230 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1225 1231 #[serde(borrow)] 1226 - pub r#ref: Option<jacquard_common::CowStr<'a>>, 1232 + pub r#ref: std::option::Option<jacquard_common::CowStr<'a>>, 1227 1233 } 1228 1234 1229 1235 pub mod status_attr_state {
+1 -1
crates/weaver-api/src/com_atproto/admin/disable_account_invites.rs
··· 22 22 /// Optional reason for disabled invites. 23 23 #[serde(skip_serializing_if = "std::option::Option::is_none")] 24 24 #[serde(borrow)] 25 - pub note: Option<jacquard_common::CowStr<'a>>, 25 + pub note: std::option::Option<jacquard_common::CowStr<'a>>, 26 26 } 27 27 28 28 pub mod disable_account_invites_state {
+1 -1
crates/weaver-api/src/com_atproto/admin/enable_account_invites.rs
··· 22 22 /// Optional reason for enabled invites. 23 23 #[serde(skip_serializing_if = "std::option::Option::is_none")] 24 24 #[serde(borrow)] 25 - pub note: Option<jacquard_common::CowStr<'a>>, 25 + pub note: std::option::Option<jacquard_common::CowStr<'a>>, 26 26 } 27 27 28 28 pub mod enable_account_invites_state {
+2 -2
crates/weaver-api/src/com_atproto/admin/send_email.rs
··· 20 20 /// Additional comment by the sender that won't be used in the email itself but helpful to provide more context for moderators/reviewers 21 21 #[serde(skip_serializing_if = "std::option::Option::is_none")] 22 22 #[serde(borrow)] 23 - pub comment: Option<jacquard_common::CowStr<'a>>, 23 + pub comment: std::option::Option<jacquard_common::CowStr<'a>>, 24 24 #[serde(borrow)] 25 25 pub content: jacquard_common::CowStr<'a>, 26 26 #[serde(borrow)] ··· 29 29 pub sender_did: jacquard_common::types::string::Did<'a>, 30 30 #[serde(skip_serializing_if = "std::option::Option::is_none")] 31 31 #[serde(borrow)] 32 - pub subject: Option<jacquard_common::CowStr<'a>>, 32 + pub subject: std::option::Option<jacquard_common::CowStr<'a>>, 33 33 } 34 34 35 35 pub mod send_email_state {
+2 -2
crates/weaver-api/src/com_atproto/admin/update_subject_status.rs
··· 19 19 pub struct UpdateSubjectStatus<'a> { 20 20 #[serde(skip_serializing_if = "std::option::Option::is_none")] 21 21 #[serde(borrow)] 22 - pub deactivated: Option<crate::com_atproto::admin::StatusAttr<'a>>, 22 + pub deactivated: std::option::Option<crate::com_atproto::admin::StatusAttr<'a>>, 23 23 #[serde(borrow)] 24 24 pub subject: UpdateSubjectStatusSubject<'a>, 25 25 #[serde(skip_serializing_if = "std::option::Option::is_none")] 26 26 #[serde(borrow)] 27 - pub takedown: Option<crate::com_atproto::admin::StatusAttr<'a>>, 27 + pub takedown: std::option::Option<crate::com_atproto::admin::StatusAttr<'a>>, 28 28 } 29 29 30 30 pub mod update_subject_status_state {
+7 -7
crates/weaver-api/src/com_atproto/label.rs
··· 26 26 /// Optionally, CID specifying the specific version of 'uri' resource this label applies to. 27 27 #[serde(skip_serializing_if = "std::option::Option::is_none")] 28 28 #[serde(borrow)] 29 - pub cid: Option<jacquard_common::types::string::Cid<'a>>, 29 + pub cid: std::option::Option<jacquard_common::types::string::Cid<'a>>, 30 30 /// Timestamp when this label was created. 31 31 pub cts: jacquard_common::types::string::Datetime, 32 32 /// Timestamp at which this label expires (no longer applies). 33 33 #[serde(skip_serializing_if = "std::option::Option::is_none")] 34 - pub exp: Option<jacquard_common::types::string::Datetime>, 34 + pub exp: std::option::Option<jacquard_common::types::string::Datetime>, 35 35 /// If true, this is a negation label, overwriting a previous label. 36 36 #[serde(skip_serializing_if = "std::option::Option::is_none")] 37 - pub neg: Option<bool>, 37 + pub neg: std::option::Option<bool>, 38 38 /// Signature of dag-cbor encoded label. 39 39 #[serde(skip_serializing_if = "std::option::Option::is_none")] 40 - pub sig: Option<bytes::Bytes>, 40 + pub sig: std::option::Option<bytes::Bytes>, 41 41 /// DID of the actor who created this label. 42 42 #[serde(borrow)] 43 43 pub src: jacquard_common::types::string::Did<'a>, ··· 49 49 pub val: jacquard_common::CowStr<'a>, 50 50 /// The AT Protocol version of the label object. 51 51 #[serde(skip_serializing_if = "std::option::Option::is_none")] 52 - pub ver: Option<i64>, 52 + pub ver: std::option::Option<i64>, 53 53 } 54 54 55 55 pub mod label_state { ··· 1024 1024 pub struct LabelValueDefinition<'a> { 1025 1025 /// Does the user need to have adult content enabled in order to configure this label? 1026 1026 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1027 - pub adult_only: Option<bool>, 1027 + pub adult_only: std::option::Option<bool>, 1028 1028 /// 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. 1029 1029 #[serde(borrow)] 1030 1030 pub blurs: jacquard_common::CowStr<'a>, 1031 1031 /// The default setting for this label. 1032 1032 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1033 1033 #[serde(borrow)] 1034 - pub default_setting: Option<jacquard_common::CowStr<'a>>, 1034 + pub default_setting: std::option::Option<jacquard_common::CowStr<'a>>, 1035 1035 /// The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+). 1036 1036 #[serde(borrow)] 1037 1037 pub identifier: jacquard_common::CowStr<'a>,
+4 -2
crates/weaver-api/src/com_atproto/moderation/create_report.rs
··· 19 19 pub struct CreateReport<'a> { 20 20 #[serde(skip_serializing_if = "std::option::Option::is_none")] 21 21 #[serde(borrow)] 22 - pub mod_tool: Option<crate::com_atproto::moderation::create_report::ModTool<'a>>, 22 + pub mod_tool: std::option::Option< 23 + crate::com_atproto::moderation::create_report::ModTool<'a>, 24 + >, 23 25 /// Additional context about the content and violation. 24 26 #[serde(skip_serializing_if = "std::option::Option::is_none")] 25 27 #[serde(borrow)] 26 - pub reason: Option<jacquard_common::CowStr<'a>>, 28 + pub reason: std::option::Option<jacquard_common::CowStr<'a>>, 27 29 /// Indicates the broad category of violation the report is for. 28 30 #[serde(borrow)] 29 31 pub reason_type: crate::com_atproto::moderation::ReasonType<'a>,
+5 -5
crates/weaver-api/src/com_atproto/repo/apply_writes.rs
··· 23 23 /// NOTE: maxLength is redundant with record-key format. Keeping it temporarily to ensure backwards compatibility. 24 24 #[serde(skip_serializing_if = "std::option::Option::is_none")] 25 25 #[serde(borrow)] 26 - pub rkey: Option< 26 + pub rkey: std::option::Option< 27 27 jacquard_common::types::string::RecordKey< 28 28 jacquard_common::types::string::Rkey<'a>, 29 29 >, ··· 715 715 pub uri: jacquard_common::types::string::AtUri<'a>, 716 716 #[serde(skip_serializing_if = "std::option::Option::is_none")] 717 717 #[serde(borrow)] 718 - pub validation_status: Option<jacquard_common::CowStr<'a>>, 718 + pub validation_status: std::option::Option<jacquard_common::CowStr<'a>>, 719 719 } 720 720 721 721 pub mod create_result_state { ··· 1131 1131 /// If provided, the entire operation will fail if the current repo commit CID does not match this value. Used to prevent conflicting repo mutations. 1132 1132 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1133 1133 #[serde(borrow)] 1134 - pub swap_commit: Option<jacquard_common::types::string::Cid<'a>>, 1134 + pub swap_commit: std::option::Option<jacquard_common::types::string::Cid<'a>>, 1135 1135 /// 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. 1136 1136 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1137 - pub validate: Option<bool>, 1137 + pub validate: std::option::Option<bool>, 1138 1138 #[serde(borrow)] 1139 1139 pub writes: Vec<ApplyWritesWritesItem<'a>>, 1140 1140 } ··· 1686 1686 pub uri: jacquard_common::types::string::AtUri<'a>, 1687 1687 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1688 1688 #[serde(borrow)] 1689 - pub validation_status: Option<jacquard_common::CowStr<'a>>, 1689 + pub validation_status: std::option::Option<jacquard_common::CowStr<'a>>, 1690 1690 } 1691 1691 1692 1692 pub mod update_result_state {
+3 -3
crates/weaver-api/src/com_atproto/repo/create_record.rs
··· 29 29 /// The Record Key. 30 30 #[serde(skip_serializing_if = "std::option::Option::is_none")] 31 31 #[serde(borrow)] 32 - pub rkey: Option< 32 + pub rkey: std::option::Option< 33 33 jacquard_common::types::string::RecordKey< 34 34 jacquard_common::types::string::Rkey<'a>, 35 35 >, ··· 37 37 /// Compare and swap with the previous commit by CID. 38 38 #[serde(skip_serializing_if = "std::option::Option::is_none")] 39 39 #[serde(borrow)] 40 - pub swap_commit: Option<jacquard_common::types::string::Cid<'a>>, 40 + pub swap_commit: std::option::Option<jacquard_common::types::string::Cid<'a>>, 41 41 /// 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. 42 42 #[serde(skip_serializing_if = "std::option::Option::is_none")] 43 - pub validate: Option<bool>, 43 + pub validate: std::option::Option<bool>, 44 44 } 45 45 46 46 pub mod create_record_state {
+2 -2
crates/weaver-api/src/com_atproto/repo/delete_record.rs
··· 31 31 /// Compare and swap with the previous commit by CID. 32 32 #[serde(skip_serializing_if = "std::option::Option::is_none")] 33 33 #[serde(borrow)] 34 - pub swap_commit: Option<jacquard_common::types::string::Cid<'a>>, 34 + pub swap_commit: std::option::Option<jacquard_common::types::string::Cid<'a>>, 35 35 /// Compare and swap with the previous record by CID. 36 36 #[serde(skip_serializing_if = "std::option::Option::is_none")] 37 37 #[serde(borrow)] 38 - pub swap_record: Option<jacquard_common::types::string::Cid<'a>>, 38 + pub swap_record: std::option::Option<jacquard_common::types::string::Cid<'a>>, 39 39 } 40 40 41 41 pub mod delete_record_state {
+3 -3
crates/weaver-api/src/com_atproto/repo/put_record.rs
··· 34 34 /// Compare and swap with the previous commit by CID. 35 35 #[serde(skip_serializing_if = "std::option::Option::is_none")] 36 36 #[serde(borrow)] 37 - pub swap_commit: Option<jacquard_common::types::string::Cid<'a>>, 37 + pub swap_commit: std::option::Option<jacquard_common::types::string::Cid<'a>>, 38 38 /// Compare and swap with the previous record by CID. WARNING: nullable and optional field; may cause problems with golang implementation 39 39 #[serde(skip_serializing_if = "std::option::Option::is_none")] 40 40 #[serde(borrow)] 41 - pub swap_record: Option<jacquard_common::types::string::Cid<'a>>, 41 + pub swap_record: std::option::Option<jacquard_common::types::string::Cid<'a>>, 42 42 /// 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. 43 43 #[serde(skip_serializing_if = "std::option::Option::is_none")] 44 - pub validate: Option<bool>, 44 + pub validate: std::option::Option<bool>, 45 45 } 46 46 47 47 pub mod put_record_state {
+8 -8
crates/weaver-api/src/com_atproto/server/create_account.rs
··· 20 20 /// Pre-existing atproto DID, being imported to a new account. 21 21 #[serde(skip_serializing_if = "std::option::Option::is_none")] 22 22 #[serde(borrow)] 23 - pub did: Option<jacquard_common::types::string::Did<'a>>, 23 + pub did: std::option::Option<jacquard_common::types::string::Did<'a>>, 24 24 #[serde(skip_serializing_if = "std::option::Option::is_none")] 25 25 #[serde(borrow)] 26 - pub email: Option<jacquard_common::CowStr<'a>>, 26 + pub email: std::option::Option<jacquard_common::CowStr<'a>>, 27 27 /// Requested handle for the account. 28 28 #[serde(borrow)] 29 29 pub handle: jacquard_common::types::string::Handle<'a>, 30 30 #[serde(skip_serializing_if = "std::option::Option::is_none")] 31 31 #[serde(borrow)] 32 - pub invite_code: Option<jacquard_common::CowStr<'a>>, 32 + pub invite_code: std::option::Option<jacquard_common::CowStr<'a>>, 33 33 /// Initial account password. May need to meet instance-specific password strength requirements. 34 34 #[serde(skip_serializing_if = "std::option::Option::is_none")] 35 35 #[serde(borrow)] 36 - pub password: Option<jacquard_common::CowStr<'a>>, 36 + pub password: std::option::Option<jacquard_common::CowStr<'a>>, 37 37 /// 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. 38 38 #[serde(skip_serializing_if = "std::option::Option::is_none")] 39 39 #[serde(borrow)] 40 - pub plc_op: Option<jacquard_common::types::value::Data<'a>>, 40 + pub plc_op: std::option::Option<jacquard_common::types::value::Data<'a>>, 41 41 /// DID PLC rotation key (aka, recovery key) to be included in PLC creation operation. 42 42 #[serde(skip_serializing_if = "std::option::Option::is_none")] 43 43 #[serde(borrow)] 44 - pub recovery_key: Option<jacquard_common::CowStr<'a>>, 44 + pub recovery_key: std::option::Option<jacquard_common::CowStr<'a>>, 45 45 #[serde(skip_serializing_if = "std::option::Option::is_none")] 46 46 #[serde(borrow)] 47 - pub verification_code: Option<jacquard_common::CowStr<'a>>, 47 + pub verification_code: std::option::Option<jacquard_common::CowStr<'a>>, 48 48 #[serde(skip_serializing_if = "std::option::Option::is_none")] 49 49 #[serde(borrow)] 50 - pub verification_phone: Option<jacquard_common::CowStr<'a>>, 50 + pub verification_phone: std::option::Option<jacquard_common::CowStr<'a>>, 51 51 } 52 52 53 53 pub mod create_account_state {
+1 -1
crates/weaver-api/src/com_atproto/server/create_app_password.rs
··· 23 23 #[serde(borrow)] 24 24 pub password: jacquard_common::CowStr<'a>, 25 25 #[serde(skip_serializing_if = "std::option::Option::is_none")] 26 - pub privileged: Option<bool>, 26 + pub privileged: std::option::Option<bool>, 27 27 } 28 28 29 29 pub mod app_password_state {
+1 -1
crates/weaver-api/src/com_atproto/server/create_invite_code.rs
··· 19 19 pub struct CreateInviteCode<'a> { 20 20 #[serde(skip_serializing_if = "std::option::Option::is_none")] 21 21 #[serde(borrow)] 22 - pub for_account: Option<jacquard_common::types::string::Did<'a>>, 22 + pub for_account: std::option::Option<jacquard_common::types::string::Did<'a>>, 23 23 pub use_count: i64, 24 24 } 25 25
+1 -1
crates/weaver-api/src/com_atproto/server/create_invite_codes.rs
··· 347 347 pub code_count: i64, 348 348 #[serde(skip_serializing_if = "std::option::Option::is_none")] 349 349 #[serde(borrow)] 350 - pub for_accounts: Option<Vec<jacquard_common::types::string::Did<'a>>>, 350 + pub for_accounts: std::option::Option<Vec<jacquard_common::types::string::Did<'a>>>, 351 351 pub use_count: i64, 352 352 } 353 353
+1 -1
crates/weaver-api/src/com_atproto/server/list_app_passwords.rs
··· 21 21 #[serde(borrow)] 22 22 pub name: jacquard_common::CowStr<'a>, 23 23 #[serde(skip_serializing_if = "std::option::Option::is_none")] 24 - pub privileged: Option<bool>, 24 + pub privileged: std::option::Option<bool>, 25 25 } 26 26 27 27 pub mod app_password_state {
+2 -2
crates/weaver-api/src/com_atproto/sync/list_repos.rs
··· 171 171 #[serde(rename_all = "camelCase")] 172 172 pub struct Repo<'a> { 173 173 #[serde(skip_serializing_if = "std::option::Option::is_none")] 174 - pub active: Option<bool>, 174 + pub active: std::option::Option<bool>, 175 175 #[serde(borrow)] 176 176 pub did: jacquard_common::types::string::Did<'a>, 177 177 /// Current repo commit CID ··· 181 181 /// 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. 182 182 #[serde(skip_serializing_if = "std::option::Option::is_none")] 183 183 #[serde(borrow)] 184 - pub status: Option<jacquard_common::CowStr<'a>>, 184 + pub status: std::option::Option<jacquard_common::CowStr<'a>>, 185 185 } 186 186 187 187 pub mod repo_state {
+4 -4
crates/weaver-api/src/com_atproto/sync/subscribe_repos.rs
··· 26 26 /// If active=false, this optional field indicates a reason for why the account is not active. 27 27 #[serde(skip_serializing_if = "std::option::Option::is_none")] 28 28 #[serde(borrow)] 29 - pub status: Option<jacquard_common::CowStr<'a>>, 29 + pub status: std::option::Option<jacquard_common::CowStr<'a>>, 30 30 pub time: jacquard_common::types::string::Datetime, 31 31 } 32 32 ··· 938 938 /// 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. 939 939 #[serde(skip_serializing_if = "std::option::Option::is_none")] 940 940 #[serde(borrow)] 941 - pub prev_data: Option<jacquard_common::types::cid::CidLink<'a>>, 941 + pub prev_data: std::option::Option<jacquard_common::types::cid::CidLink<'a>>, 942 942 /// DEPRECATED -- unused 943 943 pub rebase: bool, 944 944 /// The repo this event comes from. Note that all other message types name this field 'did'. ··· 1586 1586 /// 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. 1587 1587 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1588 1588 #[serde(borrow)] 1589 - pub handle: Option<jacquard_common::types::string::Handle<'a>>, 1589 + pub handle: std::option::Option<jacquard_common::types::string::Handle<'a>>, 1590 1590 pub seq: i64, 1591 1591 pub time: jacquard_common::types::string::Datetime, 1592 1592 } ··· 2085 2085 /// For updates and deletes, the previous record CID (required for inductive firehose). For creations, field should not be defined. 2086 2086 #[serde(skip_serializing_if = "std::option::Option::is_none")] 2087 2087 #[serde(borrow)] 2088 - pub prev: Option<jacquard_common::types::cid::CidLink<'a>>, 2088 + pub prev: std::option::Option<jacquard_common::types::cid::CidLink<'a>>, 2089 2089 } 2090 2090 2091 2091 pub mod repo_op_state {
+30 -28
crates/weaver-api/src/sh_weaver/actor.rs
··· 25 25 pub did: jacquard_common::types::string::Did<'a>, 26 26 /// signed bytes of the corresponding notebook record in the author's repo 27 27 #[serde(skip_serializing_if = "std::option::Option::is_none")] 28 - pub signature: Option<bytes::Bytes>, 28 + pub signature: std::option::Option<bytes::Bytes>, 29 29 } 30 30 31 31 pub mod author_state { ··· 1256 1256 pub struct ProfileView<'a> { 1257 1257 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1258 1258 #[serde(borrow)] 1259 - pub avatar: Option<jacquard_common::types::string::Uri<'a>>, 1259 + pub avatar: std::option::Option<jacquard_common::types::string::Uri<'a>>, 1260 1260 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1261 1261 #[serde(borrow)] 1262 - pub banner: Option<jacquard_common::types::string::Uri<'a>>, 1262 + pub banner: std::option::Option<jacquard_common::types::string::Uri<'a>>, 1263 1263 /// Include link to this account on Bluesky. 1264 1264 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1265 - pub bluesky: Option<bool>, 1265 + pub bluesky: std::option::Option<bool>, 1266 1266 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1267 - pub created_at: Option<jacquard_common::types::string::Datetime>, 1267 + pub created_at: std::option::Option<jacquard_common::types::string::Datetime>, 1268 1268 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1269 1269 #[serde(borrow)] 1270 - pub description: Option<jacquard_common::CowStr<'a>>, 1270 + pub description: std::option::Option<jacquard_common::CowStr<'a>>, 1271 1271 #[serde(borrow)] 1272 1272 pub did: jacquard_common::types::string::Did<'a>, 1273 1273 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1274 1274 #[serde(borrow)] 1275 - pub display_name: Option<jacquard_common::CowStr<'a>>, 1275 + pub display_name: std::option::Option<jacquard_common::CowStr<'a>>, 1276 1276 #[serde(borrow)] 1277 1277 pub handle: jacquard_common::types::string::Handle<'a>, 1278 1278 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1279 - pub indexed_at: Option<jacquard_common::types::string::Datetime>, 1279 + pub indexed_at: std::option::Option<jacquard_common::types::string::Datetime>, 1280 1280 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1281 1281 #[serde(borrow)] 1282 - pub labels: Option<Vec<crate::com_atproto::label::Label<'a>>>, 1282 + pub labels: std::option::Option<Vec<crate::com_atproto::label::Label<'a>>>, 1283 1283 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1284 1284 #[serde(borrow)] 1285 - pub links: Option<Vec<jacquard_common::types::string::Uri<'a>>>, 1285 + pub links: std::option::Option<Vec<jacquard_common::types::string::Uri<'a>>>, 1286 1286 /// Free-form location text. 1287 1287 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1288 1288 #[serde(borrow)] 1289 - pub location: Option<jacquard_common::CowStr<'a>>, 1289 + pub location: std::option::Option<jacquard_common::CowStr<'a>>, 1290 1290 /// Notebooks or other records pinned for display. 1291 1291 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1292 1292 #[serde(borrow)] 1293 - pub pinned: Option<crate::sh_weaver::actor::PinnedList<'a>>, 1293 + pub pinned: std::option::Option<crate::sh_weaver::actor::PinnedList<'a>>, 1294 1294 /// Pronouns to use in user-generated content. 1295 1295 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1296 1296 #[serde(borrow)] 1297 - pub pronouns: Option<crate::sh_weaver::actor::PronounsList<'a>>, 1297 + pub pronouns: std::option::Option<crate::sh_weaver::actor::PronounsList<'a>>, 1298 1298 /// Include link to this account on stream.place. 1299 1299 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1300 - pub streamplace: Option<bool>, 1300 + pub streamplace: std::option::Option<bool>, 1301 1301 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1302 - pub subscribed_count: Option<i64>, 1302 + pub subscribed_count: std::option::Option<i64>, 1303 1303 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1304 - pub subscriber_count: Option<i64>, 1304 + pub subscriber_count: std::option::Option<i64>, 1305 1305 /// Include link to this account on Tangled. 1306 1306 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1307 - pub tangled: Option<bool>, 1307 + pub tangled: std::option::Option<bool>, 1308 1308 } 1309 1309 1310 1310 pub mod profile_view_state { ··· 1930 1930 pub struct ProfileViewBasic<'a> { 1931 1931 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1932 1932 #[serde(borrow)] 1933 - pub avatar: Option<jacquard_common::types::string::Uri<'a>>, 1933 + pub avatar: std::option::Option<jacquard_common::types::string::Uri<'a>>, 1934 1934 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1935 - pub created_at: Option<jacquard_common::types::string::Datetime>, 1935 + pub created_at: std::option::Option<jacquard_common::types::string::Datetime>, 1936 1936 #[serde(borrow)] 1937 1937 pub did: jacquard_common::types::string::Did<'a>, 1938 1938 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1939 1939 #[serde(borrow)] 1940 - pub display_name: Option<jacquard_common::CowStr<'a>>, 1940 + pub display_name: std::option::Option<jacquard_common::CowStr<'a>>, 1941 1941 #[serde(borrow)] 1942 1942 pub handle: jacquard_common::types::string::Handle<'a>, 1943 1943 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1944 - pub indexed_at: Option<jacquard_common::types::string::Datetime>, 1944 + pub indexed_at: std::option::Option<jacquard_common::types::string::Datetime>, 1945 1945 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1946 1946 #[serde(borrow)] 1947 - pub labels: Option<Vec<crate::com_atproto::label::Label<'a>>>, 1947 + pub labels: std::option::Option<Vec<crate::com_atproto::label::Label<'a>>>, 1948 1948 /// Pronouns to use in user-generated content. 1949 1949 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1950 1950 #[serde(borrow)] 1951 - pub pronouns: Option<crate::sh_weaver::actor::PronounsList<'a>>, 1951 + pub pronouns: std::option::Option<crate::sh_weaver::actor::PronounsList<'a>>, 1952 1952 } 1953 1953 1954 1954 pub mod profile_view_basic_state { ··· 2288 2288 /// Free-form profile description text. 2289 2289 #[serde(skip_serializing_if = "std::option::Option::is_none")] 2290 2290 #[serde(borrow)] 2291 - pub description: Option<jacquard_common::CowStr<'a>>, 2291 + pub description: std::option::Option<jacquard_common::CowStr<'a>>, 2292 2292 #[serde(borrow)] 2293 2293 pub did: jacquard_common::types::string::Did<'a>, 2294 2294 #[serde(borrow)] 2295 2295 pub handle: jacquard_common::types::string::Handle<'a>, 2296 2296 #[serde(skip_serializing_if = "std::option::Option::is_none")] 2297 2297 #[serde(borrow)] 2298 - pub links: Option<Vec<jacquard_common::types::string::Uri<'a>>>, 2298 + pub links: std::option::Option<Vec<jacquard_common::types::string::Uri<'a>>>, 2299 2299 /// Free-form location text. 2300 2300 #[serde(skip_serializing_if = "std::option::Option::is_none")] 2301 2301 #[serde(borrow)] 2302 - pub location: Option<jacquard_common::CowStr<'a>>, 2302 + pub location: std::option::Option<jacquard_common::CowStr<'a>>, 2303 2303 /// Any ATURI, it is up to appviews to validate these fields. 2304 2304 #[serde(skip_serializing_if = "std::option::Option::is_none")] 2305 2305 #[serde(borrow)] 2306 - pub pinned_repositories: Option<Vec<jacquard_common::types::string::AtUri<'a>>>, 2306 + pub pinned_repositories: std::option::Option< 2307 + Vec<jacquard_common::types::string::AtUri<'a>>, 2308 + >, 2307 2309 #[serde(skip_serializing_if = "std::option::Option::is_none")] 2308 2310 #[serde(borrow)] 2309 - pub stats: Option<Vec<jacquard_common::CowStr<'a>>>, 2311 + pub stats: std::option::Option<Vec<jacquard_common::CowStr<'a>>>, 2310 2312 } 2311 2313 2312 2314 pub mod tangled_profile_view_state {
+13 -13
crates/weaver-api/src/sh_weaver/actor/profile.rs
··· 21 21 /// Small image to be displayed next to posts from account. AKA, 'profile picture' 22 22 #[serde(skip_serializing_if = "std::option::Option::is_none")] 23 23 #[serde(borrow)] 24 - pub avatar: Option<jacquard_common::types::blob::BlobRef<'a>>, 24 + pub avatar: std::option::Option<jacquard_common::types::blob::BlobRef<'a>>, 25 25 /// Larger horizontal image to display behind profile view. 26 26 #[serde(skip_serializing_if = "std::option::Option::is_none")] 27 27 #[serde(borrow)] 28 - pub banner: Option<jacquard_common::types::blob::BlobRef<'a>>, 28 + pub banner: std::option::Option<jacquard_common::types::blob::BlobRef<'a>>, 29 29 /// Include link to this account on Bluesky. 30 30 #[serde(skip_serializing_if = "std::option::Option::is_none")] 31 - pub bluesky: Option<bool>, 31 + pub bluesky: std::option::Option<bool>, 32 32 #[serde(skip_serializing_if = "std::option::Option::is_none")] 33 - pub created_at: Option<jacquard_common::types::string::Datetime>, 33 + pub created_at: std::option::Option<jacquard_common::types::string::Datetime>, 34 34 /// Free-form profile description text. 35 35 #[serde(skip_serializing_if = "std::option::Option::is_none")] 36 36 #[serde(borrow)] 37 - pub description: Option<jacquard_common::CowStr<'a>>, 37 + pub description: std::option::Option<jacquard_common::CowStr<'a>>, 38 38 #[serde(skip_serializing_if = "std::option::Option::is_none")] 39 39 #[serde(borrow)] 40 - pub display_name: Option<jacquard_common::CowStr<'a>>, 40 + pub display_name: std::option::Option<jacquard_common::CowStr<'a>>, 41 41 /// Self-label values, specific to the application, on the overall account. 42 42 #[serde(skip_serializing_if = "std::option::Option::is_none")] 43 43 #[serde(borrow)] 44 - pub labels: Option<crate::com_atproto::label::SelfLabels<'a>>, 44 + pub labels: std::option::Option<crate::com_atproto::label::SelfLabels<'a>>, 45 45 #[serde(skip_serializing_if = "std::option::Option::is_none")] 46 46 #[serde(borrow)] 47 - pub links: Option<Vec<jacquard_common::types::string::Uri<'a>>>, 47 + pub links: std::option::Option<Vec<jacquard_common::types::string::Uri<'a>>>, 48 48 /// Free-form location text. 49 49 #[serde(skip_serializing_if = "std::option::Option::is_none")] 50 50 #[serde(borrow)] 51 - pub location: Option<jacquard_common::CowStr<'a>>, 51 + pub location: std::option::Option<jacquard_common::CowStr<'a>>, 52 52 /// Notebooks or other records pinned for display. 53 53 #[serde(skip_serializing_if = "std::option::Option::is_none")] 54 54 #[serde(borrow)] 55 - pub pinned: Option<crate::sh_weaver::actor::PinnedList<'a>>, 55 + pub pinned: std::option::Option<crate::sh_weaver::actor::PinnedList<'a>>, 56 56 /// Pronouns to use in user-generated content. 57 57 #[serde(skip_serializing_if = "std::option::Option::is_none")] 58 58 #[serde(borrow)] 59 - pub pronouns: Option<crate::sh_weaver::actor::PronounsList<'a>>, 59 + pub pronouns: std::option::Option<crate::sh_weaver::actor::PronounsList<'a>>, 60 60 /// Include link to this account on stream.place. 61 61 #[serde(skip_serializing_if = "std::option::Option::is_none")] 62 - pub streamplace: Option<bool>, 62 + pub streamplace: std::option::Option<bool>, 63 63 /// Include link to this account on Tangled. 64 64 #[serde(skip_serializing_if = "std::option::Option::is_none")] 65 - pub tangled: Option<bool>, 65 + pub tangled: std::option::Option<bool>, 66 66 } 67 67 68 68 pub mod profile_state {
+1 -1
crates/weaver-api/src/sh_weaver/edit/cursor.rs
··· 746 746 pub id: crate::sh_weaver::edit::cursor::Id<'a>, 747 747 #[serde(skip_serializing_if = "std::option::Option::is_none")] 748 748 #[serde(borrow)] 749 - pub side: Option<crate::sh_weaver::edit::cursor::CursorSide<'a>>, 749 + pub side: std::option::Option<crate::sh_weaver::edit::cursor::CursorSide<'a>>, 750 750 } 751 751 752 752 pub mod cursor_state {
+2 -2
crates/weaver-api/src/sh_weaver/embed/external.rs
··· 21 21 pub description: jacquard_common::CowStr<'a>, 22 22 #[serde(skip_serializing_if = "std::option::Option::is_none")] 23 23 #[serde(borrow)] 24 - pub thumb: Option<jacquard_common::types::blob::BlobRef<'a>>, 24 + pub thumb: std::option::Option<jacquard_common::types::blob::BlobRef<'a>>, 25 25 #[serde(borrow)] 26 26 pub title: jacquard_common::CowStr<'a>, 27 27 #[serde(borrow)] ··· 800 800 pub description: jacquard_common::CowStr<'a>, 801 801 #[serde(skip_serializing_if = "std::option::Option::is_none")] 802 802 #[serde(borrow)] 803 - pub thumb: Option<jacquard_common::types::string::Uri<'a>>, 803 + pub thumb: std::option::Option<jacquard_common::types::string::Uri<'a>>, 804 804 #[serde(borrow)] 805 805 pub title: jacquard_common::CowStr<'a>, 806 806 #[serde(borrow)]
+5 -5
crates/weaver-api/src/sh_weaver/embed/images.rs
··· 23 23 /// Blurhash string for the image, used for low-resolution placeholders. This must be a valid Blurhash string. 24 24 #[serde(skip_serializing_if = "std::option::Option::is_none")] 25 25 #[serde(borrow)] 26 - pub blurhash: Option<jacquard_common::CowStr<'a>>, 26 + pub blurhash: std::option::Option<jacquard_common::CowStr<'a>>, 27 27 #[serde(skip_serializing_if = "std::option::Option::is_none")] 28 28 #[serde(borrow)] 29 - pub dimensions: Option<ImageDimensions<'a>>, 29 + pub dimensions: std::option::Option<ImageDimensions<'a>>, 30 30 #[serde(borrow)] 31 31 pub image: jacquard_common::types::blob::BlobRef<'a>, 32 32 #[serde(skip_serializing_if = "std::option::Option::is_none")] 33 33 #[serde(borrow)] 34 - pub name: Option<jacquard_common::CowStr<'a>>, 34 + pub name: std::option::Option<jacquard_common::CowStr<'a>>, 35 35 } 36 36 37 37 pub mod image_state { ··· 880 880 pub alt: jacquard_common::CowStr<'a>, 881 881 #[serde(skip_serializing_if = "std::option::Option::is_none")] 882 882 #[serde(borrow)] 883 - pub dimensions: Option<ViewImageDimensions<'a>>, 883 + pub dimensions: std::option::Option<ViewImageDimensions<'a>>, 884 884 /// 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. 885 885 #[serde(borrow)] 886 886 pub fullsize: jacquard_common::types::string::Uri<'a>, 887 887 #[serde(skip_serializing_if = "std::option::Option::is_none")] 888 888 #[serde(borrow)] 889 - pub name: Option<jacquard_common::CowStr<'a>>, 889 + pub name: std::option::Option<jacquard_common::CowStr<'a>>, 890 890 /// Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View. 891 891 #[serde(borrow)] 892 892 pub thumb: jacquard_common::types::string::Uri<'a>,
+1 -1
crates/weaver-api/src/sh_weaver/embed/record_with_media.rs
··· 310 310 pub media: ViewMedia<'a>, 311 311 #[serde(skip_serializing_if = "std::option::Option::is_none")] 312 312 #[serde(borrow)] 313 - pub records: Option<crate::sh_weaver::embed::records::View<'a>>, 313 + pub records: std::option::Option<crate::sh_weaver::embed::records::View<'a>>, 314 314 } 315 315 316 316 pub mod view_state {
+10 -8
crates/weaver-api/src/sh_weaver/embed/records.rs
··· 638 638 pub struct RecordEmbed<'a> { 639 639 #[serde(skip_serializing_if = "std::option::Option::is_none")] 640 640 #[serde(borrow)] 641 - pub name: Option<jacquard_common::CowStr<'a>>, 641 + pub name: std::option::Option<jacquard_common::CowStr<'a>>, 642 642 #[serde(borrow)] 643 643 pub record: crate::com_atproto::repo::strong_ref::StrongRef<'a>, 644 644 } ··· 810 810 pub struct RecordEmbedView<'a> { 811 811 #[serde(skip_serializing_if = "std::option::Option::is_none")] 812 812 #[serde(borrow)] 813 - pub name: Option<jacquard_common::CowStr<'a>>, 813 + pub name: std::option::Option<jacquard_common::CowStr<'a>>, 814 814 #[serde(borrow)] 815 815 pub record_view: RecordEmbedViewRecordView<'a>, 816 816 } ··· 1737 1737 pub cid: jacquard_common::types::string::Cid<'a>, 1738 1738 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1739 1739 #[serde(borrow)] 1740 - pub embeds: Option<Vec<crate::sh_weaver::embed::records::RecordEmbedView<'a>>>, 1740 + pub embeds: std::option::Option< 1741 + Vec<crate::sh_weaver::embed::records::RecordEmbedView<'a>>, 1742 + >, 1741 1743 pub indexed_at: jacquard_common::types::string::Datetime, 1742 1744 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1743 1745 #[serde(borrow)] 1744 - pub labels: Option<Vec<crate::com_atproto::label::Label<'a>>>, 1746 + pub labels: std::option::Option<Vec<crate::com_atproto::label::Label<'a>>>, 1745 1747 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1746 - pub like_count: Option<i64>, 1748 + pub like_count: std::option::Option<i64>, 1747 1749 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1748 - pub quote_count: Option<i64>, 1750 + pub quote_count: std::option::Option<i64>, 1749 1751 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1750 - pub reply_count: Option<i64>, 1752 + pub reply_count: std::option::Option<i64>, 1751 1753 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1752 - pub repost_count: Option<i64>, 1754 + pub repost_count: std::option::Option<i64>, 1753 1755 #[serde(borrow)] 1754 1756 pub uri: jacquard_common::types::string::AtUri<'a>, 1755 1757 /// The record data itself.
+8 -8
crates/weaver-api/src/sh_weaver/embed/video.rs
··· 615 615 /// Alt text description of the video, for accessibility. 616 616 #[serde(skip_serializing_if = "std::option::Option::is_none")] 617 617 #[serde(borrow)] 618 - pub alt: Option<jacquard_common::CowStr<'a>>, 618 + pub alt: std::option::Option<jacquard_common::CowStr<'a>>, 619 619 #[serde(skip_serializing_if = "std::option::Option::is_none")] 620 620 #[serde(borrow)] 621 - pub captions: Option<Vec<crate::sh_weaver::embed::video::Caption<'a>>>, 621 + pub captions: std::option::Option<Vec<crate::sh_weaver::embed::video::Caption<'a>>>, 622 622 #[serde(skip_serializing_if = "std::option::Option::is_none")] 623 623 #[serde(borrow)] 624 - pub dimensions: Option<VideoDimensions<'a>>, 624 + pub dimensions: std::option::Option<VideoDimensions<'a>>, 625 625 #[serde(skip_serializing_if = "std::option::Option::is_none")] 626 626 #[serde(borrow)] 627 - pub name: Option<jacquard_common::CowStr<'a>>, 627 + pub name: std::option::Option<jacquard_common::CowStr<'a>>, 628 628 /// The mp4 video file. May be up to 100mb, formerly limited to 50mb. 629 629 #[serde(borrow)] 630 630 pub video: jacquard_common::types::blob::BlobRef<'a>, ··· 914 914 pub struct View<'a> { 915 915 #[serde(skip_serializing_if = "std::option::Option::is_none")] 916 916 #[serde(borrow)] 917 - pub alt: Option<jacquard_common::CowStr<'a>>, 917 + pub alt: std::option::Option<jacquard_common::CowStr<'a>>, 918 918 #[serde(borrow)] 919 919 pub cid: jacquard_common::types::string::Cid<'a>, 920 920 #[serde(skip_serializing_if = "std::option::Option::is_none")] 921 921 #[serde(borrow)] 922 - pub dimensions: Option<ViewDimensions<'a>>, 922 + pub dimensions: std::option::Option<ViewDimensions<'a>>, 923 923 #[serde(skip_serializing_if = "std::option::Option::is_none")] 924 924 #[serde(borrow)] 925 - pub name: Option<jacquard_common::CowStr<'a>>, 925 + pub name: std::option::Option<jacquard_common::CowStr<'a>>, 926 926 #[serde(borrow)] 927 927 pub playlist: jacquard_common::types::string::Uri<'a>, 928 928 #[serde(skip_serializing_if = "std::option::Option::is_none")] 929 929 #[serde(borrow)] 930 - pub thumbnail: Option<jacquard_common::types::string::Uri<'a>>, 930 + pub thumbnail: std::option::Option<jacquard_common::types::string::Uri<'a>>, 931 931 } 932 932 933 933 pub mod view_state {
+9 -9
crates/weaver-api/src/sh_weaver/notebook.rs
··· 30 30 pub record: crate::sh_weaver::actor::ProfileDataView<'a>, 31 31 #[serde(skip_serializing_if = "std::option::Option::is_none")] 32 32 #[serde(borrow)] 33 - pub uri: Option<jacquard_common::types::string::AtUri<'a>>, 33 + pub uri: std::option::Option<jacquard_common::types::string::AtUri<'a>>, 34 34 } 35 35 36 36 pub mod author_list_view_state { ··· 874 874 pub index: i64, 875 875 #[serde(skip_serializing_if = "std::option::Option::is_none")] 876 876 #[serde(borrow)] 877 - pub next: Option<crate::sh_weaver::notebook::BookEntryRef<'a>>, 877 + pub next: std::option::Option<crate::sh_weaver::notebook::BookEntryRef<'a>>, 878 878 #[serde(skip_serializing_if = "std::option::Option::is_none")] 879 879 #[serde(borrow)] 880 - pub prev: Option<crate::sh_weaver::notebook::BookEntryRef<'a>>, 880 + pub prev: std::option::Option<crate::sh_weaver::notebook::BookEntryRef<'a>>, 881 881 } 882 882 883 883 pub mod book_entry_view_state { ··· 1139 1139 pub record: jacquard_common::types::value::Data<'a>, 1140 1140 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1141 1141 #[serde(borrow)] 1142 - pub rendered_view: Option<crate::sh_weaver::notebook::RenderedView<'a>>, 1142 + pub rendered_view: std::option::Option<crate::sh_weaver::notebook::RenderedView<'a>>, 1143 1143 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1144 1144 #[serde(borrow)] 1145 - pub tags: Option<crate::sh_weaver::notebook::Tags<'a>>, 1145 + pub tags: std::option::Option<crate::sh_weaver::notebook::Tags<'a>>, 1146 1146 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1147 1147 #[serde(borrow)] 1148 - pub title: Option<crate::sh_weaver::notebook::Title<'a>>, 1148 + pub title: std::option::Option<crate::sh_weaver::notebook::Title<'a>>, 1149 1149 #[serde(borrow)] 1150 1150 pub uri: jacquard_common::types::string::AtUri<'a>, 1151 1151 } ··· 1511 1511 pub record: jacquard_common::types::value::Data<'a>, 1512 1512 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1513 1513 #[serde(borrow)] 1514 - pub tags: Option<crate::sh_weaver::notebook::Tags<'a>>, 1514 + pub tags: std::option::Option<crate::sh_weaver::notebook::Tags<'a>>, 1515 1515 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1516 1516 #[serde(borrow)] 1517 - pub title: Option<crate::sh_weaver::notebook::Title<'a>>, 1517 + pub title: std::option::Option<crate::sh_weaver::notebook::Title<'a>>, 1518 1518 #[serde(borrow)] 1519 1519 pub uri: jacquard_common::types::string::AtUri<'a>, 1520 1520 } ··· 1854 1854 pub struct RenderedView<'a> { 1855 1855 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1856 1856 #[serde(borrow)] 1857 - pub css: Option<jacquard_common::types::blob::BlobRef<'a>>, 1857 + pub css: std::option::Option<jacquard_common::types::blob::BlobRef<'a>>, 1858 1858 #[serde(borrow)] 1859 1859 pub html: jacquard_common::types::blob::BlobRef<'a>, 1860 1860 }
+3 -3
crates/weaver-api/src/sh_weaver/notebook/authors.rs
··· 19 19 #[serde(rename_all = "camelCase")] 20 20 pub struct AuthorListItem<'a> { 21 21 #[serde(skip_serializing_if = "std::option::Option::is_none")] 22 - pub index: Option<i64>, 22 + pub index: std::option::Option<i64>, 23 23 #[serde(skip_serializing_if = "std::option::Option::is_none")] 24 24 #[serde(borrow)] 25 - pub profile: Option<AuthorListItemProfile<'a>>, 25 + pub profile: std::option::Option<AuthorListItemProfile<'a>>, 26 26 } 27 27 28 28 pub mod author_list_item_state { ··· 313 313 #[serde(borrow)] 314 314 pub author_list: Vec<crate::sh_weaver::notebook::authors::AuthorListItem<'a>>, 315 315 #[serde(skip_serializing_if = "std::option::Option::is_none")] 316 - pub created_at: Option<jacquard_common::types::string::Datetime>, 316 + pub created_at: std::option::Option<jacquard_common::types::string::Datetime>, 317 317 } 318 318 319 319 pub mod authors_state {
+5 -5
crates/weaver-api/src/sh_weaver/notebook/book.rs
··· 22 22 pub authors: Vec<crate::sh_weaver::actor::Author<'a>>, 23 23 /// Client-declared timestamp when this was originally created. 24 24 #[serde(skip_serializing_if = "std::option::Option::is_none")] 25 - pub created_at: Option<jacquard_common::types::string::Datetime>, 25 + pub created_at: std::option::Option<jacquard_common::types::string::Datetime>, 26 26 #[serde(borrow)] 27 27 pub entry_list: Vec<crate::com_atproto::repo::strong_ref::StrongRef<'a>>, 28 28 #[serde(skip_serializing_if = "std::option::Option::is_none")] 29 29 #[serde(borrow)] 30 - pub path: Option<crate::sh_weaver::notebook::Path<'a>>, 30 + pub path: std::option::Option<crate::sh_weaver::notebook::Path<'a>>, 31 31 #[serde(skip_serializing_if = "std::option::Option::is_none")] 32 32 #[serde(borrow)] 33 - pub tags: Option<crate::sh_weaver::notebook::Tags<'a>>, 33 + pub tags: std::option::Option<crate::sh_weaver::notebook::Tags<'a>>, 34 34 #[serde(skip_serializing_if = "std::option::Option::is_none")] 35 35 #[serde(borrow)] 36 - pub theme: Option<crate::com_atproto::repo::strong_ref::StrongRef<'a>>, 36 + pub theme: std::option::Option<crate::com_atproto::repo::strong_ref::StrongRef<'a>>, 37 37 #[serde(skip_serializing_if = "std::option::Option::is_none")] 38 38 #[serde(borrow)] 39 - pub title: Option<crate::sh_weaver::notebook::Title<'a>>, 39 + pub title: std::option::Option<crate::sh_weaver::notebook::Title<'a>>, 40 40 } 41 41 42 42 pub mod book_state {
+3 -3
crates/weaver-api/src/sh_weaver/notebook/chapter.rs
··· 22 22 pub authors: Vec<crate::sh_weaver::actor::Author<'a>>, 23 23 /// Client-declared timestamp when this was originally created. 24 24 #[serde(skip_serializing_if = "std::option::Option::is_none")] 25 - pub created_at: Option<jacquard_common::types::string::Datetime>, 25 + pub created_at: std::option::Option<jacquard_common::types::string::Datetime>, 26 26 #[serde(borrow)] 27 27 pub entry_list: Vec<crate::com_atproto::repo::strong_ref::StrongRef<'a>>, 28 28 /// The notebook this chapter belongs to. ··· 30 30 pub notebook: crate::com_atproto::repo::strong_ref::StrongRef<'a>, 31 31 #[serde(skip_serializing_if = "std::option::Option::is_none")] 32 32 #[serde(borrow)] 33 - pub tags: Option<crate::sh_weaver::notebook::Tags<'a>>, 33 + pub tags: std::option::Option<crate::sh_weaver::notebook::Tags<'a>>, 34 34 #[serde(skip_serializing_if = "std::option::Option::is_none")] 35 35 #[serde(borrow)] 36 - pub title: Option<crate::sh_weaver::notebook::Title<'a>>, 36 + pub title: std::option::Option<crate::sh_weaver::notebook::Title<'a>>, 37 37 } 38 38 39 39 pub mod chapter_state {
+2 -2
crates/weaver-api/src/sh_weaver/notebook/entry.rs
··· 26 26 /// The set of images and records, if any, embedded in the notebook entry. 27 27 #[serde(skip_serializing_if = "std::option::Option::is_none")] 28 28 #[serde(borrow)] 29 - pub embeds: Option<EntryEmbeds<'a>>, 29 + pub embeds: std::option::Option<EntryEmbeds<'a>>, 30 30 #[serde(borrow)] 31 31 pub path: crate::sh_weaver::notebook::Path<'a>, 32 32 #[serde(skip_serializing_if = "std::option::Option::is_none")] 33 33 #[serde(borrow)] 34 - pub tags: Option<crate::sh_weaver::notebook::Tags<'a>>, 34 + pub tags: std::option::Option<crate::sh_weaver::notebook::Tags<'a>>, 35 35 #[serde(borrow)] 36 36 pub title: crate::sh_weaver::notebook::Title<'a>, 37 37 }
+3 -3
crates/weaver-api/src/sh_weaver/notebook/page.rs
··· 20 20 pub struct Page<'a> { 21 21 /// Client-declared timestamp when this was originally created. 22 22 #[serde(skip_serializing_if = "std::option::Option::is_none")] 23 - pub created_at: Option<jacquard_common::types::string::Datetime>, 23 + pub created_at: std::option::Option<jacquard_common::types::string::Datetime>, 24 24 #[serde(borrow)] 25 25 pub entry_list: Vec<crate::com_atproto::repo::strong_ref::StrongRef<'a>>, 26 26 /// The notebook this page belongs to. ··· 28 28 pub notebook: crate::com_atproto::repo::strong_ref::StrongRef<'a>, 29 29 #[serde(skip_serializing_if = "std::option::Option::is_none")] 30 30 #[serde(borrow)] 31 - pub tags: Option<crate::sh_weaver::notebook::Tags<'a>>, 31 + pub tags: std::option::Option<crate::sh_weaver::notebook::Tags<'a>>, 32 32 #[serde(skip_serializing_if = "std::option::Option::is_none")] 33 33 #[serde(borrow)] 34 - pub title: Option<crate::sh_weaver::notebook::Title<'a>>, 34 + pub title: std::option::Option<crate::sh_weaver::notebook::Title<'a>>, 35 35 } 36 36 37 37 pub mod page_state {
+1 -1
crates/weaver-api/src/sh_weaver/notebook/theme.rs
··· 565 565 pub dark_scheme: crate::com_atproto::repo::strong_ref::StrongRef<'a>, 566 566 #[serde(skip_serializing_if = "std::option::Option::is_none")] 567 567 #[serde(borrow)] 568 - pub default_theme: Option<jacquard_common::CowStr<'a>>, 568 + pub default_theme: std::option::Option<jacquard_common::CowStr<'a>>, 569 569 #[serde(borrow)] 570 570 pub fonts: ThemeFonts<'a>, 571 571 /// Syntax highlighting theme for light mode
+2 -2
crates/weaver-api/src/tools_ozone/communication.rs
··· 31 31 pub id: jacquard_common::CowStr<'a>, 32 32 /// Message language. 33 33 #[serde(skip_serializing_if = "std::option::Option::is_none")] 34 - pub lang: Option<jacquard_common::types::string::Language>, 34 + pub lang: std::option::Option<jacquard_common::types::string::Language>, 35 35 /// DID of the user who last updated the template. 36 36 #[serde(borrow)] 37 37 pub last_updated_by: jacquard_common::types::string::Did<'a>, ··· 41 41 /// Content of the template, can contain markdown and variable placeholders. 42 42 #[serde(skip_serializing_if = "std::option::Option::is_none")] 43 43 #[serde(borrow)] 44 - pub subject: Option<jacquard_common::CowStr<'a>>, 44 + pub subject: std::option::Option<jacquard_common::CowStr<'a>>, 45 45 pub updated_at: jacquard_common::types::string::Datetime, 46 46 } 47 47
+137 -74
crates/weaver-api/src/tools_ozone/moderation.rs
··· 38 38 pub active: bool, 39 39 #[serde(skip_serializing_if = "std::option::Option::is_none")] 40 40 #[serde(borrow)] 41 - pub comment: Option<jacquard_common::CowStr<'a>>, 41 + pub comment: std::option::Option<jacquard_common::CowStr<'a>>, 42 42 #[serde(skip_serializing_if = "std::option::Option::is_none")] 43 43 #[serde(borrow)] 44 - pub status: Option<jacquard_common::CowStr<'a>>, 44 + pub status: std::option::Option<jacquard_common::CowStr<'a>>, 45 45 pub timestamp: jacquard_common::types::string::Datetime, 46 46 } 47 47 ··· 1277 1277 }), 1278 1278 ); 1279 1279 map.insert( 1280 + ::jacquard_common::smol_str::SmolStr::new_static( 1281 + "isDelivered", 1282 + ), 1283 + ::jacquard_lexicon::lexicon::LexObjectProperty::Boolean(::jacquard_lexicon::lexicon::LexBoolean { 1284 + description: None, 1285 + default: None, 1286 + r#const: None, 1287 + }), 1288 + ); 1289 + map.insert( 1280 1290 ::jacquard_common::smol_str::SmolStr::new_static("policies"), 1281 1291 ::jacquard_lexicon::lexicon::LexObjectProperty::Array(::jacquard_lexicon::lexicon::LexArray { 1282 1292 description: Some( ··· 2053 2063 r#enum: None, 2054 2064 r#const: None, 2055 2065 known_values: None, 2066 + }), 2067 + ); 2068 + map.insert( 2069 + ::jacquard_common::smol_str::SmolStr::new_static( 2070 + "targetServices", 2071 + ), 2072 + ::jacquard_lexicon::lexicon::LexObjectProperty::Array(::jacquard_lexicon::lexicon::LexArray { 2073 + description: Some( 2074 + ::jacquard_common::CowStr::new_static( 2075 + "List of services where the takedown should be applied. If empty or not provided, takedown is applied on all configured services.", 2076 + ), 2077 + ), 2078 + items: ::jacquard_lexicon::lexicon::LexArrayItem::String(::jacquard_lexicon::lexicon::LexString { 2079 + description: None, 2080 + format: None, 2081 + default: None, 2082 + min_length: None, 2083 + max_length: None, 2084 + min_graphemes: None, 2085 + max_graphemes: None, 2086 + r#enum: None, 2087 + r#const: None, 2088 + known_values: None, 2089 + }), 2090 + min_length: None, 2091 + max_length: None, 2056 2092 }), 2057 2093 ); 2058 2094 map ··· 4960 4996 /// The IP address used when completing the AA flow. 4961 4997 #[serde(skip_serializing_if = "std::option::Option::is_none")] 4962 4998 #[serde(borrow)] 4963 - pub complete_ip: Option<jacquard_common::CowStr<'a>>, 4999 + pub complete_ip: std::option::Option<jacquard_common::CowStr<'a>>, 4964 5000 /// The user agent used when completing the AA flow. 4965 5001 #[serde(skip_serializing_if = "std::option::Option::is_none")] 4966 5002 #[serde(borrow)] 4967 - pub complete_ua: Option<jacquard_common::CowStr<'a>>, 5003 + pub complete_ua: std::option::Option<jacquard_common::CowStr<'a>>, 4968 5004 /// The date and time of this write operation. 4969 5005 pub created_at: jacquard_common::types::string::Datetime, 4970 5006 /// The IP address used when initiating the AA flow. 4971 5007 #[serde(skip_serializing_if = "std::option::Option::is_none")] 4972 5008 #[serde(borrow)] 4973 - pub init_ip: Option<jacquard_common::CowStr<'a>>, 5009 + pub init_ip: std::option::Option<jacquard_common::CowStr<'a>>, 4974 5010 /// The user agent used when initiating the AA flow. 4975 5011 #[serde(skip_serializing_if = "std::option::Option::is_none")] 4976 5012 #[serde(borrow)] 4977 - pub init_ua: Option<jacquard_common::CowStr<'a>>, 5013 + pub init_ua: std::option::Option<jacquard_common::CowStr<'a>>, 4978 5014 /// The status of the age assurance process. 4979 5015 #[serde(borrow)] 4980 5016 pub status: jacquard_common::CowStr<'a>, ··· 5312 5348 pub created_at: jacquard_common::types::string::Datetime, 5313 5349 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5314 5350 #[serde(borrow)] 5315 - pub details: Option<BlobViewDetails<'a>>, 5351 + pub details: std::option::Option<BlobViewDetails<'a>>, 5316 5352 #[serde(borrow)] 5317 5353 pub mime_type: jacquard_common::CowStr<'a>, 5318 5354 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5319 5355 #[serde(borrow)] 5320 - pub moderation: Option<crate::tools_ozone::moderation::Moderation<'a>>, 5356 + pub moderation: std::option::Option<crate::tools_ozone::moderation::Moderation<'a>>, 5321 5357 pub size: i64, 5322 5358 } 5323 5359 ··· 5662 5698 pub struct IdentityEvent<'a> { 5663 5699 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5664 5700 #[serde(borrow)] 5665 - pub comment: Option<jacquard_common::CowStr<'a>>, 5701 + pub comment: std::option::Option<jacquard_common::CowStr<'a>>, 5666 5702 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5667 5703 #[serde(borrow)] 5668 - pub handle: Option<jacquard_common::types::string::Handle<'a>>, 5704 + pub handle: std::option::Option<jacquard_common::types::string::Handle<'a>>, 5669 5705 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5670 5706 #[serde(borrow)] 5671 - pub pds_host: Option<jacquard_common::types::string::Uri<'a>>, 5707 + pub pds_host: std::option::Option<jacquard_common::types::string::Uri<'a>>, 5672 5708 pub timestamp: jacquard_common::types::string::Datetime, 5673 5709 #[serde(skip_serializing_if = "std::option::Option::is_none")] 5674 - pub tombstone: Option<bool>, 5710 + pub tombstone: std::option::Option<bool>, 5675 5711 } 5676 5712 5677 5713 pub mod identity_event_state { ··· 6180 6216 #[serde(skip_serializing_if = "std::option::Option::is_none")] 6181 6217 #[serde(borrow)] 6182 6218 pub content: std::option::Option<jacquard_common::CowStr<'a>>, 6219 + /// Indicates whether the email was successfully delivered to the user's inbox. 6220 + #[serde(skip_serializing_if = "std::option::Option::is_none")] 6221 + pub is_delivered: std::option::Option<bool>, 6183 6222 /// Names/Keywords of the policies that necessitated the email. 6184 6223 #[serde(skip_serializing_if = "std::option::Option::is_none")] 6185 6224 #[serde(borrow)] ··· 6278 6317 pub struct ModEventLabel<'a> { 6279 6318 #[serde(skip_serializing_if = "std::option::Option::is_none")] 6280 6319 #[serde(borrow)] 6281 - pub comment: Option<jacquard_common::CowStr<'a>>, 6320 + pub comment: std::option::Option<jacquard_common::CowStr<'a>>, 6282 6321 #[serde(borrow)] 6283 6322 pub create_label_vals: Vec<jacquard_common::CowStr<'a>>, 6284 6323 /// Indicates how long the label will remain on the subject. Only applies on labels that are being added. 6285 6324 #[serde(skip_serializing_if = "std::option::Option::is_none")] 6286 - pub duration_in_hours: Option<i64>, 6325 + pub duration_in_hours: std::option::Option<i64>, 6287 6326 #[serde(borrow)] 6288 6327 pub negate_label_vals: Vec<jacquard_common::CowStr<'a>>, 6289 6328 } ··· 6495 6534 pub struct ModEventMute<'a> { 6496 6535 #[serde(skip_serializing_if = "std::option::Option::is_none")] 6497 6536 #[serde(borrow)] 6498 - pub comment: Option<jacquard_common::CowStr<'a>>, 6537 + pub comment: std::option::Option<jacquard_common::CowStr<'a>>, 6499 6538 /// Indicates how long the subject should remain muted. 6500 6539 pub duration_in_hours: i64, 6501 6540 } ··· 6695 6734 pub struct ModEventPriorityScore<'a> { 6696 6735 #[serde(skip_serializing_if = "std::option::Option::is_none")] 6697 6736 #[serde(borrow)] 6698 - pub comment: Option<jacquard_common::CowStr<'a>>, 6737 + pub comment: std::option::Option<jacquard_common::CowStr<'a>>, 6699 6738 pub score: i64, 6700 6739 } 6701 6740 ··· 6882 6921 pub struct ModEventReport<'a> { 6883 6922 #[serde(skip_serializing_if = "std::option::Option::is_none")] 6884 6923 #[serde(borrow)] 6885 - pub comment: Option<jacquard_common::CowStr<'a>>, 6924 + pub comment: std::option::Option<jacquard_common::CowStr<'a>>, 6886 6925 /// 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. 6887 6926 #[serde(skip_serializing_if = "std::option::Option::is_none")] 6888 - pub is_reporter_muted: Option<bool>, 6927 + pub is_reporter_muted: std::option::Option<bool>, 6889 6928 #[serde(borrow)] 6890 6929 pub report_type: crate::com_atproto::moderation::ReasonType<'a>, 6891 6930 } ··· 7163 7202 /// Additional comment about added/removed tags. 7164 7203 #[serde(skip_serializing_if = "std::option::Option::is_none")] 7165 7204 #[serde(borrow)] 7166 - pub comment: Option<jacquard_common::CowStr<'a>>, 7205 + pub comment: std::option::Option<jacquard_common::CowStr<'a>>, 7167 7206 /// Tags to be removed to the subject. Ignores a tag If it doesn't exist, won't be duplicated. 7168 7207 #[serde(borrow)] 7169 7208 pub remove: Vec<jacquard_common::CowStr<'a>>, ··· 7382 7421 /// When the strike should expire. If not provided, the strike never expires. 7383 7422 #[serde(skip_serializing_if = "std::option::Option::is_none")] 7384 7423 pub strike_expires_at: std::option::Option<jacquard_common::types::string::Datetime>, 7424 + /// List of services where the takedown should be applied. If empty or not provided, takedown is applied on all configured services. 7425 + #[serde(skip_serializing_if = "std::option::Option::is_none")] 7426 + #[serde(borrow)] 7427 + pub target_services: std::option::Option<Vec<jacquard_common::CowStr<'a>>>, 7385 7428 } 7386 7429 7387 7430 impl<'a> ::jacquard_lexicon::schema::LexiconSchema for ModEventTakedown<'a> { ··· 7504 7547 pub created_by: jacquard_common::types::string::Did<'a>, 7505 7548 #[serde(skip_serializing_if = "std::option::Option::is_none")] 7506 7549 #[serde(borrow)] 7507 - pub creator_handle: Option<jacquard_common::CowStr<'a>>, 7550 + pub creator_handle: std::option::Option<jacquard_common::CowStr<'a>>, 7508 7551 #[serde(borrow)] 7509 7552 pub event: ModEventViewEvent<'a>, 7510 7553 pub id: i64, 7511 7554 #[serde(skip_serializing_if = "std::option::Option::is_none")] 7512 7555 #[serde(borrow)] 7513 - pub mod_tool: Option<crate::tools_ozone::moderation::ModTool<'a>>, 7556 + pub mod_tool: std::option::Option<crate::tools_ozone::moderation::ModTool<'a>>, 7514 7557 #[serde(borrow)] 7515 7558 pub subject: ModEventViewSubject<'a>, 7516 7559 #[serde(borrow)] 7517 7560 pub subject_blob_cids: Vec<jacquard_common::CowStr<'a>>, 7518 7561 #[serde(skip_serializing_if = "std::option::Option::is_none")] 7519 7562 #[serde(borrow)] 7520 - pub subject_handle: Option<jacquard_common::CowStr<'a>>, 7563 + pub subject_handle: std::option::Option<jacquard_common::CowStr<'a>>, 7521 7564 } 7522 7565 7523 7566 pub mod mod_event_view_state { ··· 8033 8076 pub id: i64, 8034 8077 #[serde(skip_serializing_if = "std::option::Option::is_none")] 8035 8078 #[serde(borrow)] 8036 - pub mod_tool: Option<crate::tools_ozone::moderation::ModTool<'a>>, 8079 + pub mod_tool: std::option::Option<crate::tools_ozone::moderation::ModTool<'a>>, 8037 8080 #[serde(borrow)] 8038 8081 pub subject: ModEventViewDetailSubject<'a>, 8039 8082 #[serde(borrow)] ··· 8610 8653 pub struct RecordEvent<'a> { 8611 8654 #[serde(skip_serializing_if = "std::option::Option::is_none")] 8612 8655 #[serde(borrow)] 8613 - pub cid: Option<jacquard_common::types::string::Cid<'a>>, 8656 + pub cid: std::option::Option<jacquard_common::types::string::Cid<'a>>, 8614 8657 #[serde(skip_serializing_if = "std::option::Option::is_none")] 8615 8658 #[serde(borrow)] 8616 - pub comment: Option<jacquard_common::CowStr<'a>>, 8659 + pub comment: std::option::Option<jacquard_common::CowStr<'a>>, 8617 8660 #[serde(borrow)] 8618 8661 pub op: jacquard_common::CowStr<'a>, 8619 8662 pub timestamp: jacquard_common::types::string::Datetime, ··· 9265 9308 pub indexed_at: jacquard_common::types::string::Datetime, 9266 9309 #[serde(skip_serializing_if = "std::option::Option::is_none")] 9267 9310 #[serde(borrow)] 9268 - pub labels: Option<Vec<crate::com_atproto::label::Label<'a>>>, 9311 + pub labels: std::option::Option<Vec<crate::com_atproto::label::Label<'a>>>, 9269 9312 #[serde(borrow)] 9270 9313 pub moderation: crate::tools_ozone::moderation::ModerationDetail<'a>, 9271 9314 #[serde(borrow)] ··· 9868 9911 #[serde(rename_all = "camelCase")] 9869 9912 pub struct RepoView<'a> { 9870 9913 #[serde(skip_serializing_if = "std::option::Option::is_none")] 9871 - pub deactivated_at: Option<jacquard_common::types::string::Datetime>, 9914 + pub deactivated_at: std::option::Option<jacquard_common::types::string::Datetime>, 9872 9915 #[serde(borrow)] 9873 9916 pub did: jacquard_common::types::string::Did<'a>, 9874 9917 #[serde(skip_serializing_if = "std::option::Option::is_none")] 9875 9918 #[serde(borrow)] 9876 - pub email: Option<jacquard_common::CowStr<'a>>, 9919 + pub email: std::option::Option<jacquard_common::CowStr<'a>>, 9877 9920 #[serde(borrow)] 9878 9921 pub handle: jacquard_common::types::string::Handle<'a>, 9879 9922 pub indexed_at: jacquard_common::types::string::Datetime, 9880 9923 #[serde(skip_serializing_if = "std::option::Option::is_none")] 9881 9924 #[serde(borrow)] 9882 - pub invite_note: Option<jacquard_common::CowStr<'a>>, 9925 + pub invite_note: std::option::Option<jacquard_common::CowStr<'a>>, 9883 9926 #[serde(skip_serializing_if = "std::option::Option::is_none")] 9884 9927 #[serde(borrow)] 9885 - pub invited_by: Option<crate::com_atproto::server::InviteCode<'a>>, 9928 + pub invited_by: std::option::Option<crate::com_atproto::server::InviteCode<'a>>, 9886 9929 #[serde(skip_serializing_if = "std::option::Option::is_none")] 9887 - pub invites_disabled: Option<bool>, 9930 + pub invites_disabled: std::option::Option<bool>, 9888 9931 #[serde(borrow)] 9889 9932 pub moderation: crate::tools_ozone::moderation::Moderation<'a>, 9890 9933 #[serde(borrow)] 9891 9934 pub related_records: Vec<jacquard_common::types::value::Data<'a>>, 9892 9935 #[serde(skip_serializing_if = "std::option::Option::is_none")] 9893 9936 #[serde(borrow)] 9894 - pub threat_signatures: Option<Vec<crate::com_atproto::admin::ThreatSignature<'a>>>, 9937 + pub threat_signatures: std::option::Option< 9938 + Vec<crate::com_atproto::admin::ThreatSignature<'a>>, 9939 + >, 9895 9940 } 9896 9941 9897 9942 pub mod repo_view_state { ··· 10316 10361 #[serde(rename_all = "camelCase")] 10317 10362 pub struct RepoViewDetail<'a> { 10318 10363 #[serde(skip_serializing_if = "std::option::Option::is_none")] 10319 - pub deactivated_at: Option<jacquard_common::types::string::Datetime>, 10364 + pub deactivated_at: std::option::Option<jacquard_common::types::string::Datetime>, 10320 10365 #[serde(borrow)] 10321 10366 pub did: jacquard_common::types::string::Did<'a>, 10322 10367 #[serde(skip_serializing_if = "std::option::Option::is_none")] 10323 10368 #[serde(borrow)] 10324 - pub email: Option<jacquard_common::CowStr<'a>>, 10369 + pub email: std::option::Option<jacquard_common::CowStr<'a>>, 10325 10370 #[serde(skip_serializing_if = "std::option::Option::is_none")] 10326 - pub email_confirmed_at: Option<jacquard_common::types::string::Datetime>, 10371 + pub email_confirmed_at: std::option::Option< 10372 + jacquard_common::types::string::Datetime, 10373 + >, 10327 10374 #[serde(borrow)] 10328 10375 pub handle: jacquard_common::types::string::Handle<'a>, 10329 10376 pub indexed_at: jacquard_common::types::string::Datetime, 10330 10377 #[serde(skip_serializing_if = "std::option::Option::is_none")] 10331 10378 #[serde(borrow)] 10332 - pub invite_note: Option<jacquard_common::CowStr<'a>>, 10379 + pub invite_note: std::option::Option<jacquard_common::CowStr<'a>>, 10333 10380 #[serde(skip_serializing_if = "std::option::Option::is_none")] 10334 10381 #[serde(borrow)] 10335 - pub invited_by: Option<crate::com_atproto::server::InviteCode<'a>>, 10382 + pub invited_by: std::option::Option<crate::com_atproto::server::InviteCode<'a>>, 10336 10383 #[serde(skip_serializing_if = "std::option::Option::is_none")] 10337 10384 #[serde(borrow)] 10338 - pub invites: Option<Vec<crate::com_atproto::server::InviteCode<'a>>>, 10385 + pub invites: std::option::Option<Vec<crate::com_atproto::server::InviteCode<'a>>>, 10339 10386 #[serde(skip_serializing_if = "std::option::Option::is_none")] 10340 - pub invites_disabled: Option<bool>, 10387 + pub invites_disabled: std::option::Option<bool>, 10341 10388 #[serde(skip_serializing_if = "std::option::Option::is_none")] 10342 10389 #[serde(borrow)] 10343 - pub labels: Option<Vec<crate::com_atproto::label::Label<'a>>>, 10390 + pub labels: std::option::Option<Vec<crate::com_atproto::label::Label<'a>>>, 10344 10391 #[serde(borrow)] 10345 10392 pub moderation: crate::tools_ozone::moderation::ModerationDetail<'a>, 10346 10393 #[serde(borrow)] 10347 10394 pub related_records: Vec<jacquard_common::types::value::Data<'a>>, 10348 10395 #[serde(skip_serializing_if = "std::option::Option::is_none")] 10349 10396 #[serde(borrow)] 10350 - pub threat_signatures: Option<Vec<crate::com_atproto::admin::ThreatSignature<'a>>>, 10397 + pub threat_signatures: std::option::Option< 10398 + Vec<crate::com_atproto::admin::ThreatSignature<'a>>, 10399 + >, 10351 10400 } 10352 10401 10353 10402 pub mod repo_view_detail_state { ··· 11644 11693 /// Serialized event object that will be propagated to the event when performed 11645 11694 #[serde(skip_serializing_if = "std::option::Option::is_none")] 11646 11695 #[serde(borrow)] 11647 - pub event_data: Option<jacquard_common::types::value::Data<'a>>, 11696 + pub event_data: std::option::Option<jacquard_common::types::value::Data<'a>>, 11648 11697 /// Earliest time to execute the action (for randomized scheduling) 11649 11698 #[serde(skip_serializing_if = "std::option::Option::is_none")] 11650 - pub execute_after: Option<jacquard_common::types::string::Datetime>, 11699 + pub execute_after: std::option::Option<jacquard_common::types::string::Datetime>, 11651 11700 /// Exact time to execute the action 11652 11701 #[serde(skip_serializing_if = "std::option::Option::is_none")] 11653 - pub execute_at: Option<jacquard_common::types::string::Datetime>, 11702 + pub execute_at: std::option::Option<jacquard_common::types::string::Datetime>, 11654 11703 /// Latest time to execute the action (for randomized scheduling) 11655 11704 #[serde(skip_serializing_if = "std::option::Option::is_none")] 11656 - pub execute_until: Option<jacquard_common::types::string::Datetime>, 11705 + pub execute_until: std::option::Option<jacquard_common::types::string::Datetime>, 11657 11706 /// ID of the moderation event created when action was successfully executed 11658 11707 #[serde(skip_serializing_if = "std::option::Option::is_none")] 11659 - pub execution_event_id: Option<i64>, 11708 + pub execution_event_id: std::option::Option<i64>, 11660 11709 /// Auto-incrementing row ID 11661 11710 pub id: i64, 11662 11711 /// When the action was last attempted to be executed 11663 11712 #[serde(skip_serializing_if = "std::option::Option::is_none")] 11664 - pub last_executed_at: Option<jacquard_common::types::string::Datetime>, 11713 + pub last_executed_at: std::option::Option<jacquard_common::types::string::Datetime>, 11665 11714 /// Reason for the last execution failure 11666 11715 #[serde(skip_serializing_if = "std::option::Option::is_none")] 11667 11716 #[serde(borrow)] 11668 - pub last_failure_reason: Option<jacquard_common::CowStr<'a>>, 11717 + pub last_failure_reason: std::option::Option<jacquard_common::CowStr<'a>>, 11669 11718 /// Whether execution time should be randomized within the specified range 11670 11719 #[serde(skip_serializing_if = "std::option::Option::is_none")] 11671 - pub randomize_execution: Option<bool>, 11720 + pub randomize_execution: std::option::Option<bool>, 11672 11721 /// Current status of the scheduled action 11673 11722 #[serde(borrow)] 11674 11723 pub status: jacquard_common::CowStr<'a>, 11675 11724 /// When the scheduled action was last updated 11676 11725 #[serde(skip_serializing_if = "std::option::Option::is_none")] 11677 - pub updated_at: Option<jacquard_common::types::string::Datetime>, 11726 + pub updated_at: std::option::Option<jacquard_common::types::string::Datetime>, 11678 11727 } 11679 11728 11680 11729 pub mod scheduled_action_view_state { ··· 12297 12346 /// Statistics related to the account subject 12298 12347 #[serde(skip_serializing_if = "std::option::Option::is_none")] 12299 12348 #[serde(borrow)] 12300 - pub account_stats: Option<crate::tools_ozone::moderation::AccountStats<'a>>, 12349 + pub account_stats: std::option::Option< 12350 + crate::tools_ozone::moderation::AccountStats<'a>, 12351 + >, 12301 12352 /// Strike information for the account (account-level only) 12302 12353 #[serde(skip_serializing_if = "std::option::Option::is_none")] 12303 12354 #[serde(borrow)] 12304 - pub account_strike: Option<crate::tools_ozone::moderation::AccountStrike<'a>>, 12355 + pub account_strike: std::option::Option< 12356 + crate::tools_ozone::moderation::AccountStrike<'a>, 12357 + >, 12305 12358 /// Current age assurance state of the subject. 12306 12359 #[serde(skip_serializing_if = "std::option::Option::is_none")] 12307 12360 #[serde(borrow)] 12308 - pub age_assurance_state: Option<jacquard_common::CowStr<'a>>, 12361 + pub age_assurance_state: std::option::Option<jacquard_common::CowStr<'a>>, 12309 12362 /// Whether or not the last successful update to age assurance was made by the user or admin. 12310 12363 #[serde(skip_serializing_if = "std::option::Option::is_none")] 12311 12364 #[serde(borrow)] 12312 - pub age_assurance_updated_by: Option<jacquard_common::CowStr<'a>>, 12365 + pub age_assurance_updated_by: std::option::Option<jacquard_common::CowStr<'a>>, 12313 12366 /// 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. 12314 12367 #[serde(skip_serializing_if = "std::option::Option::is_none")] 12315 - pub appealed: Option<bool>, 12368 + pub appealed: std::option::Option<bool>, 12316 12369 /// Sticky comment on the subject. 12317 12370 #[serde(skip_serializing_if = "std::option::Option::is_none")] 12318 12371 #[serde(borrow)] 12319 - pub comment: Option<jacquard_common::CowStr<'a>>, 12372 + pub comment: std::option::Option<jacquard_common::CowStr<'a>>, 12320 12373 /// Timestamp referencing the first moderation status impacting event was emitted on the subject 12321 12374 pub created_at: jacquard_common::types::string::Datetime, 12322 12375 #[serde(skip_serializing_if = "std::option::Option::is_none")] 12323 12376 #[serde(borrow)] 12324 - pub hosting: Option<SubjectStatusViewHosting<'a>>, 12377 + pub hosting: std::option::Option<SubjectStatusViewHosting<'a>>, 12325 12378 pub id: i64, 12326 12379 /// Timestamp referencing when the author of the subject appealed a moderation action 12327 12380 #[serde(skip_serializing_if = "std::option::Option::is_none")] 12328 - pub last_appealed_at: Option<jacquard_common::types::string::Datetime>, 12381 + pub last_appealed_at: std::option::Option<jacquard_common::types::string::Datetime>, 12329 12382 #[serde(skip_serializing_if = "std::option::Option::is_none")] 12330 - pub last_reported_at: Option<jacquard_common::types::string::Datetime>, 12383 + pub last_reported_at: std::option::Option<jacquard_common::types::string::Datetime>, 12331 12384 #[serde(skip_serializing_if = "std::option::Option::is_none")] 12332 - pub last_reviewed_at: Option<jacquard_common::types::string::Datetime>, 12385 + pub last_reviewed_at: std::option::Option<jacquard_common::types::string::Datetime>, 12333 12386 #[serde(skip_serializing_if = "std::option::Option::is_none")] 12334 12387 #[serde(borrow)] 12335 - pub last_reviewed_by: Option<jacquard_common::types::string::Did<'a>>, 12388 + pub last_reviewed_by: std::option::Option<jacquard_common::types::string::Did<'a>>, 12336 12389 #[serde(skip_serializing_if = "std::option::Option::is_none")] 12337 - pub mute_reporting_until: Option<jacquard_common::types::string::Datetime>, 12390 + pub mute_reporting_until: std::option::Option< 12391 + jacquard_common::types::string::Datetime, 12392 + >, 12338 12393 #[serde(skip_serializing_if = "std::option::Option::is_none")] 12339 - pub mute_until: Option<jacquard_common::types::string::Datetime>, 12394 + pub mute_until: std::option::Option<jacquard_common::types::string::Datetime>, 12340 12395 /// Numeric value representing the level of priority. Higher score means higher priority. 12341 12396 #[serde(skip_serializing_if = "std::option::Option::is_none")] 12342 - pub priority_score: Option<i64>, 12397 + pub priority_score: std::option::Option<i64>, 12343 12398 /// Statistics related to the record subjects authored by the subject's account 12344 12399 #[serde(skip_serializing_if = "std::option::Option::is_none")] 12345 12400 #[serde(borrow)] 12346 - pub records_stats: Option<crate::tools_ozone::moderation::RecordsStats<'a>>, 12401 + pub records_stats: std::option::Option< 12402 + crate::tools_ozone::moderation::RecordsStats<'a>, 12403 + >, 12347 12404 #[serde(borrow)] 12348 12405 pub review_state: crate::tools_ozone::moderation::SubjectReviewState<'a>, 12349 12406 #[serde(borrow)] 12350 12407 pub subject: SubjectStatusViewSubject<'a>, 12351 12408 #[serde(skip_serializing_if = "std::option::Option::is_none")] 12352 12409 #[serde(borrow)] 12353 - pub subject_blob_cids: Option<Vec<jacquard_common::types::string::Cid<'a>>>, 12410 + pub subject_blob_cids: std::option::Option< 12411 + Vec<jacquard_common::types::string::Cid<'a>>, 12412 + >, 12354 12413 #[serde(skip_serializing_if = "std::option::Option::is_none")] 12355 12414 #[serde(borrow)] 12356 - pub subject_repo_handle: Option<jacquard_common::CowStr<'a>>, 12415 + pub subject_repo_handle: std::option::Option<jacquard_common::CowStr<'a>>, 12357 12416 #[serde(skip_serializing_if = "std::option::Option::is_none")] 12358 - pub suspend_until: Option<jacquard_common::types::string::Datetime>, 12417 + pub suspend_until: std::option::Option<jacquard_common::types::string::Datetime>, 12359 12418 #[serde(skip_serializing_if = "std::option::Option::is_none")] 12360 12419 #[serde(borrow)] 12361 - pub tags: Option<Vec<jacquard_common::CowStr<'a>>>, 12420 + pub tags: std::option::Option<Vec<jacquard_common::CowStr<'a>>>, 12362 12421 #[serde(skip_serializing_if = "std::option::Option::is_none")] 12363 - pub takendown: Option<bool>, 12422 + pub takendown: std::option::Option<bool>, 12364 12423 /// Timestamp referencing when the last update was made to the moderation status of the subject 12365 12424 pub updated_at: jacquard_common::types::string::Datetime, 12366 12425 } ··· 13158 13217 pub struct SubjectView<'a> { 13159 13218 #[serde(skip_serializing_if = "std::option::Option::is_none")] 13160 13219 #[serde(borrow)] 13161 - pub profile: Option<jacquard_common::types::value::Data<'a>>, 13220 + pub profile: std::option::Option<jacquard_common::types::value::Data<'a>>, 13162 13221 #[serde(skip_serializing_if = "std::option::Option::is_none")] 13163 13222 #[serde(borrow)] 13164 - pub record: Option<crate::tools_ozone::moderation::RecordViewDetail<'a>>, 13223 + pub record: std::option::Option< 13224 + crate::tools_ozone::moderation::RecordViewDetail<'a>, 13225 + >, 13165 13226 #[serde(skip_serializing_if = "std::option::Option::is_none")] 13166 13227 #[serde(borrow)] 13167 - pub repo: Option<crate::tools_ozone::moderation::RepoViewDetail<'a>>, 13228 + pub repo: std::option::Option<crate::tools_ozone::moderation::RepoViewDetail<'a>>, 13168 13229 #[serde(skip_serializing_if = "std::option::Option::is_none")] 13169 13230 #[serde(borrow)] 13170 - pub status: Option<crate::tools_ozone::moderation::SubjectStatusView<'a>>, 13231 + pub status: std::option::Option< 13232 + crate::tools_ozone::moderation::SubjectStatusView<'a>, 13233 + >, 13171 13234 #[serde(borrow)] 13172 13235 pub subject: jacquard_common::CowStr<'a>, 13173 13236 #[serde(borrow)]
+2 -2
crates/weaver-api/src/tools_ozone/moderation/cancel_scheduled_actions.rs
··· 440 440 pub error: jacquard_common::CowStr<'a>, 441 441 #[serde(skip_serializing_if = "std::option::Option::is_none")] 442 442 #[serde(borrow)] 443 - pub error_code: Option<jacquard_common::CowStr<'a>>, 443 + pub error_code: std::option::Option<jacquard_common::CowStr<'a>>, 444 444 } 445 445 446 446 pub mod failed_cancellation_state { ··· 637 637 /// Optional comment describing the reason for cancellation 638 638 #[serde(skip_serializing_if = "std::option::Option::is_none")] 639 639 #[serde(borrow)] 640 - pub comment: Option<jacquard_common::CowStr<'a>>, 640 + pub comment: std::option::Option<jacquard_common::CowStr<'a>>, 641 641 /// Array of DID subjects to cancel scheduled actions for 642 642 #[serde(borrow)] 643 643 pub subjects: Vec<jacquard_common::types::string::Did<'a>>,
+5 -3
crates/weaver-api/src/tools_ozone/moderation/emit_event.rs
··· 24 24 /// 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. 25 25 #[serde(skip_serializing_if = "std::option::Option::is_none")] 26 26 #[serde(borrow)] 27 - pub external_id: Option<jacquard_common::CowStr<'a>>, 27 + pub external_id: std::option::Option<jacquard_common::CowStr<'a>>, 28 28 #[serde(skip_serializing_if = "std::option::Option::is_none")] 29 29 #[serde(borrow)] 30 - pub mod_tool: Option<crate::tools_ozone::moderation::ModTool<'a>>, 30 + pub mod_tool: std::option::Option<crate::tools_ozone::moderation::ModTool<'a>>, 31 31 #[serde(borrow)] 32 32 pub subject: EmitEventSubject<'a>, 33 33 #[serde(skip_serializing_if = "std::option::Option::is_none")] 34 34 #[serde(borrow)] 35 - pub subject_blob_cids: Option<Vec<jacquard_common::types::string::Cid<'a>>>, 35 + pub subject_blob_cids: std::option::Option< 36 + Vec<jacquard_common::types::string::Cid<'a>>, 37 + >, 36 38 } 37 39 38 40 pub mod emit_event_state {
+5 -5
crates/weaver-api/src/tools_ozone/moderation/list_scheduled_actions.rs
··· 20 20 /// Cursor for pagination 21 21 #[serde(skip_serializing_if = "std::option::Option::is_none")] 22 22 #[serde(borrow)] 23 - pub cursor: Option<jacquard_common::CowStr<'a>>, 23 + pub cursor: std::option::Option<jacquard_common::CowStr<'a>>, 24 24 /// Filter actions scheduled to execute before this time 25 25 #[serde(skip_serializing_if = "std::option::Option::is_none")] 26 - pub ends_before: Option<jacquard_common::types::string::Datetime>, 26 + pub ends_before: std::option::Option<jacquard_common::types::string::Datetime>, 27 27 /// Maximum number of results to return 28 28 #[serde(skip_serializing_if = "std::option::Option::is_none")] 29 - pub limit: Option<i64>, 29 + pub limit: std::option::Option<i64>, 30 30 /// Filter actions scheduled to execute after this time 31 31 #[serde(skip_serializing_if = "std::option::Option::is_none")] 32 - pub starts_after: Option<jacquard_common::types::string::Datetime>, 32 + pub starts_after: std::option::Option<jacquard_common::types::string::Datetime>, 33 33 /// Filter actions by status 34 34 #[serde(borrow)] 35 35 pub statuses: Vec<jacquard_common::CowStr<'a>>, 36 36 /// Filter actions for specific DID subjects 37 37 #[serde(skip_serializing_if = "std::option::Option::is_none")] 38 38 #[serde(borrow)] 39 - pub subjects: Option<Vec<jacquard_common::types::string::Did<'a>>>, 39 + pub subjects: std::option::Option<Vec<jacquard_common::types::string::Did<'a>>>, 40 40 } 41 41 42 42 pub mod list_scheduled_actions_state {
+119 -2
crates/weaver-api/src/tools_ozone/moderation/schedule_action.rs
··· 21 21 pub error: jacquard_common::CowStr<'a>, 22 22 #[serde(skip_serializing_if = "std::option::Option::is_none")] 23 23 #[serde(borrow)] 24 - pub error_code: Option<jacquard_common::CowStr<'a>>, 24 + pub error_code: std::option::Option<jacquard_common::CowStr<'a>>, 25 25 #[serde(borrow)] 26 26 pub subject: jacquard_common::types::string::Did<'a>, 27 27 } ··· 574 574 }), 575 575 ); 576 576 map.insert( 577 + ::jacquard_common::smol_str::SmolStr::new_static( 578 + "emailContent", 579 + ), 580 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 581 + description: Some( 582 + ::jacquard_common::CowStr::new_static( 583 + "Email content to be sent to the user upon takedown.", 584 + ), 585 + ), 586 + format: None, 587 + default: None, 588 + min_length: None, 589 + max_length: None, 590 + min_graphemes: None, 591 + max_graphemes: None, 592 + r#enum: None, 593 + r#const: None, 594 + known_values: None, 595 + }), 596 + ); 597 + map.insert( 598 + ::jacquard_common::smol_str::SmolStr::new_static( 599 + "emailSubject", 600 + ), 601 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 602 + description: Some( 603 + ::jacquard_common::CowStr::new_static( 604 + "Subject of the email to be sent to the user upon takedown.", 605 + ), 606 + ), 607 + format: None, 608 + default: None, 609 + min_length: None, 610 + max_length: None, 611 + min_graphemes: None, 612 + max_graphemes: None, 613 + r#enum: None, 614 + r#const: None, 615 + known_values: None, 616 + }), 617 + ); 618 + map.insert( 577 619 ::jacquard_common::smol_str::SmolStr::new_static("policies"), 578 620 ::jacquard_lexicon::lexicon::LexObjectProperty::Array(::jacquard_lexicon::lexicon::LexArray { 579 621 description: Some( ··· 597 639 max_length: Some(5usize), 598 640 }), 599 641 ); 642 + map.insert( 643 + ::jacquard_common::smol_str::SmolStr::new_static( 644 + "severityLevel", 645 + ), 646 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 647 + description: Some( 648 + ::jacquard_common::CowStr::new_static( 649 + "Severity level of the violation (e.g., 'sev-0', 'sev-1', 'sev-2', etc.).", 650 + ), 651 + ), 652 + format: None, 653 + default: None, 654 + min_length: None, 655 + max_length: None, 656 + min_graphemes: None, 657 + max_graphemes: None, 658 + r#enum: None, 659 + r#const: None, 660 + known_values: None, 661 + }), 662 + ); 663 + map.insert( 664 + ::jacquard_common::smol_str::SmolStr::new_static( 665 + "strikeCount", 666 + ), 667 + ::jacquard_lexicon::lexicon::LexObjectProperty::Integer(::jacquard_lexicon::lexicon::LexInteger { 668 + description: None, 669 + default: None, 670 + minimum: None, 671 + maximum: None, 672 + r#enum: None, 673 + r#const: None, 674 + }), 675 + ); 676 + map.insert( 677 + ::jacquard_common::smol_str::SmolStr::new_static( 678 + "strikeExpiresAt", 679 + ), 680 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 681 + description: Some( 682 + ::jacquard_common::CowStr::new_static( 683 + "When the strike should expire. If not provided, the strike never expires.", 684 + ), 685 + ), 686 + format: Some( 687 + ::jacquard_lexicon::lexicon::LexStringFormat::Datetime, 688 + ), 689 + default: None, 690 + min_length: None, 691 + max_length: None, 692 + min_graphemes: None, 693 + max_graphemes: None, 694 + r#enum: None, 695 + r#const: None, 696 + known_values: None, 697 + }), 698 + ); 600 699 map 601 700 }, 602 701 }), ··· 642 741 /// This will be propagated to the moderation event when it is applied 643 742 #[serde(skip_serializing_if = "std::option::Option::is_none")] 644 743 #[serde(borrow)] 645 - pub mod_tool: Option<crate::tools_ozone::moderation::ModTool<'a>>, 744 + pub mod_tool: std::option::Option<crate::tools_ozone::moderation::ModTool<'a>>, 646 745 #[serde(borrow)] 647 746 pub scheduling: crate::tools_ozone::moderation::schedule_action::SchedulingConfig< 648 747 'a, ··· 1198 1297 /// Indicates how long the takedown should be in effect before automatically expiring. 1199 1298 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1200 1299 pub duration_in_hours: std::option::Option<i64>, 1300 + /// Email content to be sent to the user upon takedown. 1301 + #[serde(skip_serializing_if = "std::option::Option::is_none")] 1302 + #[serde(borrow)] 1303 + pub email_content: std::option::Option<jacquard_common::CowStr<'a>>, 1304 + /// Subject of the email to be sent to the user upon takedown. 1305 + #[serde(skip_serializing_if = "std::option::Option::is_none")] 1306 + #[serde(borrow)] 1307 + pub email_subject: std::option::Option<jacquard_common::CowStr<'a>>, 1201 1308 /// Names/Keywords of the policies that drove the decision. 1202 1309 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1203 1310 #[serde(borrow)] 1204 1311 pub policies: std::option::Option<Vec<jacquard_common::CowStr<'a>>>, 1312 + /// Severity level of the violation (e.g., 'sev-0', 'sev-1', 'sev-2', etc.). 1313 + #[serde(skip_serializing_if = "std::option::Option::is_none")] 1314 + #[serde(borrow)] 1315 + pub severity_level: std::option::Option<jacquard_common::CowStr<'a>>, 1316 + /// Number of strikes to assign to the user when takedown is applied. 1317 + #[serde(skip_serializing_if = "std::option::Option::is_none")] 1318 + pub strike_count: std::option::Option<i64>, 1319 + /// When the strike should expire. If not provided, the strike never expires. 1320 + #[serde(skip_serializing_if = "std::option::Option::is_none")] 1321 + pub strike_expires_at: std::option::Option<jacquard_common::types::string::Datetime>, 1205 1322 } 1206 1323 1207 1324 impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Takedown<'a> {
+2 -2
crates/weaver-api/src/tools_ozone/safelink.rs
··· 110 110 /// Optional comment about the decision 111 111 #[serde(skip_serializing_if = "std::option::Option::is_none")] 112 112 #[serde(borrow)] 113 - pub comment: Option<jacquard_common::CowStr<'a>>, 113 + pub comment: std::option::Option<jacquard_common::CowStr<'a>>, 114 114 pub created_at: jacquard_common::types::string::Datetime, 115 115 /// DID of the user who created this rule 116 116 #[serde(borrow)] ··· 1206 1206 /// Optional comment about the decision 1207 1207 #[serde(skip_serializing_if = "std::option::Option::is_none")] 1208 1208 #[serde(borrow)] 1209 - pub comment: Option<jacquard_common::CowStr<'a>>, 1209 + pub comment: std::option::Option<jacquard_common::CowStr<'a>>, 1210 1210 /// Timestamp when the rule was created 1211 1211 pub created_at: jacquard_common::types::string::Datetime, 1212 1212 /// DID of the user added the rule.
+2 -2
crates/weaver-api/src/tools_ozone/safelink/add_rule.rs
··· 22 22 /// Optional comment about the decision 23 23 #[serde(skip_serializing_if = "std::option::Option::is_none")] 24 24 #[serde(borrow)] 25 - pub comment: Option<jacquard_common::CowStr<'a>>, 25 + pub comment: std::option::Option<jacquard_common::CowStr<'a>>, 26 26 /// Author DID. Only respected when using admin auth 27 27 #[serde(skip_serializing_if = "std::option::Option::is_none")] 28 28 #[serde(borrow)] 29 - pub created_by: Option<jacquard_common::types::string::Did<'a>>, 29 + pub created_by: std::option::Option<jacquard_common::types::string::Did<'a>>, 30 30 #[serde(borrow)] 31 31 pub pattern: crate::tools_ozone::safelink::PatternType<'a>, 32 32 #[serde(borrow)]
+2 -2
crates/weaver-api/src/tools_ozone/safelink/remove_rule.rs
··· 20 20 /// Optional comment about why the rule is being removed 21 21 #[serde(skip_serializing_if = "std::option::Option::is_none")] 22 22 #[serde(borrow)] 23 - pub comment: Option<jacquard_common::CowStr<'a>>, 23 + pub comment: std::option::Option<jacquard_common::CowStr<'a>>, 24 24 /// Optional DID of the user. Only respected when using admin auth. 25 25 #[serde(skip_serializing_if = "std::option::Option::is_none")] 26 26 #[serde(borrow)] 27 - pub created_by: Option<jacquard_common::types::string::Did<'a>>, 27 + pub created_by: std::option::Option<jacquard_common::types::string::Did<'a>>, 28 28 #[serde(borrow)] 29 29 pub pattern: crate::tools_ozone::safelink::PatternType<'a>, 30 30 /// The URL or domain to remove the rule for
+2 -2
crates/weaver-api/src/tools_ozone/safelink/update_rule.rs
··· 22 22 /// Optional comment about the update 23 23 #[serde(skip_serializing_if = "std::option::Option::is_none")] 24 24 #[serde(borrow)] 25 - pub comment: Option<jacquard_common::CowStr<'a>>, 25 + pub comment: std::option::Option<jacquard_common::CowStr<'a>>, 26 26 /// Optional DID to credit as the creator. Only respected for admin_token authentication. 27 27 #[serde(skip_serializing_if = "std::option::Option::is_none")] 28 28 #[serde(borrow)] 29 - pub created_by: Option<jacquard_common::types::string::Did<'a>>, 29 + pub created_by: std::option::Option<jacquard_common::types::string::Did<'a>>, 30 30 #[serde(borrow)] 31 31 pub pattern: crate::tools_ozone::safelink::PatternType<'a>, 32 32 #[serde(borrow)]
+1 -1
crates/weaver-api/src/tools_ozone/set.rs
··· 283 283 pub created_at: jacquard_common::types::string::Datetime, 284 284 #[serde(skip_serializing_if = "std::option::Option::is_none")] 285 285 #[serde(borrow)] 286 - pub description: Option<jacquard_common::CowStr<'a>>, 286 + pub description: std::option::Option<jacquard_common::CowStr<'a>>, 287 287 #[serde(borrow)] 288 288 pub name: jacquard_common::CowStr<'a>, 289 289 pub set_size: i64,
+4 -4
crates/weaver-api/src/tools_ozone/setting.rs
··· 22 22 #[serde(rename_all = "camelCase")] 23 23 pub struct DefsOption<'a> { 24 24 #[serde(skip_serializing_if = "std::option::Option::is_none")] 25 - pub created_at: Option<jacquard_common::types::string::Datetime>, 25 + pub created_at: std::option::Option<jacquard_common::types::string::Datetime>, 26 26 #[serde(borrow)] 27 27 pub created_by: jacquard_common::types::string::Did<'a>, 28 28 #[serde(skip_serializing_if = "std::option::Option::is_none")] 29 29 #[serde(borrow)] 30 - pub description: Option<jacquard_common::CowStr<'a>>, 30 + pub description: std::option::Option<jacquard_common::CowStr<'a>>, 31 31 #[serde(borrow)] 32 32 pub did: jacquard_common::types::string::Did<'a>, 33 33 #[serde(borrow)] ··· 36 36 pub last_updated_by: jacquard_common::types::string::Did<'a>, 37 37 #[serde(skip_serializing_if = "std::option::Option::is_none")] 38 38 #[serde(borrow)] 39 - pub manager_role: Option<jacquard_common::CowStr<'a>>, 39 + pub manager_role: std::option::Option<jacquard_common::CowStr<'a>>, 40 40 #[serde(borrow)] 41 41 pub scope: jacquard_common::CowStr<'a>, 42 42 #[serde(skip_serializing_if = "std::option::Option::is_none")] 43 - pub updated_at: Option<jacquard_common::types::string::Datetime>, 43 + pub updated_at: std::option::Option<jacquard_common::types::string::Datetime>, 44 44 #[serde(borrow)] 45 45 pub value: jacquard_common::types::value::Data<'a>, 46 46 }
+2 -2
crates/weaver-api/src/tools_ozone/setting/upsert_option.rs
··· 19 19 pub struct UpsertOption<'a> { 20 20 #[serde(skip_serializing_if = "std::option::Option::is_none")] 21 21 #[serde(borrow)] 22 - pub description: Option<jacquard_common::CowStr<'a>>, 22 + pub description: std::option::Option<jacquard_common::CowStr<'a>>, 23 23 #[serde(borrow)] 24 24 pub key: jacquard_common::types::string::Nsid<'a>, 25 25 #[serde(skip_serializing_if = "std::option::Option::is_none")] 26 26 #[serde(borrow)] 27 - pub manager_role: Option<jacquard_common::CowStr<'a>>, 27 + pub manager_role: std::option::Option<jacquard_common::CowStr<'a>>, 28 28 #[serde(borrow)] 29 29 pub scope: jacquard_common::CowStr<'a>, 30 30 #[serde(borrow)]
+5 -5
crates/weaver-api/src/tools_ozone/team.rs
··· 23 23 #[serde(rename_all = "camelCase")] 24 24 pub struct Member<'a> { 25 25 #[serde(skip_serializing_if = "std::option::Option::is_none")] 26 - pub created_at: Option<jacquard_common::types::string::Datetime>, 26 + pub created_at: std::option::Option<jacquard_common::types::string::Datetime>, 27 27 #[serde(borrow)] 28 28 pub did: jacquard_common::types::string::Did<'a>, 29 29 #[serde(skip_serializing_if = "std::option::Option::is_none")] 30 - pub disabled: Option<bool>, 30 + pub disabled: std::option::Option<bool>, 31 31 #[serde(skip_serializing_if = "std::option::Option::is_none")] 32 32 #[serde(borrow)] 33 - pub last_updated_by: Option<jacquard_common::CowStr<'a>>, 33 + pub last_updated_by: std::option::Option<jacquard_common::CowStr<'a>>, 34 34 #[serde(skip_serializing_if = "std::option::Option::is_none")] 35 35 #[serde(borrow)] 36 - pub profile: Option<crate::app_bsky::actor::ProfileViewDetailed<'a>>, 36 + pub profile: std::option::Option<crate::app_bsky::actor::ProfileViewDetailed<'a>>, 37 37 #[serde(borrow)] 38 38 pub role: jacquard_common::CowStr<'a>, 39 39 #[serde(skip_serializing_if = "std::option::Option::is_none")] 40 - pub updated_at: Option<jacquard_common::types::string::Datetime>, 40 + pub updated_at: std::option::Option<jacquard_common::types::string::Datetime>, 41 41 } 42 42 43 43 pub mod member_state {
+2 -2
crates/weaver-api/src/tools_ozone/team/update_member.rs
··· 20 20 #[serde(borrow)] 21 21 pub did: jacquard_common::types::string::Did<'a>, 22 22 #[serde(skip_serializing_if = "std::option::Option::is_none")] 23 - pub disabled: Option<bool>, 23 + pub disabled: std::option::Option<bool>, 24 24 #[serde(skip_serializing_if = "std::option::Option::is_none")] 25 25 #[serde(borrow)] 26 - pub role: Option<jacquard_common::CowStr<'a>>, 26 + pub role: std::option::Option<jacquard_common::CowStr<'a>>, 27 27 } 28 28 29 29 pub mod update_member_state {
+7 -7
crates/weaver-api/src/tools_ozone/verification.rs
··· 35 35 pub issuer: jacquard_common::types::string::Did<'a>, 36 36 #[serde(skip_serializing_if = "std::option::Option::is_none")] 37 37 #[serde(borrow)] 38 - pub issuer_profile: Option<jacquard_common::types::value::Data<'a>>, 38 + pub issuer_profile: std::option::Option<jacquard_common::types::value::Data<'a>>, 39 39 #[serde(skip_serializing_if = "std::option::Option::is_none")] 40 40 #[serde(borrow)] 41 - pub issuer_repo: Option<VerificationViewIssuerRepo<'a>>, 41 + pub issuer_repo: std::option::Option<VerificationViewIssuerRepo<'a>>, 42 42 /// Describes the reason for revocation, also indicating that the verification is no longer valid. 43 43 #[serde(skip_serializing_if = "std::option::Option::is_none")] 44 44 #[serde(borrow)] 45 - pub revoke_reason: Option<jacquard_common::CowStr<'a>>, 45 + pub revoke_reason: std::option::Option<jacquard_common::CowStr<'a>>, 46 46 /// Timestamp when the verification was revoked. 47 47 #[serde(skip_serializing_if = "std::option::Option::is_none")] 48 - pub revoked_at: Option<jacquard_common::types::string::Datetime>, 48 + pub revoked_at: std::option::Option<jacquard_common::types::string::Datetime>, 49 49 /// The user who revoked this verification. 50 50 #[serde(skip_serializing_if = "std::option::Option::is_none")] 51 51 #[serde(borrow)] 52 - pub revoked_by: Option<jacquard_common::types::string::Did<'a>>, 52 + pub revoked_by: std::option::Option<jacquard_common::types::string::Did<'a>>, 53 53 /// The subject of the verification. 54 54 #[serde(borrow)] 55 55 pub subject: jacquard_common::types::string::Did<'a>, 56 56 #[serde(skip_serializing_if = "std::option::Option::is_none")] 57 57 #[serde(borrow)] 58 - pub subject_profile: Option<jacquard_common::types::value::Data<'a>>, 58 + pub subject_profile: std::option::Option<jacquard_common::types::value::Data<'a>>, 59 59 #[serde(skip_serializing_if = "std::option::Option::is_none")] 60 60 #[serde(borrow)] 61 - pub subject_repo: Option<VerificationViewSubjectRepo<'a>>, 61 + pub subject_repo: std::option::Option<VerificationViewSubjectRepo<'a>>, 62 62 /// The AT-URI of the verification record. 63 63 #[serde(borrow)] 64 64 pub uri: jacquard_common::types::string::AtUri<'a>,
+1 -1
crates/weaver-api/src/tools_ozone/verification/grant_verifications.rs
··· 618 618 pub struct VerificationInput<'a> { 619 619 /// Timestamp for verification record. Defaults to current time when not specified. 620 620 #[serde(skip_serializing_if = "std::option::Option::is_none")] 621 - pub created_at: Option<jacquard_common::types::string::Datetime>, 621 + pub created_at: std::option::Option<jacquard_common::types::string::Datetime>, 622 622 /// Display name of the subject the verification applies to at the moment of verifying. 623 623 #[serde(borrow)] 624 624 pub display_name: jacquard_common::CowStr<'a>,
+1 -1
crates/weaver-api/src/tools_ozone/verification/revoke_verifications.rs
··· 20 20 /// Reason for revoking the verification. This is optional and can be omitted if not needed. 21 21 #[serde(skip_serializing_if = "std::option::Option::is_none")] 22 22 #[serde(borrow)] 23 - pub revoke_reason: Option<jacquard_common::CowStr<'a>>, 23 + pub revoke_reason: std::option::Option<jacquard_common::CowStr<'a>>, 24 24 /// Array of verification record uris to revoke 25 25 #[serde(borrow)] 26 26 pub uris: Vec<jacquard_common::types::string::AtUri<'a>>,
+10
crates/weaver-app/.env-dev
··· 1 + WEAVER_APP_ENV="dev" 2 + WEAVER_APP_HOST="http://localhost" 3 + WEAVER_APP_DOMAIN="" 4 + WEAVER_PORT=8080 5 + WEAVER_APP_SCOPES="atproto transition:generic" 6 + WEAVER_CLIENT_NAME="Weaver" 7 + 8 + WEAVER_LOGO_URI="" 9 + WEAVER_TOS_URI="" 10 + WEAVER_PRIVACY_POLICY_URI=""
-10
crates/weaver-app/.env-prod
··· 1 - WEAVER_APP_ENV="prod" 2 - WEAVER_APP_HOST="https://alpha.weaver.sh" 3 - WEAVER_APP_DOMAIN="https://alpha.weaver.sh" 4 - WEAVER_PORT=8080 5 - WEAVER_APP_SCOPES="atproto transition:generic" 6 - WEAVER_CLIENT_NAME="Weaver" 7 - 8 - WEAVER_LOGO_URI="" 9 - WEAVER_TOS_URI="" 10 - WEAVER_PRIVACY_POLICY_URI=""
+2 -1
crates/weaver-app/Cargo.toml
··· 24 24 weaver-common = { path = "../weaver-common" } 25 25 jacquard = { workspace = true}#, features = ["streaming"] } 26 26 jacquard-lexicon = { workspace = true } 27 + jacquard-identity = { workspace = true } 27 28 jacquard-axum = { workspace = true, optional = true } 28 29 weaver-api = { path = "../weaver-api"}#, features = ["streaming"] } 29 30 markdown-weaver = { workspace = true } ··· 41 42 http = "1.3" 42 43 reqwest = { version = "0.12", default-features = false, features = ["json"] } 43 44 44 - dioxus-free-icons = { version = "0.10.0" } 45 45 # diesel = { version = "2.3", features = ["sqlite", "returning_clauses_for_sqlite_3_35", "chrono", "serde_json"] } 46 46 # diesel_migrations = { version = "2.3", features = ["sqlite"] } 47 47 tokio = { version = "1.28", features = ["sync"] } 48 48 serde_html_form = "0.2.8" 49 49 tracing.workspace = true 50 + serde_ipld_dagcbor = { version = "0.6" } 50 51 51 52 [target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies] 52 53 webbrowser = "1.0.6"
+3 -4
crates/weaver-app/src/auth/mod.rs
··· 5 5 mod state; 6 6 pub use state::AuthState; 7 7 8 + use crate::fetch::Fetcher; 8 9 #[cfg(all(feature = "fullstack-server", feature = "server"))] 9 10 use dioxus::prelude::*; 10 11 #[cfg(all(feature = "fullstack-server", feature = "server"))] ··· 25 26 } 26 27 27 28 #[cfg(not(target_arch = "wasm32"))] 28 - pub async fn restore_session() -> Result<(), String> { 29 + pub async fn restore_session(_fetcher: Fetcher) -> Result<(), String> { 29 30 Ok(()) 30 31 } 31 32 32 33 #[cfg(target_arch = "wasm32")] 33 - pub async fn restore_session() -> Result<(), CapturedError> { 34 - use crate::fetch::CachedFetcher; 34 + pub async fn restore_session(fetcher: Fetcher) -> Result<(), CapturedError> { 35 35 use dioxus::prelude::*; 36 36 use gloo_storage::{LocalStorage, Storage}; 37 37 use jacquard::types::string::Did; ··· 59 59 let (did_str, session_id) = 60 60 found_session.ok_or(CapturedError::from_display("No saved session found"))?; 61 61 let did = Did::new_owned(did_str)?; 62 - let fetcher = use_context::<CachedFetcher>(); 63 62 64 63 let session = fetcher 65 64 .client
+31 -7
crates/weaver-app/src/blobcache.rs
··· 42 42 } 43 43 AtIdentifier::Handle(handle) => self.client.pds_for_handle(&handle).await?, 44 44 }; 45 - let blob = self 46 - .client 47 - .xrpc(pds_url) 48 - .send(&GetBlob::new().cid(cid.clone()).did(repo_did).build()) 49 - .await? 50 - .buffer() 51 - .clone(); 45 + // let blob = if let Ok(blob_stream) = self 46 + // .client 47 + // .xrpc(pds_url) 48 + // .send( 49 + // &GetBlob::new() 50 + // .cid(cid.clone()) 51 + // .did(repo_did.clone()) 52 + // .build(), 53 + // ) 54 + // .await 55 + // { 56 + // blob_stream.buffer().clone() 57 + // } else { 58 + // reqwest::get(format!( 59 + // "https://cdn.bsky.app/img/feed_fullsize/plain/{}/{}@jpeg", 60 + // repo_did, cid 61 + // )) 62 + // .await? 63 + // .bytes() 64 + // .await? 65 + // .clone() 66 + // }; 67 + // 68 + let blob = reqwest::get(format!( 69 + "https://cdn.bsky.app/img/feed_fullsize/plain/{}/{}@jpeg", 70 + repo_did, cid 71 + )) 72 + .await? 73 + .bytes() 74 + .await? 75 + .clone(); 52 76 53 77 self.cache.insert(cid.clone(), blob); 54 78 if let Some(name) = name {
+2 -2
crates/weaver-app/src/components/css.rs
··· 35 35 use weaver_renderer::css::{generate_base_css, generate_syntax_css}; 36 36 use weaver_renderer::theme::{default_resolved_theme, resolve_theme}; 37 37 38 - let fetcher = use_context::<fetch::CachedFetcher>(); 38 + let fetcher = use_context::<fetch::Fetcher>(); 39 39 40 40 let css_content = use_resource(move || { 41 41 let ident = ident.clone(); ··· 87 87 } 88 88 89 89 #[cfg(feature = "fullstack-server")] 90 - #[get("/css/{ident}/{notebook}", fetcher: Extension<Arc<fetch::CachedFetcher>>)] 90 + #[get("/css/{ident}/{notebook}", fetcher: Extension<Arc<fetch::Fetcher>>)] 91 91 pub async fn css(ident: SmolStr, notebook: SmolStr) -> Result<Response> { 92 92 use dioxus::fullstack::http::header::CONTENT_TYPE; 93 93 use jacquard::client::AgentSessionExt;
+58 -66
crates/weaver-app/src/components/entry.rs
··· 4 4 use crate::blobcache::BlobCache; 5 5 use crate::{ 6 6 components::avatar::{Avatar, AvatarImage}, 7 - data::use_handle, 7 + data::{NotebookHandle, use_handle}, 8 8 }; 9 9 10 10 use crate::Route; ··· 22 22 }; 23 23 #[allow(unused_imports)] 24 24 use std::sync::Arc; 25 - use weaver_api::sh_weaver::notebook::{BookEntryView, entry}; 25 + use weaver_api::sh_weaver::notebook::{BookEntryView, EntryView, entry}; 26 + 27 + #[component] 28 + pub fn EntryPage( 29 + ident: ReadSignal<AtIdentifier<'static>>, 30 + book_title: ReadSignal<SmolStr>, 31 + title: ReadSignal<SmolStr>, 32 + ) -> Element { 33 + rsx! { 34 + {std::iter::once(rsx! {Entry {ident, book_title, title}})} 35 + } 36 + } 26 37 27 38 #[component] 28 39 pub fn Entry( ··· 31 42 title: ReadSignal<SmolStr>, 32 43 ) -> Element { 33 44 // Use feature-gated hook for SSR support 34 - let entry = crate::data::use_entry_data(ident(), book_title(), title())?; 45 + let entry = crate::data::use_entry_data(ident(), book_title(), title()); 46 + let handle = use_context::<NotebookHandle>(); 47 + let fetcher = use_context::<crate::fetch::Fetcher>(); 35 48 36 49 // Handle blob caching when entry data is available 37 - use_effect(move || { 38 - if let Some((_book_entry_view, entry_record)) = &*entry.read() { 39 - if let Some(embeds) = &entry_record.embeds { 40 - if let Some(images) = &embeds.images { 41 - // Register blob mappings with service worker (client-side only) 42 - #[cfg(all( 43 - target_family = "wasm", 44 - target_os = "unknown", 45 - not(feature = "fullstack-server") 46 - ))] 47 - { 48 - let images = images.clone(); 49 - spawn(async move { 50 - let fetcher = use_context::<fetch::CachedFetcher>(); 51 - let _ = crate::service_worker::register_entry_blobs( 52 - &ident(), 53 - book_title().as_str(), 54 - &images, 55 - &fetcher, 56 - ) 57 - .await; 58 - }); 59 - } 60 - #[cfg(feature = "fullstack-server")] 61 - { 62 - let ident = ident(); 63 - let images = images.clone(); 64 - spawn(async move { 65 - for image in &images.images { 66 - use crate::data::cache_blob; 67 - 68 - let cid = image.image.blob().cid(); 69 - cache_blob( 70 - ident.to_smolstr(), 71 - cid.to_smolstr(), 72 - image.name.as_ref().map(|n| n.to_smolstr()), 50 + if let Ok(entry) = entry { 51 + match &*entry.read() { 52 + Some((book_entry_view, entry_record)) => { 53 + if let Some(embeds) = &entry_record.embeds { 54 + if let Some(images) = &embeds.images { 55 + // Register blob mappings with service worker (client-side only) 56 + #[cfg(all( 57 + target_family = "wasm", 58 + target_os = "unknown", 59 + not(feature = "fullstack-server") 60 + ))] 61 + { 62 + let fetcher = fetcher.clone(); 63 + let images = images.clone(); 64 + spawn(async move { 65 + let _ = crate::service_worker::register_entry_blobs( 66 + &ident(), 67 + book_title().as_str(), 68 + &images, 69 + &fetcher, 73 70 ) 74 - .await 75 - .ok(); 76 - } 77 - }); 71 + .await; 72 + }); 73 + } 78 74 } 79 75 } 76 + rsx! { EntryPageView { 77 + book_entry_view: book_entry_view.clone(), 78 + entry_record: entry_record.clone(), 79 + ident: handle.as_ref().unwrap().clone(), 80 + book_title: book_title() 81 + } } 80 82 } 81 - } 82 - }); 83 - 84 - match &*entry.read_unchecked() { 85 - Some((book_entry_view, entry_record)) => { 86 - rsx! { EntryPage { 87 - book_entry_view: book_entry_view.clone(), 88 - entry_record: entry_record.clone(), 89 - ident: use_handle(ident())?(), 90 - book_title: book_title() 91 - } } 83 + _ => rsx! { p { "Loading..." } }, 92 84 } 93 - _ => rsx! { p { "Loading..." } }, 85 + } else { 86 + rsx! { p { "Loading..." } } 94 87 } 95 88 } 96 89 97 90 /// Full entry page with metadata, content, and navigation 98 91 #[component] 99 - fn EntryPage( 92 + fn EntryPageView( 100 93 book_entry_view: BookEntryView<'static>, 101 94 entry_record: entry::Entry<'static>, 102 95 ident: AtIdentifier<'static>, ··· 166 159 author_count: usize, 167 160 ) -> Element { 168 161 use crate::Route; 169 - use jacquard::{IntoStatic, from_data}; 162 + use jacquard::from_data; 170 163 use weaver_api::sh_weaver::notebook::entry::Entry; 171 164 172 165 let entry_view = &entry.entry; ··· 176 169 .map(|t| t.as_ref()) 177 170 .unwrap_or("Untitled"); 178 171 179 - let ident = use_handle(entry_view.uri.authority().clone().into_static())?; 180 - 172 + let ident = use_context::<NotebookHandle>(); 181 173 // Format date 182 174 let formatted_date = entry_view 183 175 .indexed_at ··· 204 196 rsx! { 205 197 div { class: "entry-card", 206 198 Link { 207 - to: Route::Entry { 208 - ident: ident(), 199 + to: Route::EntryPage { 200 + ident: ident.as_ref().unwrap().clone(), 209 201 book_title: book_title.clone(), 210 202 title: title.to_string().into() 211 203 }, ··· 236 228 } 237 229 } 238 230 span { class: "author-name", "{display_name}" } 239 - span { class: "meta-label", "@{ident}" } 231 + span { class: "meta-label", "@{ident.as_ref().unwrap()}" } 240 232 } 241 233 } 242 234 ProfileDataViewInner::ProfileViewDetailed(profile) => { ··· 248 240 } 249 241 } 250 242 span { class: "author-name", "{display_name}" } 251 - span { class: "meta-label", "@{ident}" } 243 + span { class: "meta-label", "@{ident.as_ref().unwrap()}" } 252 244 } 253 245 } 254 246 ProfileDataViewInner::TangledProfileView(profile) => { ··· 291 283 /// Metadata header showing title, authors, date, tags 292 284 #[component] 293 285 fn EntryMetadata( 294 - entry_view: weaver_api::sh_weaver::notebook::EntryView<'static>, 286 + entry_view: EntryView<'static>, 295 287 ident: AtIdentifier<'static>, 296 288 created_at: Datetime, 297 289 ) -> Element { ··· 402 394 #[component] 403 395 fn NavButton( 404 396 direction: &'static str, 405 - entry: weaver_api::sh_weaver::notebook::EntryView<'static>, 397 + entry: EntryView<'static>, 406 398 ident: AtIdentifier<'static>, 407 399 book_title: SmolStr, 408 400 ) -> Element { ··· 415 407 416 408 rsx! { 417 409 Link { 418 - to: Route::Entry { 410 + to: Route::EntryPage { 419 411 ident: ident.clone(), 420 412 book_title: book_title.clone(), 421 413 title: entry_title.to_string().into()
+38 -36
crates/weaver-app/src/components/identity.rs
··· 8 8 9 9 #[component] 10 10 pub fn Repository(ident: ReadSignal<AtIdentifier<'static>>) -> Element { 11 + // Fetch notebooks for this specific DID with SSR support 12 + let notebooks = data::use_notebooks_for_did(ident())?; 13 + use_context_provider(|| notebooks); 11 14 rsx! { 12 - // We can create elements inside the rsx macro with the element name followed by a block of attributes and children. 13 15 div { 14 16 Outlet::<Route> {} 15 17 } 16 18 } 17 19 } 18 20 21 + pub fn use_repo_notebook_context() 22 + -> Option<Resource<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>>> { 23 + try_use_context::<Resource<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>>>() 24 + } 25 + 19 26 #[component] 20 27 pub fn RepositoryIndex(ident: ReadSignal<AtIdentifier<'static>>) -> Element { 21 28 use crate::components::ProfileDisplay; 22 - 23 - // Fetch notebooks for this specific DID with SSR support 24 - let notebooks = data::use_notebooks_for_did(ident()).ok(); 25 - 29 + let notebooks = use_repo_notebook_context(); 26 30 rsx! { 27 31 document::Stylesheet { href: NOTEBOOK_CARD_CSS } 28 32 ··· 35 39 // Main content area 36 40 main { class: "repository-main", 37 41 div { class: "notebooks-list", 38 - if let Some(notebooks_memo) = &notebooks { 39 - match &*notebooks_memo.read_unchecked() { 40 - Some(notebook_list) => rsx! { 41 - for notebook in notebook_list.iter() { 42 - { 43 - let view = &notebook.0; 44 - let entries = &notebook.1; 45 - rsx! { 46 - div { 47 - key: "{view.cid}", 48 - NotebookCard { 49 - notebook: view.clone(), 50 - entry_refs: entries.clone() 51 - } 52 - } 53 - } 54 - } 55 - } 56 - }, 57 - None => rsx! { 58 - div { "Loading notebooks..." } 59 - } 60 - } 61 - } else { 62 - div { "Loading notebooks..." } 63 - } 42 + if let Some(notebooks) = notebooks { 43 + match &*notebooks.read() { 44 + Some(Some(notebook_list)) => rsx! { 45 + for notebook in notebook_list.iter() { 46 + { 47 + let view = &notebook.0; 48 + let entries = &notebook.1; 49 + rsx! { 50 + div { 51 + key: "{view.cid}", 52 + NotebookCard { 53 + notebook: view.clone(), 54 + entry_refs: entries.clone() 55 + } 56 + } 57 + } 58 + } 59 + } 60 + }, 61 + _ => rsx! { 62 + div { "Loading notebooks..." } 63 + } 64 + } 65 + } 64 66 } 65 67 } 66 68 } ··· 74 76 ) -> Element { 75 77 use jacquard::IntoStatic; 76 78 77 - let fetcher = use_context::<fetch::CachedFetcher>(); 79 + let fetcher = use_context::<fetch::Fetcher>(); 78 80 79 81 let title = notebook 80 82 .title ··· 109 111 div { class: "notebook-card-container", 110 112 111 113 Link { 112 - to: Route::Entry { 114 + to: Route::EntryPage { 113 115 ident: ident.clone(), 114 116 book_title: title.to_string().into(), 115 117 title: "".into() // Will redirect to first entry ··· 192 194 193 195 rsx! { 194 196 Link { 195 - to: Route::Entry { 197 + to: Route::EntryPage { 196 198 ident: ident.clone(), 197 199 book_title: book_title.clone(), 198 200 title: entry_title.to_string().into() ··· 236 238 237 239 rsx! { 238 240 Link { 239 - to: Route::Entry { 241 + to: Route::EntryPage { 240 242 ident: ident.clone(), 241 243 book_title: book_title.clone(), 242 244 title: entry_title.to_string().into() ··· 289 291 290 292 rsx! { 291 293 Link { 292 - to: Route::Entry { 294 + to: Route::EntryPage { 293 295 ident: ident.clone(), 294 296 book_title: book_title.clone(), 295 297 title: entry_title.to_string().into()
+70 -43
crates/weaver-app/src/components/login.rs
··· 4 4 use jacquard::oauth::session::ClientData; 5 5 use jacquard::{oauth::types::AuthorizeOptions, smol_str::SmolStr}; 6 6 7 - use crate::CONFIG; 7 + use crate::{CONFIG, Route}; 8 8 use crate::{ 9 9 components::{ 10 10 button::{Button, ButtonVariant}, 11 11 dialog::{DialogContent, DialogRoot, DialogTitle}, 12 12 input::Input, 13 13 }, 14 - fetch::CachedFetcher, 14 + fetch::Fetcher, 15 15 }; 16 16 17 + fn handle_submit( 18 + full_route: Route, 19 + fetcher: Fetcher, 20 + mut error: Signal<Option<String>>, 21 + mut is_loading: Signal<bool>, 22 + handle_input: Signal<String>, 23 + mut open: Signal<bool>, 24 + ) { 25 + let handle = handle_input.read().clone(); 26 + if handle.is_empty() { 27 + error.set(Some("Please enter a handle".to_string())); 28 + return; 29 + } 30 + 31 + is_loading.set(true); 32 + error.set(None); 33 + 34 + #[cfg(target_arch = "wasm32")] 35 + { 36 + use gloo_storage::Storage; 37 + gloo_storage::LocalStorage::set("cached_route", format!("{}", full_route)).ok(); 38 + spawn(async move { 39 + match start_oauth_flow(handle, fetcher).await { 40 + Ok(_) => { 41 + open.set(false); 42 + } 43 + Err(e) => { 44 + error!("Authentication failed: {}", e); 45 + error.set(Some(format!("Authentication failed: {}", e))); 46 + is_loading.set(false); 47 + } 48 + } 49 + }); 50 + } 51 + } 52 + 17 53 #[component] 18 54 pub fn LoginModal(open: Signal<bool>) -> Element { 19 55 let mut handle_input = use_signal(|| String::new()); 20 - let mut error = use_signal(|| Option::<String>::None); 21 - let mut is_loading = use_signal(|| false); 22 - 23 - let mut handle_submit = move || { 24 - let handle = handle_input.read().clone(); 25 - if handle.is_empty() { 26 - error.set(Some("Please enter a handle".to_string())); 27 - return; 28 - } 29 - 30 - is_loading.set(true); 31 - error.set(None); 32 - 33 - let fetcher = use_context::<CachedFetcher>(); 34 - 35 - #[cfg(target_arch = "wasm32")] 36 - { 37 - use crate::Route; 38 - use gloo_storage::Storage; 39 - let full_route = use_route::<Route>(); 40 - gloo_storage::LocalStorage::set("cached_route", format!("{}", full_route)).ok(); 41 - } 56 + let error = use_signal(|| Option::<String>::None); 57 + let is_loading = use_signal(|| false); 58 + let full_route = use_route::<Route>(); 59 + let fetcher = use_context::<Fetcher>(); 60 + let submit_route = full_route.clone(); 61 + let submit_fetcher = fetcher.clone(); 62 + let submit_closure1 = move || { 63 + let submit_route = submit_route.clone(); 64 + let submit_fetcher = submit_fetcher.clone(); 65 + handle_submit( 66 + submit_route, 67 + submit_fetcher, 68 + error, 69 + is_loading, 70 + handle_input, 71 + open, 72 + ); 73 + }; 42 74 43 - use_effect(move || { 44 - let handle = handle.clone(); 45 - let fetcher = fetcher.clone(); 46 - spawn(async move { 47 - match start_oauth_flow(handle, fetcher).await { 48 - Ok(_) => { 49 - open.set(false); 50 - } 51 - Err(e) => { 52 - error!("Authentication failed: {}", e); 53 - error.set(Some(format!("Authentication failed: {}", e))); 54 - is_loading.set(false); 55 - } 56 - } 57 - }); 58 - }); 75 + let submit_closure2 = move || { 76 + let submit_route = full_route.clone(); 77 + let submit_fetcher = fetcher.clone(); 78 + handle_submit( 79 + submit_route, 80 + submit_fetcher, 81 + error, 82 + is_loading, 83 + handle_input, 84 + open, 85 + ); 59 86 }; 60 87 61 88 rsx! { ··· 76 103 oninput: move |e: FormEvent| handle_input.set(e.value()), 77 104 onkeypress: move |k: KeyboardEvent| { 78 105 if k.key() == Key::Enter { 79 - handle_submit(); 106 + submit_closure1(); 80 107 } 81 108 }, 82 109 placeholder: "Enter your handle", ··· 98 125 r#type: "submit", 99 126 disabled: is_loading(), 100 127 onclick: move |_| { 101 - handle_submit(); 128 + submit_closure2(); 102 129 }, 103 130 if is_loading() { "Authenticating..." } else { "Sign In" } 104 131 } ··· 108 135 } 109 136 } 110 137 111 - async fn start_oauth_flow(handle: String, fetcher: CachedFetcher) -> Result<(), SmolStr> { 138 + async fn start_oauth_flow(handle: String, fetcher: Fetcher) -> Result<(), SmolStr> { 112 139 info!("Starting OAuth flow for handle: {}", handle); 113 140 114 141 let client_data = ClientData {
+7 -4
crates/weaver-app/src/components/mod.rs
··· 7 7 8 8 mod entry; 9 9 #[allow(unused_imports)] 10 - pub use entry::{Entry, EntryCard, EntryMarkdown}; 10 + pub use entry::{Entry, EntryCard, EntryMarkdown, EntryPage}; 11 11 12 12 pub mod identity; 13 13 #[allow(unused_imports)] ··· 21 21 pub use notebook_cover::NotebookCover; 22 22 23 23 pub mod login; 24 + 25 + pub mod record_editor; 26 + pub mod record_view; 24 27 25 28 use dioxus::prelude::*; 26 29 ··· 121 124 .with_hash_suffix(false) 122 125 .into_asset_options() 123 126 ); 124 - pub mod input; 125 - pub mod dialog; 126 - pub mod button; 127 127 pub mod accordion; 128 + pub mod button; 129 + pub mod dialog; 130 + pub mod input;
+7 -7
crates/weaver-app/src/components/notebook_cover.rs
··· 1 1 #![allow(non_snake_case)] 2 2 3 - use crate::components::avatar::{Avatar, AvatarImage}; 3 + use crate::{ 4 + components::avatar::{Avatar, AvatarImage}, 5 + data::NotebookHandle, 6 + }; 4 7 use dioxus::prelude::*; 5 8 use weaver_api::sh_weaver::notebook::NotebookView; 6 9 ··· 21 24 h1 { class: "notebook-cover-title", "{title}" } 22 25 div { "Error loading notebook details" } 23 26 } 24 - } 27 + }; 25 28 } 26 29 }; 27 30 ··· 89 92 90 93 #[component] 91 94 fn NotebookAuthor(author: weaver_api::sh_weaver::notebook::AuthorListView<'static>) -> Element { 92 - use crate::data::use_handle; 93 95 use weaver_api::sh_weaver::actor::ProfileDataViewInner; 94 96 95 97 // Author already has profile data hydrated ··· 100 102 .as_ref() 101 103 .map(|n| n.as_ref()) 102 104 .unwrap_or("Unknown"); 103 - let handle = use_handle(p.did.clone().into())?; 104 105 105 106 rsx! { 106 107 div { class: "notebook-author", ··· 111 112 } 112 113 div { class: "notebook-author-info", 113 114 div { class: "notebook-author-name", "{display_name}" } 114 - div { class: "notebook-author-handle", "@{handle()}" } 115 + div { class: "notebook-author-handle", "@{p.handle}" } 115 116 } 116 117 } 117 118 } ··· 122 123 .as_ref() 123 124 .map(|n| n.as_ref()) 124 125 .unwrap_or("Unknown"); 125 - let handle = use_handle(p.did.clone().into())?; 126 126 127 127 rsx! { 128 128 div { class: "notebook-author", ··· 133 133 } 134 134 div { class: "notebook-author-info", 135 135 div { class: "notebook-author-name", "{display_name}" } 136 - div { class: "notebook-author-handle", "@{handle()}" } 136 + div { class: "notebook-author-handle", "@{p.handle}" } 137 137 } 138 138 } 139 139 }
+65 -55
crates/weaver-app/src/components/profile.rs
··· 1 1 #![allow(non_snake_case)] 2 2 3 - use crate::{ 4 - components::{ 5 - avatar::{Avatar, AvatarImage}, 6 - BskyIcon, TangledIcon, 7 - }, 8 - data::use_handle, 3 + use crate::components::{ 4 + BskyIcon, TangledIcon, 5 + avatar::{Avatar, AvatarImage}, 6 + identity::use_repo_notebook_context, 9 7 }; 10 8 use dioxus::prelude::*; 11 9 use jacquard::types::ident::AtIdentifier; ··· 14 12 const PROFILE_CSS: Asset = asset!("/assets/styling/profile.css"); 15 13 16 14 #[component] 17 - pub fn ProfileDisplay(ident: AtIdentifier<'static>) -> Element { 15 + pub fn ProfileDisplay(ident: ReadSignal<AtIdentifier<'static>>) -> Element { 18 16 // Fetch profile data 19 - let profile = crate::data::use_profile_data(ident.clone())?; 17 + let profile = crate::data::use_profile_data(ident()); 20 18 21 - match profile().as_ref() { 22 - Some(profile_view) => rsx! { 23 - document::Stylesheet { href: PROFILE_CSS } 19 + match &*profile?.read() { 20 + Some(profile_view) => { 21 + let profile_view = use_signal(|| profile_view.clone()); 22 + rsx! { 23 + document::Stylesheet { href: PROFILE_CSS } 24 24 25 - div { class: "profile-display", 26 - // Banner if present 27 - {match &profile_view.inner { 28 - ProfileDataViewInner::ProfileView(p) => { 29 - if let Some(ref banner) = p.banner { 30 - rsx! { 31 - div { class: "profile-banner", 32 - img { src: "{banner.as_ref()}", alt: "Profile banner" } 25 + div { class: "profile-display", 26 + // Banner if present 27 + {match &profile_view.read().inner { 28 + ProfileDataViewInner::ProfileView(p) => { 29 + if let Some(ref banner) = p.banner { 30 + rsx! { 31 + div { class: "profile-banner", 32 + img { src: "{banner.as_ref()}", alt: "Profile banner" } 33 + } 33 34 } 35 + } else { 36 + rsx! { } 34 37 } 35 - } else { 36 - rsx! { } 37 38 } 38 - } 39 - ProfileDataViewInner::ProfileViewDetailed(p) => { 40 - if let Some(ref banner) = p.banner { 41 - rsx! { 42 - div { class: "profile-banner", 43 - img { src: "{banner.as_ref()}", alt: "Profile banner" } 39 + ProfileDataViewInner::ProfileViewDetailed(p) => { 40 + if let Some(ref banner) = p.banner { 41 + rsx! { 42 + div { class: "profile-banner", 43 + img { src: "{banner.as_ref()}", alt: "Profile banner" } 44 + } 44 45 } 46 + } else { 47 + rsx! { } 45 48 } 46 - } else { 47 - rsx! { } 48 49 } 49 - } 50 - _ => rsx! { } 51 - }} 50 + _ => rsx! { } 51 + }} 52 52 53 - div { class: "profile-content", 54 - // Avatar and identity 55 - ProfileIdentity { profile_view: profile_view.clone(), ident: ident.clone() } 56 - div { 57 - class: "profile-extras", 58 - // Stats 59 - ProfileStats { ident: ident.clone() } 53 + div { class: "profile-content", 54 + // Avatar and identity 55 + ProfileIdentity { profile_view, ident } 56 + div { 57 + class: "profile-extras", 58 + // Stats 59 + ProfileStats { ident } 60 60 61 - // Links 62 - ProfileLinks { profile_view: profile_view.clone(), ident: ident.clone() } 63 - } 61 + // Links 62 + ProfileLinks { profile_view, ident } 63 + } 64 64 65 65 66 + } 66 67 } 67 68 } 68 - }, 69 - None => rsx! { 69 + } 70 + _ => rsx! { 70 71 div { class: "profile-display profile-loading", 71 72 "Loading profile..." 72 73 } ··· 76 77 77 78 #[component] 78 79 fn ProfileIdentity( 79 - profile_view: weaver_api::sh_weaver::actor::ProfileDataView<'static>, 80 - ident: AtIdentifier<'static>, 80 + profile_view: ReadSignal<weaver_api::sh_weaver::actor::ProfileDataView<'static>>, 81 + ident: ReadSignal<AtIdentifier<'static>>, 81 82 ) -> Element { 82 - match &profile_view.inner { 83 + match &profile_view.read().inner { 83 84 ProfileDataViewInner::ProfileView(profile) => { 84 85 let display_name = profile 85 86 .display_name ··· 121 122 span { class: "profile-pronouns", " ({pronouns})" } 122 123 } 123 124 } 124 - div { class: "profile-handle", "@{use_handle(ident.clone())?}" } 125 + div { class: "profile-handle", "@{profile.handle}" } 125 126 126 127 if let Some(ref location) = profile.location { 127 128 div { class: "profile-location", "{location}" } ··· 155 156 156 157 div { class: "profile-name-section", 157 158 h1 { class: "profile-display-name", "{display_name}" } 158 - div { class: "profile-handle", "@{use_handle(ident.clone())?}" } 159 + div { class: "profile-handle", "@{profile.handle}" } 159 160 } 160 161 } 161 162 ··· 192 193 } 193 194 194 195 #[component] 195 - fn ProfileStats(ident: AtIdentifier<'static>) -> Element { 196 + fn ProfileStats(ident: ReadSignal<AtIdentifier<'static>>) -> Element { 196 197 // Fetch notebook count 197 - let notebooks = crate::data::use_notebooks_for_did(ident.clone())?; 198 + let notebooks = use_repo_notebook_context(); 198 199 199 - let notebook_count = notebooks().as_ref().map(|n| n.len()).unwrap_or(0); 200 + let notebook_count = if let Some(notebooks) = notebooks { 201 + if let Some(Some(notebooks)) = &*notebooks.read() { 202 + notebooks.len() 203 + } else { 204 + 0 205 + } 206 + } else { 207 + 0 208 + }; 200 209 201 210 rsx! { 202 211 div { class: "profile-stats", ··· 210 219 211 220 #[component] 212 221 fn ProfileLinks( 213 - profile_view: weaver_api::sh_weaver::actor::ProfileDataView<'static>, 214 - ident: AtIdentifier<'static>, 222 + profile_view: ReadSignal<weaver_api::sh_weaver::actor::ProfileDataView<'static>>, 223 + 224 + ident: ReadSignal<AtIdentifier<'static>>, 215 225 ) -> Element { 216 - match &profile_view.inner { 226 + match &profile_view.read().inner { 217 227 ProfileDataViewInner::ProfileView(profile) => { 218 228 rsx! { 219 229 div { class: "profile-links",
+1663
crates/weaver-app/src/components/record_editor.rs
··· 1 + use crate::Route; 2 + use crate::components::accordion::{Accordion, AccordionContent, AccordionItem, AccordionTrigger}; 3 + use crate::components::dialog::{DialogContent, DialogDescription, DialogRoot, DialogTitle}; 4 + use crate::components::record_view::{PathLabel, SchemaView, ViewMode}; 5 + use crate::fetch::Fetcher; 6 + use crate::record_utils::{create_array_item_default, infer_data_from_text, try_parse_as_type}; 7 + use dioxus::prelude::{FormData, *}; 8 + use http::StatusCode; 9 + use humansize::format_size; 10 + use jacquard::api::com_atproto::repo::get_record::GetRecordOutput; 11 + use jacquard::bytes::Bytes; 12 + use jacquard::client::AgentError; 13 + use jacquard::{atproto, prelude::*}; 14 + use jacquard::{ 15 + client::AgentSessionExt, 16 + common::{Data, IntoStatic}, 17 + types::{aturi::AtUri, ident::AtIdentifier, string::Nsid}, 18 + }; 19 + use jacquard_lexicon::lexicon::LexiconDoc; 20 + use jacquard_lexicon::validation::ValidationResult; 21 + use mime_sniffer::MimeTypeSniffer; 22 + use weaver_api::com_atproto::repo::{ 23 + create_record::CreateRecord, delete_record::DeleteRecord, put_record::PutRecord, 24 + }; 25 + // ============================================================================ 26 + // Pretty Editor: Component Hierarchy 27 + // ============================================================================ 28 + 29 + /// Main dispatcher - routes to specific field editors based on Data type 30 + #[component] 31 + fn EditableDataView( 32 + root: Signal<Data<'static>>, 33 + path: String, 34 + did: String, 35 + #[props(default)] remove_button: Option<Element>, 36 + ) -> Element { 37 + let path_for_memo = path.clone(); 38 + let root_read = root.read(); 39 + 40 + match root_read 41 + .get_at_path(&path_for_memo) 42 + .map(|d| d.clone().into_static()) 43 + { 44 + Some(Data::Object(_)) => { 45 + rsx! { EditableObjectField { root, path: path.clone(), did, remove_button } } 46 + } 47 + Some(Data::Array(_)) => rsx! { EditableArrayField { root, path: path.clone(), did } }, 48 + Some(Data::String(_)) => { 49 + rsx! { EditableStringField { root, path: path.clone(), remove_button } } 50 + } 51 + Some(Data::Integer(_)) => { 52 + rsx! { EditableIntegerField { root, path: path.clone(), remove_button } } 53 + } 54 + Some(Data::Boolean(_)) => { 55 + rsx! { EditableBooleanField { root, path: path.clone(), remove_button } } 56 + } 57 + Some(Data::Null) => rsx! { EditableNullField { root, path: path.clone(), remove_button } }, 58 + Some(Data::Blob(_)) => { 59 + rsx! { EditableBlobField { root, path: path.clone(), did, remove_button } } 60 + } 61 + Some(Data::Bytes(_)) => { 62 + rsx! { EditableBytesField { root, path: path.clone(), remove_button } } 63 + } 64 + Some(Data::CidLink(_)) => { 65 + rsx! { EditableCidLinkField { root, path: path.clone(), remove_button } } 66 + } 67 + 68 + None => rsx! { div { class: "field-error", "❌ Path not found: {path}" } }, 69 + } 70 + } 71 + 72 + // ============================================================================ 73 + // Primitive Field Editors 74 + // ============================================================================ 75 + 76 + /// String field with type preservation 77 + #[component] 78 + fn EditableStringField( 79 + root: Signal<Data<'static>>, 80 + path: String, 81 + #[props(default)] remove_button: Option<Element>, 82 + ) -> Element { 83 + use jacquard::types::LexiconStringType; 84 + 85 + let path_for_text = path.clone(); 86 + let path_for_type = path.clone(); 87 + 88 + // Get current string value 89 + let current_text = use_memo(move || { 90 + root.read() 91 + .get_at_path(&path_for_text) 92 + .and_then(|d| d.as_str()) 93 + .map(|s| s.to_string()) 94 + .unwrap_or_default() 95 + }); 96 + 97 + // Get string type (Copy, cheap to store) 98 + let string_type = use_memo(move || { 99 + root.read() 100 + .get_at_path(&path_for_type) 101 + .and_then(|d| match d { 102 + Data::String(s) => Some(s.string_type()), 103 + _ => None, 104 + }) 105 + .unwrap_or(LexiconStringType::String) 106 + }); 107 + 108 + // Local state for invalid input 109 + let mut input_text = use_signal(|| current_text()); 110 + let mut parse_error = use_signal(|| None::<String>); 111 + 112 + // Sync input when current changes 113 + use_effect(move || { 114 + input_text.set(current_text()); 115 + }); 116 + 117 + let path_for_mutation = path.clone(); 118 + let handle_input = move |evt: Event<FormData>| { 119 + let new_text = evt.value(); 120 + input_text.set(new_text.clone()); 121 + 122 + match try_parse_as_type(&new_text, string_type()) { 123 + Ok(new_atproto_str) => { 124 + parse_error.set(None); 125 + let mut new_data = root.read().clone(); 126 + new_data.set_at_path(&path_for_mutation, Data::String(new_atproto_str)); 127 + root.set(new_data); 128 + } 129 + Err(e) => { 130 + parse_error.set(Some(e)); 131 + } 132 + } 133 + }; 134 + 135 + let type_label = format!("{:?}", string_type()).to_lowercase(); 136 + let is_plain_string = string_type() == LexiconStringType::String; 137 + 138 + // Dynamic width based on content length 139 + let input_width = use_memo(move || { 140 + let len = input_text().len(); 141 + let min_width = match string_type() { 142 + LexiconStringType::Cid => 60, 143 + LexiconStringType::Nsid => 40, 144 + LexiconStringType::Did => 50, 145 + LexiconStringType::AtUri => 50, 146 + _ => 20, 147 + }; 148 + format!("{}ch", len.max(min_width)) 149 + }); 150 + 151 + rsx! { 152 + div { class: "record-field", 153 + div { class: "field-header", 154 + PathLabel { path: path.clone() } 155 + if type_label != "string" { 156 + span { class: "string-type-tag", " [{type_label}]" } 157 + } 158 + {remove_button} 159 + } 160 + if is_plain_string { 161 + textarea { 162 + value: "{input_text}", 163 + oninput: handle_input, 164 + class: if parse_error().is_some() { "invalid" } else { "" }, 165 + rows: "1", 166 + } 167 + } else { 168 + input { 169 + r#type: "text", 170 + value: "{input_text}", 171 + style: "width: {input_width}", 172 + oninput: handle_input, 173 + class: if parse_error().is_some() { "invalid" } else { "" }, 174 + } 175 + } 176 + if let Some(err) = parse_error() { 177 + span { class: "field-error", " ❌ {err}" } 178 + } 179 + } 180 + } 181 + } 182 + 183 + /// Integer field with validation 184 + #[component] 185 + fn EditableIntegerField( 186 + root: Signal<Data<'static>>, 187 + path: String, 188 + #[props(default)] remove_button: Option<Element>, 189 + ) -> Element { 190 + let path_for_memo = path.clone(); 191 + let current_value = use_memo(move || { 192 + root.read() 193 + .get_at_path(&path_for_memo) 194 + .and_then(|d| d.as_integer()) 195 + .unwrap_or(0) 196 + }); 197 + 198 + let mut input_text = use_signal(|| current_value().to_string()); 199 + let mut parse_error = use_signal(|| None::<String>); 200 + 201 + use_effect(move || { 202 + input_text.set(current_value().to_string()); 203 + }); 204 + 205 + let path_for_mutation = path.clone(); 206 + 207 + rsx! { 208 + div { class: "record-field", 209 + div { class: "field-header", 210 + PathLabel { path: path.clone() } 211 + {remove_button} 212 + } 213 + input { 214 + r#type: "number", 215 + value: "{input_text}", 216 + oninput: move |evt| { 217 + let text = evt.value(); 218 + input_text.set(text.clone()); 219 + 220 + match text.parse::<i64>() { 221 + Ok(num) => { 222 + parse_error.set(None); 223 + let mut data_edit = root.write_unchecked(); 224 + data_edit.set_at_path(&path_for_mutation, Data::Integer(num)); 225 + } 226 + Err(_) => { 227 + parse_error.set(Some("Must be a valid integer".to_string())); 228 + } 229 + } 230 + } 231 + } 232 + if let Some(err) = parse_error() { 233 + span { class: "field-error", " ❌ {err}" } 234 + } 235 + } 236 + } 237 + } 238 + 239 + /// Boolean field (toggle button) 240 + #[component] 241 + fn EditableBooleanField( 242 + root: Signal<Data<'static>>, 243 + path: String, 244 + #[props(default)] remove_button: Option<Element>, 245 + ) -> Element { 246 + let path_for_memo = path.clone(); 247 + let current_value = use_memo(move || { 248 + root.read() 249 + .get_at_path(&path_for_memo) 250 + .and_then(|d| d.as_boolean()) 251 + .unwrap_or(false) 252 + }); 253 + 254 + let path_for_mutation = path.clone(); 255 + rsx! { 256 + div { class: "record-field", 257 + div { class: "field-header", 258 + PathLabel { path: path.clone() } 259 + {remove_button} 260 + } 261 + button { 262 + class: if current_value() { "boolean-toggle boolean-toggle-true" } else { "boolean-toggle boolean-toggle-false" }, 263 + onclick: move |_| { 264 + root.with_mut(|data| { 265 + if let Some(target) = data.get_at_path_mut(path_for_mutation.as_str()) { 266 + if let Some(bool_val) = target.as_boolean() { 267 + *target = Data::Boolean(!bool_val); 268 + } 269 + } 270 + }); 271 + }, 272 + "{current_value()}" 273 + } 274 + } 275 + } 276 + } 277 + 278 + /// Null field with type inference 279 + #[component] 280 + fn EditableNullField( 281 + root: Signal<Data<'static>>, 282 + path: String, 283 + #[props(default)] remove_button: Option<Element>, 284 + ) -> Element { 285 + let mut input_text = use_signal(|| String::new()); 286 + let mut parse_error = use_signal(|| None::<String>); 287 + 288 + let path_for_mutation = path.clone(); 289 + rsx! { 290 + div { class: "record-field", 291 + div { class: "field-header", 292 + PathLabel { path: path.clone() } 293 + span { class: "field-value muted", "null" } 294 + {remove_button} 295 + } 296 + input { 297 + r#type: "text", 298 + placeholder: "Enter value (or {{}}, [], true, 123)...", 299 + value: "{input_text}", 300 + oninput: move |evt| { 301 + input_text.set(evt.value()); 302 + }, 303 + onkeydown: move |evt| { 304 + use dioxus::prelude::keyboard_types::Key; 305 + if evt.key() == Key::Enter { 306 + let text = input_text(); 307 + match infer_data_from_text(&text) { 308 + Ok(new_value) => { 309 + root.with_mut(|data| { 310 + if let Some(target) = data.get_at_path_mut(path_for_mutation.as_str()) { 311 + *target = new_value; 312 + } 313 + }); 314 + input_text.set(String::new()); 315 + parse_error.set(None); 316 + } 317 + Err(e) => { 318 + parse_error.set(Some(e)); 319 + } 320 + } 321 + } 322 + } 323 + } 324 + if let Some(err) = parse_error() { 325 + span { class: "field-error", " ❌ {err}" } 326 + } 327 + } 328 + } 329 + } 330 + 331 + /// Blob field - shows CID, size (editable), mime type (read-only), file upload 332 + #[component] 333 + fn EditableBlobField( 334 + root: Signal<Data<'static>>, 335 + path: String, 336 + did: String, 337 + #[props(default)] remove_button: Option<Element>, 338 + ) -> Element { 339 + let path_for_memo = path.clone(); 340 + let blob_data = use_memo(move || { 341 + root.read() 342 + .get_at_path(&path_for_memo) 343 + .and_then(|d| match d { 344 + Data::Blob(blob) => Some(( 345 + blob.r#ref.to_string(), 346 + blob.size, 347 + blob.mime_type.as_str().to_string(), 348 + )), 349 + _ => None, 350 + }) 351 + }); 352 + 353 + let mut cid_input = use_signal(|| String::new()); 354 + let mut size_input = use_signal(|| String::new()); 355 + let mut cid_error = use_signal(|| None::<String>); 356 + let mut size_error = use_signal(|| None::<String>); 357 + let mut uploading = use_signal(|| false); 358 + let mut upload_error = use_signal(|| None::<String>); 359 + let mut preview_data_url = use_signal(|| None::<String>); 360 + 361 + // Sync inputs when blob data changes 362 + use_effect(move || { 363 + if let Some((cid, size, _)) = blob_data() { 364 + cid_input.set(cid); 365 + size_input.set(size.to_string()); 366 + } 367 + }); 368 + 369 + let fetcher = use_context::<Fetcher>(); 370 + let path_for_upload = path.clone(); 371 + let handle_file = move |evt: Event<FormData>| { 372 + let fetcher = fetcher.clone(); 373 + let path_upload_clone = path_for_upload.clone(); 374 + spawn(async move { 375 + uploading.set(true); 376 + upload_error.set(None); 377 + 378 + let files = evt.files(); 379 + for file_data in files { 380 + match file_data.read_bytes().await { 381 + Ok(bytes_data) => { 382 + // Convert to jacquard Bytes and sniff MIME type 383 + let bytes = Bytes::from(bytes_data.to_vec()); 384 + let mime_str = bytes 385 + .sniff_mime_type() 386 + .unwrap_or("application/octet-stream"); 387 + let mime_type = jacquard::types::blob::MimeType::new_owned(mime_str); 388 + 389 + // Create data URL for immediate preview if it's an image 390 + if mime_str.starts_with("image/") { 391 + let base64_data = base64::Engine::encode( 392 + &base64::engine::general_purpose::STANDARD, 393 + &bytes, 394 + ); 395 + let data_url = format!("data:{};base64,{}", mime_str, base64_data); 396 + preview_data_url.set(Some(data_url.clone())); 397 + 398 + // Try to decode dimensions and populate aspectRatio field 399 + #[cfg(target_arch = "wasm32")] 400 + { 401 + let path_clone = path_upload_clone.clone(); 402 + spawn(async move { 403 + if let Some((width, height)) = 404 + decode_image_dimensions(&data_url).await 405 + { 406 + populate_aspect_ratio( 407 + root, 408 + &path_clone, 409 + width as i64, 410 + height as i64, 411 + ); 412 + } 413 + }); 414 + } 415 + } 416 + 417 + // Upload blob 418 + let client = fetcher.get_client(); 419 + match client.upload_blob(bytes, mime_type).await { 420 + Ok(new_blob) => { 421 + // Update blob in record 422 + let path_ref = path_upload_clone.clone(); 423 + root.with_mut(|record_data| { 424 + if let Some(Data::Blob(blob)) = 425 + record_data.get_at_path_mut(&path_ref) 426 + { 427 + *blob = new_blob; 428 + } 429 + }); 430 + upload_error.set(None); 431 + } 432 + Err(e) => { 433 + upload_error.set(Some(format!("Upload failed: {:?}", e))); 434 + } 435 + } 436 + } 437 + Err(e) => { 438 + upload_error.set(Some(format!("Failed to read file: {}", e))); 439 + } 440 + } 441 + } 442 + 443 + uploading.set(false); 444 + }); 445 + }; 446 + 447 + let path_for_cid = path.clone(); 448 + let handle_cid_change = move |evt: Event<FormData>| { 449 + let text = evt.value(); 450 + cid_input.set(text.clone()); 451 + 452 + match jacquard::types::cid::CidLink::new_owned(text.as_bytes()) { 453 + Ok(new_cid_link) => { 454 + cid_error.set(None); 455 + root.with_mut(|data| { 456 + if let Some(Data::Blob(blob)) = data.get_at_path_mut(&path_for_cid) { 457 + blob.r#ref = new_cid_link; 458 + } 459 + }); 460 + } 461 + Err(_) => { 462 + cid_error.set(Some("Invalid CID format".to_string())); 463 + } 464 + } 465 + }; 466 + 467 + let path_for_size = path.clone(); 468 + let handle_size_change = move |evt: Event<FormData>| { 469 + let text = evt.value(); 470 + size_input.set(text.clone()); 471 + 472 + match text.parse::<usize>() { 473 + Ok(new_size) => { 474 + size_input.set(format_size(new_size, humansize::BINARY)); 475 + size_error.set(None); 476 + root.with_mut(|data| { 477 + if let Some(Data::Blob(blob)) = data.get_at_path_mut(&path_for_size) { 478 + blob.size = new_size; 479 + } 480 + }); 481 + } 482 + Err(_) => { 483 + size_error.set(Some("Must be a non-negative integer".to_string())); 484 + } 485 + } 486 + }; 487 + 488 + let placeholder_cid = "bafkreiaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; 489 + let is_placeholder = blob_data() 490 + .map(|(cid, _, _)| cid == placeholder_cid) 491 + .unwrap_or(true); 492 + let is_image = blob_data() 493 + .map(|(_, _, mime)| mime.starts_with("image/")) 494 + .unwrap_or(false); 495 + 496 + // Use preview data URL if available (fresh upload), otherwise CDN 497 + let image_url = if let Some(data_url) = preview_data_url() { 498 + Some(data_url) 499 + } else if !is_placeholder && is_image { 500 + blob_data().map(|(cid, _, mime)| { 501 + let format = mime.strip_prefix("image/").unwrap_or("jpeg"); 502 + format!( 503 + "https://cdn.bsky.app/img/feed_fullsize/plain/{}/{}@{}", 504 + did, cid, format 505 + ) 506 + }) 507 + } else { 508 + None 509 + }; 510 + 511 + rsx! { 512 + div { class: "record-field blob-field", 513 + div { class: "field-header", 514 + PathLabel { path: path.clone() } 515 + span { class: "string-type-tag", " [blob]" } 516 + {remove_button} 517 + } 518 + div { class: "blob-fields", 519 + div { class: "blob-field-row blob-field-cid", 520 + label { "CID:" } 521 + input { 522 + r#type: "text", 523 + value: "{cid_input}", 524 + oninput: handle_cid_change, 525 + class: if cid_error().is_some() { "invalid" } else { "" }, 526 + } 527 + if let Some(err) = cid_error() { 528 + span { class: "field-error", " ❌ {err}" } 529 + } 530 + } 531 + div { class: "blob-field-row", 532 + label { "Size:" } 533 + input { 534 + r#type: "number", 535 + value: "{size_input}", 536 + oninput: handle_size_change, 537 + class: if size_error().is_some() { "invalid" } else { "" }, 538 + } 539 + if let Some(err) = size_error() { 540 + span { class: "field-error", " ❌ {err}" } 541 + } 542 + } 543 + div { class: "blob-field-row", 544 + label { "MIME Type:" } 545 + span { class: "readonly", 546 + "{blob_data().map(|(_, _, mime)| mime).unwrap_or_default()}" 547 + } 548 + } 549 + if let Some(url) = image_url { 550 + img { 551 + src: "{url}", 552 + alt: "Blob preview", 553 + class: "blob-image", 554 + } 555 + } 556 + div { class: "blob-upload-section", 557 + input { 558 + r#type: "file", 559 + accept: if is_image { "image/*" } else { "*/*" }, 560 + onchange: handle_file, 561 + disabled: uploading(), 562 + } 563 + if uploading() { 564 + span { class: "upload-status", "Uploading..." } 565 + } 566 + if let Some(err) = upload_error() { 567 + div { class: "field-error", "❌ {err}" } 568 + } 569 + } 570 + } 571 + } 572 + } 573 + } 574 + 575 + /// Decode image dimensions from data URL using browser Image API 576 + #[cfg(target_arch = "wasm32")] 577 + async fn decode_image_dimensions(data_url: &str) -> Option<(u32, u32)> { 578 + use wasm_bindgen::JsCast; 579 + use wasm_bindgen::prelude::*; 580 + use wasm_bindgen_futures::JsFuture; 581 + 582 + let window = web_sys::window()?; 583 + let document = window.document()?; 584 + 585 + let img = document.create_element("img").ok()?; 586 + let img = img.dyn_into::<web_sys::HtmlImageElement>().ok()?; 587 + 588 + img.set_src(data_url); 589 + 590 + // Wait for image to load 591 + let promise = js_sys::Promise::new(&mut |resolve, _reject| { 592 + let onload = Closure::wrap(Box::new(move || { 593 + resolve.call0(&JsValue::NULL).ok(); 594 + }) as Box<dyn FnMut()>); 595 + 596 + img.set_onload(Some(onload.as_ref().unchecked_ref())); 597 + onload.forget(); 598 + }); 599 + 600 + JsFuture::from(promise).await.ok()?; 601 + 602 + Some((img.natural_width(), img.natural_height())) 603 + } 604 + 605 + /// Find and populate aspectRatio field for a blob 606 + #[allow(unused)] 607 + fn populate_aspect_ratio( 608 + mut root: Signal<Data<'static>>, 609 + blob_path: &str, 610 + width: i64, 611 + height: i64, 612 + ) { 613 + // Query for all aspectRatio fields and collect the path we want 614 + let aspect_path_to_update = { 615 + let data = root.read(); 616 + let query_result = data.query("...aspectRatio"); 617 + 618 + query_result.multiple().and_then(|matches| { 619 + // Find aspectRatio that's a sibling of our blob 620 + // e.g. blob at "embed.images[0].image" -> look for "embed.images[0].aspectRatio" 621 + let blob_parent = blob_path.rsplit_once('.').map(|(parent, _)| parent); 622 + matches.iter().find_map(|query_match| { 623 + let aspect_parent = query_match.path.rsplit_once('.').map(|(parent, _)| parent); 624 + 625 + // Check if they share the same parent 626 + if blob_parent == aspect_parent { 627 + Some(query_match.path.clone()) 628 + } else { 629 + None 630 + } 631 + }) 632 + }) 633 + }; 634 + 635 + // Update the aspectRatio if we found a matching field 636 + if let Some(aspect_path) = aspect_path_to_update { 637 + let aspect_obj = atproto! {{ 638 + "width": width, 639 + "height": height 640 + }}; 641 + 642 + root.with_mut(|record_data| { 643 + record_data.set_at_path(&aspect_path, aspect_obj); 644 + }); 645 + } 646 + } 647 + 648 + /// Bytes field with hex/base64 auto-detection 649 + #[component] 650 + fn EditableBytesField( 651 + root: Signal<Data<'static>>, 652 + path: String, 653 + #[props(default)] remove_button: Option<Element>, 654 + ) -> Element { 655 + let path_for_memo = path.clone(); 656 + let current_bytes = use_memo(move || { 657 + root.read() 658 + .get_at_path(&path_for_memo) 659 + .and_then(|d| match d { 660 + Data::Bytes(b) => Some(bytes_to_hex(b)), 661 + _ => None, 662 + }) 663 + }); 664 + 665 + let mut input_text = use_signal(|| String::new()); 666 + let mut parse_error = use_signal(|| None::<String>); 667 + let mut detected_format = use_signal(|| None::<String>); 668 + 669 + // Sync input when bytes change 670 + use_effect(move || { 671 + if let Some(hex) = current_bytes() { 672 + input_text.set(hex); 673 + } 674 + }); 675 + 676 + let path_for_mutation = path.clone(); 677 + let handle_input = move |evt: Event<FormData>| { 678 + let text = evt.value(); 679 + input_text.set(text.clone()); 680 + 681 + match parse_bytes_input(&text) { 682 + Ok((bytes, format)) => { 683 + parse_error.set(None); 684 + detected_format.set(Some(format)); 685 + root.with_mut(|data| { 686 + if let Some(target) = data.get_at_path_mut(&path_for_mutation) { 687 + *target = Data::Bytes(bytes); 688 + } 689 + }); 690 + } 691 + Err(e) => { 692 + parse_error.set(Some(e)); 693 + detected_format.set(None); 694 + } 695 + } 696 + }; 697 + 698 + let byte_count = current_bytes() 699 + .map(|hex| hex.chars().filter(|c| c.is_ascii_hexdigit()).count() / 2) 700 + .unwrap_or(0); 701 + let size_label = if byte_count > 128 { 702 + format_size(byte_count, humansize::BINARY) 703 + } else { 704 + format!("{} bytes", byte_count) 705 + }; 706 + 707 + rsx! { 708 + div { class: "record-field bytes-field", 709 + div { class: "field-header", 710 + PathLabel { path: path.clone() } 711 + span { class: "string-type-tag", " [bytes: {size_label}]" } 712 + if let Some(format) = detected_format() { 713 + span { class: "bytes-format-tag", " ({format})" } 714 + } 715 + {remove_button} 716 + } 717 + textarea { 718 + value: "{input_text}", 719 + placeholder: "Paste hex (1a2b3c...) or base64 (YWJj...)", 720 + oninput: handle_input, 721 + class: if parse_error().is_some() { "invalid" } else { "" }, 722 + rows: "3", 723 + } 724 + if let Some(err) = parse_error() { 725 + span { class: "field-error", " ❌ {err}" } 726 + } 727 + } 728 + } 729 + } 730 + 731 + /// Parse bytes from hex or base64, auto-detecting format 732 + fn parse_bytes_input(text: &str) -> Result<(Bytes, String), String> { 733 + let trimmed = text.trim(); 734 + if trimmed.is_empty() { 735 + return Err("Input is empty".to_string()); 736 + } 737 + 738 + // Remove common whitespace/separators 739 + let cleaned: String = trimmed 740 + .chars() 741 + .filter(|c| !c.is_whitespace() && *c != ':' && *c != '-') 742 + .collect(); 743 + 744 + // Try hex first (more restrictive) 745 + if cleaned.chars().all(|c| c.is_ascii_hexdigit()) { 746 + parse_hex_bytes(&cleaned).map(|b| (b, "hex".to_string())) 747 + } else { 748 + // Try base64 749 + parse_base64_bytes(&cleaned).map(|b| (b, "base64".to_string())) 750 + } 751 + } 752 + 753 + /// Parse hex string to bytes 754 + fn parse_hex_bytes(hex: &str) -> Result<Bytes, String> { 755 + if hex.len() % 2 != 0 { 756 + return Err("Hex string must have even length".to_string()); 757 + } 758 + 759 + let mut bytes = Vec::with_capacity(hex.len() / 2); 760 + for chunk in hex.as_bytes().chunks(2) { 761 + let hex_byte = std::str::from_utf8(chunk).map_err(|e| format!("Invalid UTF-8: {}", e))?; 762 + let byte = 763 + u8::from_str_radix(hex_byte, 16).map_err(|e| format!("Invalid hex digit: {}", e))?; 764 + bytes.push(byte); 765 + } 766 + 767 + Ok(Bytes::from(bytes)) 768 + } 769 + 770 + /// Parse base64 string to bytes 771 + fn parse_base64_bytes(b64: &str) -> Result<Bytes, String> { 772 + use base64::Engine; 773 + let engine = base64::engine::general_purpose::STANDARD; 774 + 775 + engine 776 + .decode(b64) 777 + .map(Bytes::from) 778 + .map_err(|e| format!("Invalid base64: {}", e)) 779 + } 780 + 781 + /// Convert bytes to hex display string (with spacing every 4 chars) 782 + fn bytes_to_hex(bytes: &Bytes) -> String { 783 + bytes 784 + .iter() 785 + .enumerate() 786 + .map(|(i, b)| { 787 + let hex = format!("{:02x}", b); 788 + if i > 0 && i % 2 == 0 { 789 + format!(" {}", hex) 790 + } else { 791 + hex 792 + } 793 + }) 794 + .collect() 795 + } 796 + 797 + /// CidLink field with validation 798 + #[component] 799 + fn EditableCidLinkField( 800 + root: Signal<Data<'static>>, 801 + path: String, 802 + #[props(default)] remove_button: Option<Element>, 803 + ) -> Element { 804 + let path_for_memo = path.clone(); 805 + let current_cid = use_memo(move || { 806 + root.read() 807 + .get_at_path(&path_for_memo) 808 + .map(|d| match d { 809 + Data::CidLink(cid) => cid.to_string(), 810 + _ => String::new(), 811 + }) 812 + .unwrap_or_default() 813 + }); 814 + 815 + let mut input_text = use_signal(|| String::new()); 816 + let mut parse_error = use_signal(|| None::<String>); 817 + 818 + use_effect(move || { 819 + input_text.set(current_cid()); 820 + }); 821 + 822 + let input_width = use_memo(move || { 823 + let len = input_text().len(); 824 + format!("{}ch", len.max(60)) 825 + }); 826 + 827 + let path_for_mutation = path.clone(); 828 + let handle_input = move |evt: Event<FormData>| { 829 + let text = evt.value(); 830 + input_text.set(text.clone()); 831 + 832 + match jacquard::types::cid::Cid::new_owned(text.as_bytes()) { 833 + Ok(new_cid) => { 834 + parse_error.set(None); 835 + root.with_mut(|data| { 836 + if let Some(target) = data.get_at_path_mut(&path_for_mutation) { 837 + *target = Data::CidLink(new_cid); 838 + } 839 + }); 840 + } 841 + Err(_) => { 842 + parse_error.set(Some("Invalid CID format".to_string())); 843 + } 844 + } 845 + }; 846 + 847 + rsx! { 848 + div { class: "record-field cidlink-field", 849 + div { class: "field-header", 850 + PathLabel { path: path.clone() } 851 + span { class: "string-type-tag", " [cid-link]" } 852 + {remove_button} 853 + } 854 + input { 855 + r#type: "text", 856 + value: "{input_text}", 857 + style: "width: {input_width}", 858 + placeholder: "bafyrei...", 859 + oninput: handle_input, 860 + class: if parse_error().is_some() { "invalid" } else { "" }, 861 + } 862 + if let Some(err) = parse_error() { 863 + span { class: "field-error", " ❌ {err}" } 864 + } 865 + } 866 + } 867 + } 868 + 869 + // ============================================================================ 870 + // Field with Remove Button Wrapper 871 + // ============================================================================ 872 + 873 + /// Wraps a field with an optional remove button in the header 874 + #[component] 875 + fn FieldWithRemove( 876 + root: Signal<Data<'static>>, 877 + path: String, 878 + did: String, 879 + is_removable: bool, 880 + parent_path: String, 881 + field_key: String, 882 + ) -> Element { 883 + let remove_button = if is_removable { 884 + Some(rsx! { 885 + button { 886 + class: "field-remove-button", 887 + onclick: move |_| { 888 + let mut new_data = root.read().clone(); 889 + if let Some(Data::Object(obj)) = new_data.get_at_path_mut(parent_path.as_str()) { 890 + obj.0.remove(field_key.as_str()); 891 + } 892 + root.set(new_data); 893 + }, 894 + "Remove" 895 + } 896 + }) 897 + } else { 898 + None 899 + }; 900 + 901 + rsx! { 902 + EditableDataView { 903 + root: root, 904 + path: path.clone(), 905 + did: did.clone(), 906 + remove_button: remove_button, 907 + } 908 + } 909 + } 910 + 911 + // ============================================================================ 912 + // Array Field Editor (enables recursion) 913 + // ============================================================================ 914 + 915 + /// Array field - iterates items and renders child EditableDataView for each 916 + #[component] 917 + fn EditableArrayField(root: Signal<Data<'static>>, path: String, did: String) -> Element { 918 + let path_for_memo = path.clone(); 919 + let array_len = use_memo(move || { 920 + root.read() 921 + .get_at_path(&path_for_memo) 922 + .and_then(|d| d.as_array()) 923 + .map(|arr| arr.0.len()) 924 + .unwrap_or(0) 925 + }); 926 + 927 + let path_for_add = path.clone(); 928 + 929 + rsx! { 930 + div { class: "record-section array-section", 931 + Accordion { 932 + id: "edit-array-{path}", 933 + collapsible: true, 934 + AccordionItem { 935 + default_open: true, 936 + index: 0, 937 + AccordionTrigger { 938 + div { class: "section-header", 939 + div { class: "section-label", 940 + { 941 + let parts: Vec<&str> = path.split('.').collect(); 942 + let final_part = parts.last().unwrap_or(&""); 943 + rsx! { "{final_part}" } 944 + } 945 + } 946 + span { class: "array-length", "[{array_len}]" } 947 + } 948 + } 949 + AccordionContent { 950 + div { class: "section-content", 951 + for idx in 0..array_len() { 952 + { 953 + let item_path = format!("{}[{}]", path, idx); 954 + let path_for_remove = path.clone(); 955 + 956 + rsx! { 957 + div { 958 + class: "array-item", 959 + key: "{item_path}", 960 + 961 + EditableDataView { 962 + root: root, 963 + path: item_path.clone(), 964 + did: did.clone(), 965 + remove_button: rsx! { 966 + button { 967 + class: "field-remove-button", 968 + onclick: move |_| { 969 + root.with_mut(|data| { 970 + if let Some(Data::Array(arr)) = data.get_at_path_mut(&path_for_remove) { 971 + arr.0.remove(idx); 972 + } 973 + }); 974 + }, 975 + "Remove" 976 + } 977 + } 978 + } 979 + } 980 + } 981 + } 982 + } 983 + div { 984 + class: "array-item", 985 + div { 986 + class: "add-field-widget", 987 + button { 988 + onclick: move |_| { 989 + root.with_mut(|data| { 990 + if let Some(Data::Array(arr)) = data.get_at_path_mut(&path_for_add) { 991 + let new_item = create_array_item_default(arr); 992 + arr.0.push(new_item); 993 + } 994 + }); 995 + }, 996 + "+ Add Item" 997 + } 998 + } 999 + } 1000 + } 1001 + } 1002 + } 1003 + } 1004 + } 1005 + } 1006 + } 1007 + 1008 + // ============================================================================ 1009 + // Object Field Editor (enables recursion) 1010 + // ============================================================================ 1011 + 1012 + /// Object field - iterates fields and renders child EditableDataView for each 1013 + #[component] 1014 + fn EditableObjectField( 1015 + root: Signal<Data<'static>>, 1016 + path: String, 1017 + did: String, 1018 + #[props(default)] remove_button: Option<Element>, 1019 + ) -> Element { 1020 + let path_for_memo = path.clone(); 1021 + let field_keys = use_memo(move || { 1022 + root.read() 1023 + .get_at_path(&path_for_memo) 1024 + .and_then(|d| d.as_object()) 1025 + .map(|obj| obj.0.keys().cloned().collect::<Vec<_>>()) 1026 + .unwrap_or_default() 1027 + }); 1028 + 1029 + let is_root = path.is_empty(); 1030 + 1031 + rsx! { 1032 + if !is_root { 1033 + div { class: "record-section object-section", 1034 + Accordion { 1035 + id: "edit-object-{path}", 1036 + collapsible: true, 1037 + AccordionItem { 1038 + default_open: true, 1039 + index: 0, 1040 + AccordionTrigger { 1041 + div { class: "section-header", 1042 + div { class: "section-label", 1043 + { 1044 + let parts: Vec<&str> = path.split('.').collect(); 1045 + let final_part = parts.last().unwrap_or(&""); 1046 + rsx! { "{final_part}" } 1047 + } 1048 + } 1049 + {remove_button} 1050 + } 1051 + } 1052 + AccordionContent { 1053 + div { class: "section-content", 1054 + for key in field_keys() { 1055 + { 1056 + let field_path = if path.is_empty() { 1057 + key.to_string() 1058 + } else { 1059 + format!("{}.{}", path, key) 1060 + }; 1061 + let is_type_field = key == "$type"; 1062 + 1063 + rsx! { 1064 + FieldWithRemove { 1065 + key: "{field_path}", 1066 + root: root, 1067 + path: field_path.clone(), 1068 + did: did.clone(), 1069 + is_removable: !is_type_field, 1070 + parent_path: path.clone(), 1071 + field_key: key.clone(), 1072 + } 1073 + } 1074 + } 1075 + } 1076 + 1077 + AddFieldWidget { root: root, path: path.clone() } 1078 + } 1079 + } 1080 + } 1081 + } 1082 + } 1083 + } else { 1084 + for key in field_keys() { 1085 + { 1086 + let field_path = key.to_string(); 1087 + let is_type_field = key == "$type"; 1088 + 1089 + rsx! { 1090 + FieldWithRemove { 1091 + key: "{field_path}", 1092 + root: root, 1093 + path: field_path.clone(), 1094 + did: did.clone(), 1095 + is_removable: !is_type_field, 1096 + parent_path: path.clone(), 1097 + field_key: key.clone(), 1098 + } 1099 + } 1100 + } 1101 + } 1102 + 1103 + AddFieldWidget { root: root, path: path.clone() } 1104 + } 1105 + } 1106 + } 1107 + 1108 + /// Widget for adding new fields to objects 1109 + #[component] 1110 + fn AddFieldWidget(root: Signal<Data<'static>>, path: String) -> Element { 1111 + let mut field_name = use_signal(|| String::new()); 1112 + let mut field_value = use_signal(|| String::new()); 1113 + let mut error = use_signal(|| None::<String>); 1114 + let mut show_form = use_signal(|| false); 1115 + 1116 + let path_for_enter = path.clone(); 1117 + let path_for_button = path.clone(); 1118 + 1119 + rsx! { 1120 + div { class: "add-field-widget", 1121 + if !show_form() { 1122 + button { 1123 + class: "add-button", 1124 + onclick: move |_| show_form.set(true), 1125 + "+ Add Field" 1126 + } 1127 + } else { 1128 + div { class: "add-field-form", 1129 + input { 1130 + r#type: "text", 1131 + placeholder: "Field name", 1132 + value: "{field_name}", 1133 + oninput: move |evt| field_name.set(evt.value()), 1134 + } 1135 + input { 1136 + r#type: "text", 1137 + placeholder: r#"Value: {{}}, [], true, 123, "text""#, 1138 + value: "{field_value}", 1139 + oninput: move |evt| field_value.set(evt.value()), 1140 + onkeydown: move |evt| { 1141 + use dioxus::prelude::keyboard_types::Key; 1142 + if evt.key() == Key::Enter { 1143 + let name = field_name(); 1144 + let value_text = field_value(); 1145 + 1146 + if name.is_empty() { 1147 + error.set(Some("Field name required".to_string())); 1148 + return; 1149 + } 1150 + 1151 + let new_value = match infer_data_from_text(&value_text) { 1152 + Ok(data) => data, 1153 + Err(e) => { 1154 + error.set(Some(e)); 1155 + return; 1156 + } 1157 + }; 1158 + 1159 + let mut new_data = root.read().clone(); 1160 + if let Some(Data::Object(obj)) = new_data.get_at_path_mut(path_for_enter.as_str()) { 1161 + obj.0.insert(name.into(), new_value); 1162 + } 1163 + root.set(new_data); 1164 + 1165 + // Reset form 1166 + field_name.set(String::new()); 1167 + field_value.set(String::new()); 1168 + show_form.set(false); 1169 + error.set(None); 1170 + } 1171 + } 1172 + } 1173 + button { 1174 + class: "add-field-widget-edit", 1175 + onclick: move |_| { 1176 + let name = field_name(); 1177 + let value_text = field_value(); 1178 + 1179 + if name.is_empty() { 1180 + error.set(Some("Field name required".to_string())); 1181 + return; 1182 + } 1183 + 1184 + let new_value = match infer_data_from_text(&value_text) { 1185 + Ok(data) => data, 1186 + Err(e) => { 1187 + error.set(Some(e)); 1188 + return; 1189 + } 1190 + }; 1191 + 1192 + let mut new_data = root.read().clone(); 1193 + if let Some(Data::Object(obj)) = new_data.get_at_path_mut(path_for_button.as_str()) { 1194 + obj.0.insert(name.into(), new_value); 1195 + } 1196 + root.set(new_data); 1197 + 1198 + // Reset form 1199 + field_name.set(String::new()); 1200 + field_value.set(String::new()); 1201 + show_form.set(false); 1202 + error.set(None); 1203 + }, 1204 + "Add" 1205 + } 1206 + button { 1207 + class: "add-field-widget-edit", 1208 + onclick: move |_| { 1209 + show_form.set(false); 1210 + field_name.set(String::new()); 1211 + field_value.set(String::new()); 1212 + error.set(None); 1213 + }, 1214 + "Cancel" 1215 + } 1216 + if let Some(err) = error() { 1217 + div { class: "field-error", "❌ {err}" } 1218 + } 1219 + } 1220 + } 1221 + } 1222 + } 1223 + } 1224 + 1225 + #[component] 1226 + pub fn EditableRecordContent( 1227 + record_value: Data<'static>, 1228 + uri: ReadSignal<AtUri<'static>>, 1229 + view_mode: Signal<ViewMode>, 1230 + edit_mode: Signal<bool>, 1231 + record_resource: Resource<Result<GetRecordOutput<'static>, AgentError>>, 1232 + schema: ReadSignal<Option<LexiconDoc<'static>>>, 1233 + ) -> Element { 1234 + let mut edit_data = use_signal(use_reactive!(|record_value| record_value.clone())); 1235 + let nsid = use_memo(move || edit_data().type_discriminator().map(|s| s.to_string())); 1236 + let navigator = use_navigator(); 1237 + let fetcher = use_context::<Fetcher>(); 1238 + 1239 + // Validate edit_data whenever it changes and provide via context 1240 + let mut validation_result = use_signal(|| None); 1241 + use_effect(move || { 1242 + let _ = schema(); // Track schema changes 1243 + if let Some(nsid_str) = nsid() { 1244 + let data = edit_data(); 1245 + let validator = jacquard_lexicon::validation::SchemaValidator::global(); 1246 + let result = validator.validate_by_nsid(&nsid_str, &data); 1247 + validation_result.set(Some(result)); 1248 + } 1249 + }); 1250 + use_context_provider(|| validation_result); 1251 + 1252 + let update_fetcher = fetcher.clone(); 1253 + let create_fetcher = fetcher.clone(); 1254 + let replace_fetcher = fetcher.clone(); 1255 + let delete_fetcher = fetcher.clone(); 1256 + 1257 + rsx! { 1258 + div { 1259 + class: "tab-bar", 1260 + button { 1261 + class: if view_mode() == ViewMode::Pretty { "tab-button active" } else { "tab-button" }, 1262 + onclick: move |_| view_mode.set(ViewMode::Pretty), 1263 + "View" 1264 + } 1265 + button { 1266 + class: if view_mode() == ViewMode::Json { "tab-button active" } else { "tab-button" }, 1267 + onclick: move |_| view_mode.set(ViewMode::Json), 1268 + "JSON" 1269 + } 1270 + button { 1271 + class: if view_mode() == ViewMode::Schema { "tab-button active" } else { "tab-button" }, 1272 + onclick: move |_| view_mode.set(ViewMode::Schema), 1273 + "Schema" 1274 + } 1275 + ActionButtons { 1276 + on_update: move |_| { 1277 + let fetcher = update_fetcher.clone(); 1278 + let uri = uri(); 1279 + let data = edit_data(); 1280 + spawn(async move { 1281 + if let Some((did, _)) = fetcher.session_info().await { 1282 + if let (Some(collection_str), Some(rkey)) = (uri.collection(), uri.rkey()) { 1283 + let collection = Nsid::new(collection_str.as_str()).ok(); 1284 + if let Some(collection) = collection { 1285 + let request = PutRecord::new() 1286 + .repo(AtIdentifier::Did(did)) 1287 + .collection(collection) 1288 + .rkey(rkey.clone()) 1289 + .record(data.clone()) 1290 + .build(); 1291 + 1292 + match fetcher.send(request).await { 1293 + Ok(output) => { 1294 + if output.status() == StatusCode::OK.as_u16() { 1295 + tracing::info!("Record updated successfully"); 1296 + edit_data.set(data.clone()); 1297 + edit_mode.set(false); 1298 + } else { 1299 + tracing::error!("Unexpected status code: {:?}", output.status()); 1300 + } 1301 + } 1302 + Err(e) => { 1303 + tracing::error!("Failed to update record: {:?}", e); 1304 + } 1305 + } 1306 + } 1307 + } 1308 + } 1309 + }); 1310 + }, 1311 + on_save_new: move |_| { 1312 + let fetcher = create_fetcher.clone(); 1313 + let data = edit_data(); 1314 + let nav = navigator.clone(); 1315 + spawn(async move { 1316 + if let Some((did, _)) = fetcher.session_info().await { 1317 + if let Some(collection_str) = data.type_discriminator() { 1318 + let collection = Nsid::new(collection_str).ok(); 1319 + if let Some(collection) = collection { 1320 + let request = CreateRecord::new() 1321 + .repo(AtIdentifier::Did(did)) 1322 + .collection(collection) 1323 + .record(data.clone()) 1324 + .build(); 1325 + 1326 + match fetcher.send(request).await { 1327 + Ok(response) => { 1328 + if let Ok(output) = response.into_output() { 1329 + tracing::info!("Record created: {}", output.uri); 1330 + let link = format!("{}/record/{}", crate::env::WEAVER_APP_DOMAIN, output.uri); 1331 + nav.push(link); 1332 + } 1333 + } 1334 + Err(e) => { 1335 + tracing::error!("Failed to create record: {:?}", e); 1336 + } 1337 + } 1338 + } 1339 + } 1340 + } 1341 + }); 1342 + }, 1343 + on_replace: move |_| { 1344 + let fetcher = replace_fetcher.clone(); 1345 + let uri = uri(); 1346 + let data = edit_data(); 1347 + let nav = navigator.clone(); 1348 + spawn(async move { 1349 + if let Some((did, _)) = fetcher.session_info().await { 1350 + if let Some(new_collection_str) = data.type_discriminator() { 1351 + let new_collection = Nsid::new(new_collection_str).ok(); 1352 + if let Some(new_collection) = new_collection { 1353 + // Create new record 1354 + let create_req = CreateRecord::new() 1355 + .repo(AtIdentifier::Did(did.clone())) 1356 + .collection(new_collection) 1357 + .record(data.clone()) 1358 + .build(); 1359 + 1360 + match fetcher.send(create_req).await { 1361 + Ok(response) => { 1362 + if let Ok(create_output) = response.into_output() { 1363 + // Delete old record 1364 + if let (Some(old_collection_str), Some(old_rkey)) = (uri.collection(), uri.rkey()) { 1365 + let old_collection = Nsid::new(old_collection_str.as_str()).ok(); 1366 + if let Some(old_collection) = old_collection { 1367 + let delete_req = DeleteRecord::new() 1368 + .repo(AtIdentifier::Did(did)) 1369 + .collection(old_collection) 1370 + .rkey(old_rkey.clone()) 1371 + .build(); 1372 + 1373 + if let Err(e) = fetcher.send(delete_req).await { 1374 + tracing::warn!("Created new record but failed to delete old: {:?}", e); 1375 + } 1376 + } 1377 + } 1378 + 1379 + tracing::info!("Record replaced: {}", create_output.uri); 1380 + let link = format!("{}/record/{}", crate::env::WEAVER_APP_DOMAIN, create_output.uri); 1381 + nav.push(link); 1382 + } 1383 + } 1384 + Err(e) => { 1385 + tracing::error!("Failed to replace record: {:?}", e); 1386 + } 1387 + } 1388 + } 1389 + } 1390 + } 1391 + }); 1392 + }, 1393 + on_delete: move |_| { 1394 + let fetcher = delete_fetcher.clone(); 1395 + let uri = uri(); 1396 + let nav = navigator.clone(); 1397 + spawn(async move { 1398 + if let Some((did, _)) = fetcher.session_info().await { 1399 + if let (Some(collection_str), Some(rkey)) = (uri.collection(), uri.rkey()) { 1400 + let collection = Nsid::new(collection_str.as_str()).ok(); 1401 + if let Some(collection) = collection { 1402 + let request = DeleteRecord::new() 1403 + .repo(AtIdentifier::Did(did)) 1404 + .collection(collection) 1405 + .rkey(rkey.clone()) 1406 + .build(); 1407 + 1408 + match fetcher.send(request).await { 1409 + Ok(_) => { 1410 + tracing::info!("Record deleted"); 1411 + nav.push(Route::Home {}); 1412 + } 1413 + Err(e) => { 1414 + tracing::error!("Failed to delete record: {:?}", e); 1415 + } 1416 + } 1417 + } 1418 + } 1419 + } 1420 + }); 1421 + }, 1422 + on_cancel: move |_| { 1423 + edit_data.set(record_value.clone()); 1424 + edit_mode.set(false); 1425 + }, 1426 + } 1427 + } 1428 + div { 1429 + class: "tab-content", 1430 + match view_mode() { 1431 + ViewMode::Pretty => rsx! { 1432 + div { class: "pretty-record", 1433 + EditableDataView { 1434 + root: edit_data, 1435 + path: String::new(), 1436 + did: uri().authority().to_string(), 1437 + } 1438 + } 1439 + }, 1440 + ViewMode::Json => rsx! { 1441 + JsonEditor { data: edit_data, nsid, schema } 1442 + }, 1443 + ViewMode::Schema => rsx! { 1444 + SchemaView { schema } 1445 + }, 1446 + } 1447 + } 1448 + } 1449 + } 1450 + 1451 + #[component] 1452 + pub fn JsonEditor( 1453 + data: Signal<Data<'static>>, 1454 + nsid: ReadSignal<Option<String>>, 1455 + schema: ReadSignal<Option<LexiconDoc<'static>>>, 1456 + ) -> Element { 1457 + let mut json_text = 1458 + use_signal(|| serde_json::to_string_pretty(&*data.read()).unwrap_or_default()); 1459 + 1460 + let height = use_memo(move || { 1461 + let line_count = json_text().lines().count(); 1462 + let min_lines = 10; 1463 + let lines = line_count.max(min_lines); 1464 + // line-height is 1.5, font-size is 0.9rem (approx 14.4px), so each line is ~21.6px 1465 + // Add padding (1rem top + 1rem bottom = 2rem = 32px) 1466 + format!("{}px", lines * 22 + 32) 1467 + }); 1468 + 1469 + let validation = use_resource(move || { 1470 + let text = json_text(); 1471 + let nsid_val = nsid(); 1472 + let _ = schema(); // Track schema changes 1473 + 1474 + async move { 1475 + // Only validate if we have an NSID 1476 + let nsid_str = nsid_val?; 1477 + 1478 + // Parse JSON to Data 1479 + let parsed = match serde_json::from_str::<Data>(&text) { 1480 + Ok(val) => val.into_static(), 1481 + Err(e) => { 1482 + return Some((None, Some(e.to_string()))); 1483 + } 1484 + }; 1485 + 1486 + // Use global validator (schema already registered) 1487 + let validator = jacquard_lexicon::validation::SchemaValidator::global(); 1488 + let result = validator.validate_by_nsid(&nsid_str, &parsed); 1489 + 1490 + Some((Some(result), None)) 1491 + } 1492 + }); 1493 + 1494 + rsx! { 1495 + div { class: "json-editor", 1496 + textarea { 1497 + class: "json-textarea", 1498 + style: "height: {height};", 1499 + value: "{json_text}", 1500 + oninput: move |evt| { 1501 + json_text.set(evt.value()); 1502 + // Update data signal on successful parse 1503 + if let Ok(parsed) = serde_json::from_str::<Data>(&evt.value()) { 1504 + data.set(parsed.into_static()); 1505 + } 1506 + }, 1507 + } 1508 + 1509 + ValidationPanel { 1510 + validation: validation, 1511 + } 1512 + } 1513 + } 1514 + } 1515 + 1516 + #[component] 1517 + pub fn ActionButtons( 1518 + on_update: EventHandler<()>, 1519 + on_save_new: EventHandler<()>, 1520 + on_replace: EventHandler<()>, 1521 + on_delete: EventHandler<()>, 1522 + on_cancel: EventHandler<()>, 1523 + ) -> Element { 1524 + let mut show_save_dropdown = use_signal(|| false); 1525 + let mut show_replace_warning = use_signal(|| false); 1526 + let mut show_delete_confirm = use_signal(|| false); 1527 + 1528 + rsx! { 1529 + div { class: "action-buttons-group", 1530 + button { 1531 + class: "tab-button action-button", 1532 + onclick: move |_| on_update.call(()), 1533 + "Update" 1534 + } 1535 + 1536 + div { class: "dropdown-wrapper", 1537 + button { 1538 + class: "tab-button action-button", 1539 + onclick: move |_| show_save_dropdown.toggle(), 1540 + "Save as New ▼" 1541 + } 1542 + if show_save_dropdown() { 1543 + div { class: "dropdown-menu", 1544 + button { 1545 + onclick: move |_| { 1546 + show_save_dropdown.set(false); 1547 + on_save_new.call(()); 1548 + }, 1549 + "Save as New" 1550 + } 1551 + button { 1552 + onclick: move |_| { 1553 + show_save_dropdown.set(false); 1554 + show_replace_warning.set(true); 1555 + }, 1556 + "Replace" 1557 + } 1558 + } 1559 + } 1560 + } 1561 + 1562 + if show_replace_warning() { 1563 + div { class: "inline-warning", 1564 + "⚠️ This will delete the current record and create a new one with a different rkey. " 1565 + button { 1566 + onclick: move |_| { 1567 + show_replace_warning.set(false); 1568 + on_replace.call(()); 1569 + }, 1570 + "Yes" 1571 + } 1572 + button { 1573 + onclick: move |_| show_replace_warning.set(false), 1574 + "No" 1575 + } 1576 + } 1577 + } 1578 + 1579 + button { 1580 + class: "tab-button action-button action-button-danger", 1581 + onclick: move |_| show_delete_confirm.set(true), 1582 + "Delete" 1583 + } 1584 + 1585 + DialogRoot { 1586 + open: Some(show_delete_confirm()), 1587 + on_open_change: move |open: bool| { 1588 + show_delete_confirm.set(open); 1589 + }, 1590 + DialogContent { 1591 + DialogTitle { "Delete Record?" } 1592 + DialogDescription { 1593 + "This action cannot be undone." 1594 + } 1595 + div { class: "dialog-actions", 1596 + button { 1597 + onclick: move |_| { 1598 + show_delete_confirm.set(false); 1599 + on_delete.call(()); 1600 + }, 1601 + "Delete" 1602 + } 1603 + button { 1604 + onclick: move |_| show_delete_confirm.set(false), 1605 + "Cancel" 1606 + } 1607 + } 1608 + } 1609 + } 1610 + 1611 + button { 1612 + class: "tab-button action-button", 1613 + onclick: move |_| on_cancel.call(()), 1614 + "Cancel" 1615 + } 1616 + } 1617 + } 1618 + } 1619 + 1620 + #[component] 1621 + pub fn ValidationPanel( 1622 + validation: Resource<Option<(Option<ValidationResult>, Option<String>)>>, 1623 + ) -> Element { 1624 + rsx! { 1625 + div { class: "validation-panel", 1626 + if let Some(Some((result_opt, parse_error_opt))) = validation.read().as_ref() { 1627 + if let Some(parse_err) = parse_error_opt { 1628 + div { class: "parse-error", 1629 + "❌ Invalid JSON: {parse_err}" 1630 + } 1631 + } 1632 + 1633 + if let Some(result) = result_opt { 1634 + // Structural validity 1635 + if result.is_structurally_valid() { 1636 + div { class: "validation-success", "✓ Structurally valid" } 1637 + } else { 1638 + div { class: "parse-error", "❌ Structurally invalid" } 1639 + } 1640 + 1641 + // Overall validity 1642 + if result.is_valid() { 1643 + div { class: "validation-success", "✓ Fully valid" } 1644 + } else { 1645 + div { class: "validation-warning", "⚠ Has errors" } 1646 + } 1647 + 1648 + // Show errors if any 1649 + if !result.is_valid() { 1650 + div { class: "validation-errors", 1651 + h4 { "Validation Errors:" } 1652 + for error in result.all_errors() { 1653 + div { class: "error", "{error}" } 1654 + } 1655 + } 1656 + } 1657 + } 1658 + } else { 1659 + div { "Validating..." } 1660 + } 1661 + } 1662 + } 1663 + }
+610
crates/weaver-app/src/components/record_view.rs
··· 1 + use crate::components::accordion::{Accordion, AccordionContent, AccordionItem, AccordionTrigger}; 2 + use crate::record_utils::{get_errors_at_exact_path, get_expected_string_format, get_hex_rep}; 3 + use dioxus::prelude::*; 4 + use humansize::format_size; 5 + use jacquard::to_data; 6 + use jacquard::types::string::AtprotoStr; 7 + use jacquard::{ 8 + common::{Data, IntoStatic}, 9 + types::{aturi::AtUri, cid::Cid}, 10 + }; 11 + use jacquard_lexicon::lexicon::LexiconDoc; 12 + use jacquard_lexicon::validation::ValidationResult; 13 + use weaver_renderer::{code_pretty::highlight_code, css::generate_default_css}; 14 + 15 + #[derive(Clone, Copy, PartialEq)] 16 + pub enum ViewMode { 17 + Pretty, 18 + Json, 19 + Schema, 20 + } 21 + 22 + /// Layout component for record view - handles header, metadata, and wraps children 23 + #[component] 24 + pub fn RecordViewLayout( 25 + uri: AtUri<'static>, 26 + cid: Option<Cid<'static>>, 27 + schema: ReadSignal<Option<LexiconDoc<'static>>>, 28 + record_value: Data<'static>, 29 + children: Element, 30 + ) -> Element { 31 + // Validate the record if schema is available 32 + let validation_status = use_memo(move || { 33 + let _schema_doc = schema()?; 34 + let nsid_str = record_value.type_discriminator()?; 35 + 36 + let validator = jacquard_lexicon::validation::SchemaValidator::global(); 37 + let result = validator.validate_by_nsid(nsid_str, &record_value); 38 + 39 + Some(result.is_valid()) 40 + }); 41 + 42 + rsx! { 43 + div { 44 + class: "record-metadata", 45 + div { class: "metadata-row", 46 + span { class: "metadata-label", "URI" } 47 + span { class: "metadata-value", 48 + HighlightedUri { uri: uri.clone() } 49 + } 50 + } 51 + if let Some(cid) = cid { 52 + div { class: "metadata-row", 53 + span { class: "metadata-label", "CID" } 54 + code { class: "metadata-value", "{cid}" } 55 + } 56 + } 57 + if let Some(is_valid) = validation_status() { 58 + div { class: "metadata-row", 59 + span { class: "metadata-label", "Schema" } 60 + span { 61 + class: if is_valid { "metadata-value schema-valid" } else { "metadata-value schema-invalid" }, 62 + if is_valid { "Valid" } else { "Invalid" } 63 + } 64 + } 65 + } 66 + } 67 + 68 + {children} 69 + 70 + } 71 + } 72 + 73 + #[component] 74 + pub fn SchemaView(schema: ReadSignal<Option<LexiconDoc<'static>>>) -> Element { 75 + if let Some(schema_doc) = schema() { 76 + // Convert LexiconDoc to Data for display 77 + let schema_data = to_data(&schema_doc).ok().map(|d| d.into_static()); 78 + 79 + if let Some(data) = schema_data { 80 + rsx! { 81 + div { 82 + class: "pretty-record", 83 + DataView { data: data.clone(), root_data: data, path: String::new(), did: String::new() } 84 + } 85 + } 86 + } else { 87 + rsx! { 88 + div { class: "schema-error", "Failed to convert schema to displayable format" } 89 + } 90 + } 91 + } else { 92 + rsx! { 93 + div { class: "schema-loading", "Loading schema..." } 94 + } 95 + } 96 + } 97 + 98 + #[component] 99 + pub fn DataView( 100 + data: Data<'static>, 101 + root_data: ReadSignal<Data<'static>>, 102 + path: String, 103 + did: String, 104 + ) -> Element { 105 + // Try to get validation result from context and get errors exactly at this path 106 + let validation_result = try_use_context::<Signal<Option<ValidationResult>>>(); 107 + 108 + let errors = if let Some(vr_signal) = validation_result { 109 + get_errors_at_exact_path(&*vr_signal.read(), &path) 110 + } else { 111 + Vec::new() 112 + }; 113 + 114 + let has_errors = !errors.is_empty(); 115 + 116 + match &data { 117 + Data::Null => rsx! { 118 + div { class: if has_errors { "record-field field-error" } else { "record-field" }, 119 + PathLabel { path: path.clone() } 120 + span { class: "field-value muted", "null" } 121 + if has_errors { 122 + for error in &errors { 123 + div { class: "field-error-message", "{error}" } 124 + } 125 + } 126 + } 127 + }, 128 + Data::Boolean(b) => rsx! { 129 + div { class: if has_errors { "record-field field-error" } else { "record-field" }, 130 + PathLabel { path: path.clone() } 131 + span { class: "field-value", "{b}" } 132 + if has_errors { 133 + for error in &errors { 134 + div { class: "field-error-message", "{error}" } 135 + } 136 + } 137 + } 138 + }, 139 + Data::Integer(i) => rsx! { 140 + div { class: if has_errors { "record-field field-error" } else { "record-field" }, 141 + PathLabel { path: path.clone() } 142 + span { class: "field-value", "{i}" } 143 + if has_errors { 144 + for error in &errors { 145 + div { class: "field-error-message", "{error}" } 146 + } 147 + } 148 + } 149 + }, 150 + Data::String(s) => { 151 + use jacquard::types::string::AtprotoStr; 152 + use jacquard_lexicon::lexicon::LexStringFormat; 153 + 154 + // Get expected format from schema 155 + let expected_format = get_expected_string_format(&*root_data.read(), &path); 156 + 157 + // Get actual type from data 158 + let actual_type_label = match s { 159 + AtprotoStr::Datetime(_) => "datetime", 160 + AtprotoStr::Language(_) => "language", 161 + AtprotoStr::Tid(_) => "tid", 162 + AtprotoStr::Nsid(_) => "nsid", 163 + AtprotoStr::Did(_) => "did", 164 + AtprotoStr::Handle(_) => "handle", 165 + AtprotoStr::AtIdentifier(_) => "at-identifier", 166 + AtprotoStr::AtUri(_) => "at-uri", 167 + AtprotoStr::Uri(_) => "uri", 168 + AtprotoStr::Cid(_) => "cid", 169 + AtprotoStr::RecordKey(_) => "record-key", 170 + AtprotoStr::String(_) => "string", 171 + }; 172 + 173 + // Prefer schema format if available, otherwise use actual type 174 + let type_label = if let Some(fmt) = expected_format { 175 + match fmt { 176 + LexStringFormat::Datetime => "datetime", 177 + LexStringFormat::Uri => "uri", 178 + LexStringFormat::AtUri => "at-uri", 179 + LexStringFormat::Did => "did", 180 + LexStringFormat::Handle => "handle", 181 + LexStringFormat::AtIdentifier => "at-identifier", 182 + LexStringFormat::Nsid => "nsid", 183 + LexStringFormat::Cid => "cid", 184 + LexStringFormat::Language => "language", 185 + LexStringFormat::Tid => "tid", 186 + LexStringFormat::RecordKey => "record-key", 187 + } 188 + } else { 189 + actual_type_label 190 + }; 191 + 192 + rsx! { 193 + div { class: if has_errors { "record-field field-error" } else { "record-field" }, 194 + PathLabel { path: path.clone() } 195 + span { class: "field-value", 196 + 197 + HighlightedString { string_type: s.clone() } 198 + if type_label != "string" { 199 + span { class: "string-type-tag", " [{type_label}]" } 200 + } 201 + } 202 + if has_errors { 203 + for error in &errors { 204 + div { class: "field-error-message", "{error}" } 205 + } 206 + } 207 + } 208 + } 209 + } 210 + Data::Bytes(b) => { 211 + let hex_string = get_hex_rep(&mut b.to_vec()); 212 + let byte_size = if b.len() > 128 { 213 + format_size(b.len(), humansize::BINARY) 214 + } else { 215 + format!("{} bytes", b.len()) 216 + }; 217 + rsx! { 218 + div { class: if has_errors { "record-field field-error" } else { "record-field" }, 219 + PathLabel { path: path.clone() } 220 + pre { class: "field-value bytes", "{hex_string} [{byte_size}]" } 221 + if has_errors { 222 + for error in &errors { 223 + div { class: "field-error-message", "{error}" } 224 + } 225 + } 226 + } 227 + } 228 + } 229 + Data::CidLink(cid) => rsx! { 230 + div { class: if has_errors { "record-field field-error" } else { "record-field" }, 231 + span { class: "field-label", "{path}" } 232 + span { class: "field-value", "{cid}" } 233 + if has_errors { 234 + for error in &errors { 235 + div { class: "field-error-message", "{error}" } 236 + } 237 + } 238 + } 239 + }, 240 + Data::Array(arr) => { 241 + let label = path.split('.').last().unwrap_or(&path); 242 + rsx! { 243 + div { class: "record-section", 244 + Accordion { 245 + id: "array-{path}", 246 + collapsible: true, 247 + AccordionItem { 248 + default_open: true, 249 + index: 0, 250 + AccordionTrigger { 251 + div { class: "section-label", "{label}" span { class: "array-len", "[{arr.len()}]" } } 252 + } 253 + AccordionContent { 254 + if has_errors { 255 + for error in &errors { 256 + div { class: "field-error-message", "{error}" } 257 + } 258 + } 259 + div { class: "section-content", 260 + for (idx, item) in arr.iter().enumerate() { 261 + { 262 + let item_path = format!("{}[{}]", label, idx); 263 + let is_object = matches!(item, Data::Object(_)); 264 + 265 + if is_object { 266 + rsx! { 267 + div { 268 + class: "array-item", 269 + div { class: "record-section", 270 + div { class: "section-label", "{item_path}" } 271 + div { class: "section-content", 272 + DataView { 273 + data: item.clone(), 274 + root_data, 275 + path: item_path.clone(), 276 + did: did.clone() 277 + } 278 + } 279 + } 280 + } 281 + } 282 + } else { 283 + rsx! { 284 + div { 285 + class: "array-item", 286 + DataView { 287 + data: item.clone(), 288 + root_data, 289 + path: item_path, 290 + did: did.clone() 291 + } 292 + } 293 + } 294 + } 295 + } 296 + } 297 + } 298 + } 299 + } 300 + } 301 + } 302 + } 303 + } 304 + Data::Object(obj) => { 305 + let is_root = path.is_empty(); 306 + let is_array_item = path.split('.').last().unwrap_or(&path).contains('['); 307 + 308 + if is_root || is_array_item { 309 + // Root object or array item: just render children (array items already wrapped) 310 + rsx! { 311 + div { class: if !is_root { "record-section" } else {""}, 312 + if has_errors { 313 + for error in &errors { 314 + div { class: "field-error-message", "{error}" } 315 + } 316 + } 317 + for (key, value) in obj.iter() { 318 + { 319 + let new_path = if is_root { 320 + key.to_string() 321 + } else { 322 + format!("{}.{}", path, key) 323 + }; 324 + let did_clone = did.clone(); 325 + rsx! { 326 + DataView { data: value.clone(), root_data, path: new_path, did: did_clone } 327 + } 328 + } 329 + } 330 + } 331 + } 332 + } else { 333 + // Nested object (not array item): wrap in section 334 + let label = path.split('.').last().unwrap_or(&path); 335 + rsx! { 336 + div { class: "record-section", 337 + Accordion { 338 + id: "object-{path}", 339 + collapsible: true, 340 + AccordionItem { 341 + default_open: true, 342 + index: 0, 343 + AccordionTrigger { 344 + div { class: "section-label", "{label}" } 345 + } 346 + AccordionContent { 347 + if has_errors { 348 + for error in &errors { 349 + div { class: "field-error-message", "{error}" } 350 + } 351 + } 352 + div { class: "section-content", 353 + for (key, value) in obj.iter() { 354 + { 355 + let new_path = format!("{}.{}", path, key); 356 + let did_clone = did.clone(); 357 + rsx! { 358 + DataView { data: value.clone(), root_data, path: new_path, did: did_clone } 359 + } 360 + } 361 + } 362 + } 363 + } 364 + } 365 + } 366 + } 367 + } 368 + } 369 + } 370 + Data::Blob(blob) => { 371 + let is_image = blob.mime_type.starts_with("image/"); 372 + let format = blob.mime_type.strip_prefix("image/").unwrap_or("jpeg"); 373 + let image_url = format!( 374 + "https://cdn.bsky.app/img/feed_fullsize/plain/{}/{}@{}", 375 + did, 376 + blob.cid(), 377 + format 378 + ); 379 + 380 + let blob_size = format_size(blob.size, humansize::BINARY); 381 + rsx! { 382 + div { class: "record-field", 383 + span { class: "field-label", "{path}" } 384 + span { class: "field-value mime", "[mimeType: {blob.mime_type}, size: {blob_size}]" } 385 + if is_image { 386 + img { 387 + src: "{image_url}", 388 + alt: "Blob image", 389 + class: "blob-image", 390 + } 391 + } 392 + } 393 + } 394 + } 395 + } 396 + } 397 + 398 + #[component] 399 + pub fn HighlightedUri(uri: AtUri<'static>) -> Element { 400 + let s = uri.as_str(); 401 + let link = format!("{}/record/{}", crate::env::WEAVER_APP_DOMAIN, s); 402 + 403 + if let Some(rest) = s.strip_prefix("at://") { 404 + let parts: Vec<&str> = rest.splitn(3, '/').collect(); 405 + return rsx! { 406 + a { 407 + href: link, 408 + class: "uri-link", 409 + span { class: "string-at-uri", 410 + span { class: "aturi-scheme", "at://" } 411 + span { class: "aturi-authority", "{uri.authority()}" } 412 + 413 + if parts.len() > 1 { 414 + span { class: "aturi-slash", "/" } 415 + if let Some(collection) = uri.collection() { 416 + span { class: "aturi-collection", "{collection.as_ref()}" } 417 + } 418 + } 419 + if parts.len() > 2 { 420 + span { class: "aturi-slash", "/" } 421 + if let Some(rkey) = uri.rkey() { 422 + span { class: "aturi-rkey", "{rkey.as_ref()}" } 423 + } 424 + } 425 + } 426 + } 427 + }; 428 + } 429 + 430 + rsx! { a { class: "string-at-uri", href: s } } 431 + } 432 + 433 + #[component] 434 + pub fn HighlightedString(string_type: AtprotoStr<'static>) -> Element { 435 + use jacquard::types::string::AtprotoStr; 436 + 437 + match &string_type { 438 + AtprotoStr::Nsid(nsid) => { 439 + let parts: Vec<&str> = nsid.as_str().split('.').collect(); 440 + rsx! { 441 + span { class: "string-nsid", 442 + for (i, part) in parts.iter().enumerate() { 443 + span { class: "nsid-segment nsid-segment-{i % 3}", "{part}" } 444 + if i < parts.len() - 1 { 445 + span { class: "nsid-dot", "." } 446 + } 447 + } 448 + } 449 + } 450 + } 451 + AtprotoStr::Did(did) => { 452 + let s = did.as_str(); 453 + if let Some(rest) = s.strip_prefix("did:") { 454 + if let Some((method, identifier)) = rest.split_once(':') { 455 + return rsx! { 456 + span { class: "string-did", 457 + span { class: "did-scheme", "did:" } 458 + span { class: "did-method", "{method}" } 459 + span { class: "did-separator", ":" } 460 + span { class: "did-identifier", "{identifier}" } 461 + } 462 + }; 463 + } 464 + } 465 + rsx! { span { class: "string-did", "{s}" } } 466 + } 467 + AtprotoStr::Handle(handle) => { 468 + let parts: Vec<&str> = handle.as_str().split('.').collect(); 469 + rsx! { 470 + span { class: "string-handle", 471 + for (i, part) in parts.iter().enumerate() { 472 + span { class: "handle-segment handle-segment-{i % 2}", "{part}" } 473 + if i < parts.len() - 1 { 474 + span { class: "handle-dot", "." } 475 + } 476 + } 477 + } 478 + } 479 + } 480 + AtprotoStr::AtUri(uri) => { 481 + rsx! { 482 + HighlightedUri { uri: uri.clone().into_static() } 483 + } 484 + } 485 + AtprotoStr::Uri(uri) => { 486 + let s = uri.as_str(); 487 + if let Ok(at_uri) = AtUri::new(s) { 488 + return rsx! { 489 + HighlightedUri { uri: at_uri.into_static() } 490 + }; 491 + } 492 + 493 + // Try to parse scheme 494 + if let Some((scheme, rest)) = s.split_once("://") { 495 + // Split authority and path 496 + let (authority, path) = if let Some(idx) = rest.find('/') { 497 + (&rest[..idx], &rest[idx..]) 498 + } else { 499 + (rest, "") 500 + }; 501 + 502 + return rsx! { 503 + a { 504 + href: "{s}", 505 + target: "_blank", 506 + rel: "noopener noreferrer", 507 + class: "uri-link", 508 + span { class: "string-uri", 509 + span { class: "uri-scheme", "{scheme}" } 510 + span { class: "uri-separator", "://" } 511 + span { class: "uri-authority", "{authority}" } 512 + if !path.is_empty() { 513 + span { class: "uri-path", "{path}" } 514 + } 515 + } 516 + } 517 + }; 518 + } 519 + 520 + rsx! { span { class: "string-uri", "{s}" } } 521 + } 522 + _ => { 523 + let value = string_type.as_str(); 524 + rsx! { "{value}" } 525 + } 526 + } 527 + } 528 + 529 + #[derive(Props, Clone, PartialEq)] 530 + pub struct CodeViewProps { 531 + #[props(default)] 532 + id: Signal<String>, 533 + #[props(default)] 534 + class: Signal<String>, 535 + code: ReadSignal<String>, 536 + lang: Option<String>, 537 + } 538 + 539 + #[component] 540 + pub fn PrettyRecordView( 541 + record: Data<'static>, 542 + uri: AtUri<'static>, 543 + schema: ReadSignal<Option<LexiconDoc<'static>>>, 544 + ) -> Element { 545 + let did = uri.authority().to_string(); 546 + let root_data = use_signal(|| record.clone()); 547 + 548 + // Validate the record and provide via context - only after schema is loaded 549 + let mut validation_result = use_signal(|| None); 550 + use_effect(move || { 551 + // Wait for schema to be loaded 552 + if schema().is_some() { 553 + if let Some(nsid_str) = root_data.read().type_discriminator() { 554 + let validator = jacquard_lexicon::validation::SchemaValidator::global(); 555 + let result = validator.validate_by_nsid(nsid_str, &*root_data.read()); 556 + validation_result.set(Some(result)); 557 + } 558 + } 559 + }); 560 + use_context_provider(|| validation_result); 561 + 562 + rsx! { 563 + div { 564 + class: "pretty-record", 565 + DataView { data: record, root_data, path: String::new(), did } 566 + } 567 + } 568 + } 569 + 570 + #[component] 571 + pub fn CodeView(props: CodeViewProps) -> Element { 572 + let code = &*props.code.read(); 573 + 574 + let mut html_buf = String::new(); 575 + highlight_code(props.lang.as_deref(), code, &mut html_buf).unwrap(); 576 + 577 + rsx! { 578 + document::Style { {generate_default_css().unwrap()}} 579 + div { 580 + id: "{&*props.id.read()}", 581 + class: "{&*props.class.read()}", 582 + dangerous_inner_html: "{html_buf}" 583 + } 584 + } 585 + } 586 + 587 + #[component] 588 + pub fn PathLabel(path: String) -> Element { 589 + if path.is_empty() { 590 + return rsx! {}; 591 + } 592 + 593 + // Find the last separator 594 + let last_sep = path.rfind(|c| c == '.'); 595 + 596 + if let Some(idx) = last_sep { 597 + let prefix = &path[..idx + 1]; 598 + let final_segment = &path[idx + 1..]; 599 + rsx! { 600 + span { class: "field-label", 601 + span { class: "path-prefix", "{prefix}" } 602 + span { class: "path-final", "{final_segment}" } 603 + } 604 + } 605 + } else { 606 + rsx! { 607 + span { class: "field-label","{path}" } 608 + } 609 + } 610 + }
+171 -146
crates/weaver-app/src/data.rs
··· 3 3 //! In fullstack-server mode, hooks use `use_server_future` with inline async closures. 4 4 //! In client-only mode, hooks use `use_resource` with context-provided fetchers. 5 5 6 + use crate::auth::AuthState; 6 7 #[cfg(feature = "server")] 7 8 use crate::blobcache::BlobCache; 8 9 use dioxus::prelude::*; 9 10 #[cfg(feature = "fullstack-server")] 10 11 #[allow(unused_imports)] 11 12 use dioxus::{CapturedError, fullstack::extract::Extension}; 12 - use jacquard::types::{did::Did, string::Handle}; 13 + use jacquard::{ 14 + IntoStatic, 15 + identity::resolver::IdentityError, 16 + types::{did::Did, string::Handle}, 17 + }; 13 18 #[allow(unused_imports)] 14 19 use jacquard::{ 15 20 prelude::IdentityResolver, 16 21 smol_str::SmolStr, 17 22 types::{cid::Cid, string::AtIdentifier}, 18 23 }; 24 + use std::cell::Ref; 19 25 #[allow(unused_imports)] 20 26 use std::sync::Arc; 27 + use weaver_api::com_atproto::repo::strong_ref::StrongRef; 28 + use weaver_api::sh_weaver::actor::ProfileDataView; 21 29 use weaver_api::sh_weaver::notebook::{BookEntryView, NotebookView, entry::Entry}; 22 30 // ============================================================================ 23 31 // Wrapper Hooks (feature-gated) 24 32 // ============================================================================ 25 33 26 34 /// Fetches entry data with SSR support in fullstack mode. 27 - /// Returns a MappedSignal over the server future resource. 28 35 #[cfg(feature = "fullstack-server")] 29 36 pub fn use_entry_data( 30 37 ident: AtIdentifier<'static>, 31 38 book_title: SmolStr, 32 39 title: SmolStr, 33 40 ) -> Result<Memo<Option<(BookEntryView<'static>, Entry<'static>)>>, RenderError> { 34 - let fetcher = use_context::<crate::fetch::CachedFetcher>(); 41 + let fetcher = use_context::<crate::fetch::Fetcher>(); 35 42 let fetcher = fetcher.clone(); 36 43 let ident = use_signal(|| ident); 37 44 let book_title = use_signal(|| book_title); ··· 88 95 }) 89 96 }) 90 97 } 91 - 92 98 /// Fetches entry data client-side only (no SSR). 93 99 #[cfg(not(feature = "fullstack-server"))] 94 100 pub fn use_entry_data( ··· 96 102 book_title: SmolStr, 97 103 title: SmolStr, 98 104 ) -> Result<Memo<Option<(BookEntryView<'static>, Entry<'static>)>>, RenderError> { 99 - let fetcher = use_context::<crate::fetch::CachedFetcher>(); 105 + let fetcher = use_context::<crate::fetch::Fetcher>(); 100 106 let fetcher = fetcher.clone(); 101 107 let ident = use_signal(|| ident); 102 108 let book_title = use_signal(|| book_title); 103 109 let entry_title = use_signal(|| title); 104 - let r = use_resource(move || { 110 + let res = use_resource(move || { 105 111 let fetcher = fetcher.clone(); 106 112 async move { 107 113 fetcher ··· 109 115 .await 110 116 .ok() 111 117 .flatten() 112 - .map(|arc| (arc.0.clone(), arc.1.clone())) 118 + .map(|arc| { 119 + ( 120 + serde_json::to_value(entry.0.clone()).unwrap(), 121 + serde_json::to_value(entry.1.clone()).unwrap(), 122 + ) 123 + }) 113 124 } 114 125 }); 115 - Ok(use_memo(move || { 116 - if let Some(Some((ev, e))) = &*r.read_unchecked() { 117 - Some((ev.clone(), e.clone())) 118 - } else { 119 - None 120 - } 121 - })) 126 + res.map(|r| { 127 + use_memo(move || { 128 + if let Some(Some((ev, e))) = &*r.read_unchecked() { 129 + use jacquard::from_json_value; 130 + 131 + let book_entry = from_json_value::<BookEntryView>(ev.clone()).unwrap(); 132 + let entry = from_json_value::<Entry>(e.clone()).unwrap(); 133 + 134 + Some((book_entry, entry)) 135 + } else { 136 + None 137 + } 138 + }) 139 + }) 140 + } 141 + 142 + pub fn get_handle(did: Did<'static>) -> AtIdentifier<'static> { 143 + let ident = AtIdentifier::Did(did); 144 + use_handle(ident.clone()) 145 + .read() 146 + .as_ref() 147 + .unwrap_or(&Ok(ident)) 148 + .as_ref() 149 + .unwrap() 150 + .clone() 122 151 } 123 152 124 153 pub fn use_handle( 125 154 ident: AtIdentifier<'static>, 126 - ) -> Result<Memo<AtIdentifier<'static>>, RenderError> { 127 - let fetcher = use_context::<crate::fetch::CachedFetcher>(); 155 + ) -> Resource<Result<AtIdentifier<'static>, IdentityError>> { 156 + let fetcher = use_context::<crate::fetch::Fetcher>(); 128 157 let fetcher = fetcher.clone(); 129 158 let ident = use_signal(|| ident); 130 - #[cfg(feature = "fullstack-server")] 131 - let h_str = { 132 - use_server_future(move || { 133 - let client = fetcher.get_client(); 134 - async move { 135 - use jacquard::smol_str::ToSmolStr; 136 - client 137 - .resolve_ident_owned(&ident()) 138 - .await 139 - .map(|doc| doc.handles().first().map(|h| h.to_smolstr())) 140 - .ok() 141 - .flatten() 142 - } 143 - }) 159 + 160 + use_resource(move || { 161 + let client = fetcher.get_client(); 162 + async move { 163 + client 164 + .resolve_ident_owned(&*ident.read()) 165 + .await 166 + .map(|doc| { 167 + doc.handles() 168 + .first() 169 + .map(|h| AtIdentifier::Handle(h.clone()).into_static()) 170 + }) 171 + .map(|h| h.ok_or(IdentityError::invalid_well_known()))? 172 + } 173 + }) 174 + } 175 + 176 + #[derive(Clone, PartialEq, Eq)] 177 + pub struct NotebookHandle(pub Arc<Option<AtIdentifier<'static>>>); 178 + 179 + impl NotebookHandle { 180 + pub fn as_ref(&self) -> Option<&AtIdentifier<'static>> { 181 + self.0.as_ref().as_ref() 182 + } 183 + } 184 + 185 + pub fn use_notebook_handle(ident: Signal<Option<AtIdentifier<'static>>>) -> NotebookHandle { 186 + let ident = if let Some(ident) = &*ident.read() { 187 + if let Some(Ok(handle)) = &*use_handle(ident.clone()).read() { 188 + Some(handle.clone()) 189 + } else { 190 + Some(ident.clone()) 191 + } 192 + } else { 193 + ident.read().clone() 144 194 }; 145 - #[cfg(not(feature = "fullstack-server"))] 146 - let h_str = { 147 - use_resource(move || { 148 - let client = fetcher.get_client(); 149 - async move { 150 - use jacquard::smol_str::ToSmolStr; 151 - client 152 - .resolve_ident_owned(&ident()) 153 - .await 154 - .map(|doc| doc.handles().first().map(|h| h.to_smolstr())) 155 - .ok() 156 - .flatten() 157 - } 158 - }) 159 - }; 160 - Ok(h_str.map(|h_str| { 161 - use_memo(move || { 162 - if let Some(Some(e)) = &*h_str.read_unchecked() { 163 - use jacquard::IntoStatic; 164 - 165 - AtIdentifier::Handle(Handle::raw(&e).into_static()) 166 - } else { 167 - ident() 168 - } 169 - }) 170 - })?) 195 + use_context_provider(|| NotebookHandle(Arc::new(ident))) 171 196 } 172 197 173 198 /// Hook to render markdown client-side only (no SSR). ··· 178 203 ) -> Result<Resource<Option<String>>, RenderError> { 179 204 let ident = use_signal(|| ident); 180 205 let content = use_signal(|| content); 181 - let fetcher = use_context::<crate::fetch::CachedFetcher>(); 206 + let fetcher = use_context::<crate::fetch::Fetcher>(); 182 207 Ok(use_server_future(move || { 183 208 let client = fetcher.get_client(); 184 209 async move { ··· 199 224 ) -> Result<Resource<Option<String>>, RenderError> { 200 225 let ident = use_signal(|| ident); 201 226 let content = use_signal(|| content); 202 - let fetcher = use_context::<crate::fetch::CachedFetcher>(); 227 + let fetcher = use_context::<crate::fetch::Fetcher>(); 203 228 Ok(use_resource(move || { 204 229 let client = fetcher.get_client(); 205 230 async move { ··· 221 246 }; 222 247 223 248 let ctx = ClientContext::<()>::new(content.clone(), did); 224 - let parser = markdown_weaver::Parser::new(&content.content); 249 + let parser = 250 + markdown_weaver::Parser::new_ext(&content.content, weaver_renderer::default_md_options()); 225 251 let iter = ContextIterator::default(parser); 226 252 let processor = NotebookProcessor::new(ctx, iter); 227 253 ··· 236 262 #[cfg(feature = "fullstack-server")] 237 263 pub fn use_profile_data( 238 264 ident: AtIdentifier<'static>, 239 - ) -> Result<Memo<Option<weaver_api::sh_weaver::actor::ProfileDataView<'static>>>, RenderError> { 240 - let fetcher = use_context::<crate::fetch::CachedFetcher>(); 265 + ) -> Result<Memo<Option<ProfileDataView<'static>>>, RenderError> { 266 + let fetcher = use_context::<crate::fetch::Fetcher>(); 241 267 let ident = use_signal(|| ident); 242 268 let res = use_server_future(move || { 243 269 let fetcher = fetcher.clone(); ··· 252 278 })?; 253 279 Ok(use_memo(move || { 254 280 if let Some(Some(value)) = &*res.read_unchecked() { 255 - jacquard::from_json_value::<weaver_api::sh_weaver::actor::ProfileDataView>( 256 - value.clone(), 257 - ) 258 - .ok() 281 + jacquard::from_json_value::<ProfileDataView>(value.clone()).ok() 259 282 } else { 260 283 None 261 284 } ··· 266 289 #[cfg(not(feature = "fullstack-server"))] 267 290 pub fn use_profile_data( 268 291 ident: AtIdentifier<'static>, 269 - ) -> Result<Memo<Option<weaver_api::sh_weaver::actor::ProfileDataView<'static>>>, RenderError> { 270 - let fetcher = use_context::<crate::fetch::CachedFetcher>(); 271 - let r = use_resource(use_reactive!(|ident| { 292 + ) -> Result<Memo<Option<ProfileDataView<'static>>>, RenderError> { 293 + let fetcher = use_context::<crate::fetch::Fetcher>(); 294 + let ident = use_signal(|| ident); 295 + let res = use_resource(move || { 272 296 let fetcher = fetcher.clone(); 273 297 async move { 274 298 fetcher 275 - .fetch_profile(&ident) 299 + .fetch_profile(&ident()) 276 300 .await 277 301 .ok() 278 - .map(|arc| (*arc).clone()) 302 + .map(|arc| serde_json::to_value(&*arc).ok()) 303 + .flatten() 279 304 } 280 - })); 305 + }); 281 306 Ok(use_memo(move || { 282 - r.read_unchecked().as_ref().and_then(|v| v.clone()) 307 + if let Some(Some(value)) = &*res.read_unchecked() { 308 + jacquard::from_json_value::<ProfileDataView>(value.clone()).ok() 309 + } else { 310 + None 311 + } 283 312 })) 284 313 } 285 314 ··· 287 316 #[cfg(feature = "fullstack-server")] 288 317 pub fn use_notebooks_for_did( 289 318 ident: AtIdentifier<'static>, 290 - ) -> Result< 291 - Memo< 292 - Option< 293 - Vec<( 294 - weaver_api::sh_weaver::notebook::NotebookView<'static>, 295 - Vec<weaver_api::com_atproto::repo::strong_ref::StrongRef<'static>>, 296 - )>, 297 - >, 298 - >, 299 - RenderError, 300 - > { 301 - let fetcher = use_context::<crate::fetch::CachedFetcher>(); 319 + ) -> Result<Memo<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>>, RenderError> { 320 + let fetcher = use_context::<crate::fetch::Fetcher>(); 302 321 let ident = use_signal(|| ident); 303 322 let res = use_server_future(move || { 304 323 let fetcher = fetcher.clone(); ··· 321 340 values 322 341 .iter() 323 342 .map(|v| { 324 - jacquard::from_json_value::<( 325 - weaver_api::sh_weaver::notebook::NotebookView, 326 - Vec<weaver_api::com_atproto::repo::strong_ref::StrongRef>, 327 - )>(v.clone()) 328 - .ok() 343 + jacquard::from_json_value::<(NotebookView, Vec<StrongRef>)>(v.clone()).ok() 329 344 }) 330 345 .collect::<Option<Vec<_>>>() 331 346 } else { ··· 338 353 #[cfg(not(feature = "fullstack-server"))] 339 354 pub fn use_notebooks_for_did( 340 355 ident: AtIdentifier<'static>, 341 - ) -> Result< 342 - Memo< 343 - Option< 344 - Vec<( 345 - weaver_api::sh_weaver::notebook::NotebookView<'static>, 346 - Vec<weaver_api::com_atproto::repo::strong_ref::StrongRef<'static>>, 347 - )>, 348 - >, 349 - >, 350 - RenderError, 351 - > { 352 - let fetcher = use_context::<crate::fetch::CachedFetcher>(); 353 - let r = use_resource(use_reactive!(|ident| { 356 + ) -> Result<Memo<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>>, RenderError> { 357 + let fetcher = use_context::<crate::fetch::Fetcher>(); 358 + let ident = use_signal(|| ident); 359 + let res = use_resource(move || { 354 360 let fetcher = fetcher.clone(); 355 361 async move { 356 362 fetcher 357 - .fetch_notebooks_for_did(&ident) 363 + .fetch_notebooks_for_did(&ident()) 358 364 .await 359 365 .ok() 360 - .map(|notebooks| notebooks.iter().map(|arc| (*arc).clone()).collect()) 366 + .map(|notebooks| { 367 + notebooks 368 + .iter() 369 + .map(|arc| serde_json::to_value(arc.as_ref()).ok()) 370 + .collect::<Option<Vec<_>>>() 371 + }) 372 + .flatten() 361 373 } 362 - })); 374 + }); 363 375 Ok(use_memo(move || { 364 - r.read_unchecked().as_ref().and_then(|v| v.clone()) 376 + if let Some(Some(values)) = &*res.read_unchecked() { 377 + values 378 + .iter() 379 + .map(|v| { 380 + jacquard::from_json_value::<(NotebookView, Vec<StrongRef>)>(v.clone()).ok() 381 + }) 382 + .collect::<Option<Vec<_>>>() 383 + } else { 384 + None 385 + } 365 386 })) 366 387 } 367 388 368 389 /// Fetches notebooks from UFOS with SSR support in fullstack mode 369 390 #[cfg(feature = "fullstack-server")] 370 - pub fn use_notebooks_from_ufos() -> Result< 371 - Memo<Option<Vec<(NotebookView<'static>, Vec<weaver_api::com_atproto::repo::strong_ref::StrongRef<'static>>)>>>, 372 - RenderError, 373 - > { 374 - let fetcher = use_context::<crate::fetch::CachedFetcher>(); 391 + pub fn use_notebooks_from_ufos() 392 + -> Result<Memo<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>>, RenderError> { 393 + let fetcher = use_context::<crate::fetch::Fetcher>(); 375 394 let res = use_server_future(move || { 376 395 let fetcher = fetcher.clone(); 377 396 async move { ··· 393 412 values 394 413 .iter() 395 414 .map(|v| { 396 - jacquard::from_json_value::<( 397 - NotebookView, 398 - Vec<weaver_api::com_atproto::repo::strong_ref::StrongRef>, 399 - )>(v.clone()) 400 - .ok() 415 + jacquard::from_json_value::<(NotebookView, Vec<StrongRef>)>(v.clone()).ok() 401 416 }) 402 417 .collect::<Option<Vec<_>>>() 403 418 } else { ··· 408 423 409 424 /// Fetches notebooks from UFOS client-side only (no SSR) 410 425 #[cfg(not(feature = "fullstack-server"))] 411 - pub fn use_notebooks_from_ufos() -> Result< 412 - Memo<Option<Vec<(NotebookView<'static>, Vec<weaver_api::com_atproto::repo::strong_ref::StrongRef<'static>>)>>>, 413 - RenderError, 414 - > { 415 - let fetcher = use_context::<crate::fetch::CachedFetcher>(); 416 - let r = use_resource(move || { 426 + pub fn use_notebooks_from_ufos() 427 + -> Result<Memo<Option<Vec<(NotebookView<'static>, Vec<StrongRef<'static>>)>>>, RenderError> { 428 + let fetcher = use_context::<crate::fetch::Fetcher>(); 429 + let res = use_resource(move || { 417 430 let fetcher = fetcher.clone(); 418 431 async move { 419 432 fetcher 420 433 .fetch_notebooks_from_ufos() 421 434 .await 422 435 .ok() 423 - .map(|notebooks| notebooks.iter().map(|arc| (*arc).clone()).collect()) 436 + .map(|notebooks| { 437 + notebooks 438 + .iter() 439 + .map(|arc| serde_json::to_value(arc.as_ref()).ok()) 440 + .collect::<Option<Vec<_>>>() 441 + }) 442 + .flatten() 424 443 } 425 444 }); 426 445 Ok(use_memo(move || { 427 - r.read_unchecked().as_ref().and_then(|v| v.clone()) 446 + if let Some(Some(values)) = &*res.read_unchecked() { 447 + values 448 + .iter() 449 + .map(|v| { 450 + jacquard::from_json_value::<(NotebookView, Vec<StrongRef>)>(v.clone()).ok() 451 + }) 452 + .collect::<Option<Vec<_>>>() 453 + } else { 454 + None 455 + } 428 456 })) 429 457 } 430 458 ··· 433 461 pub fn use_notebook( 434 462 ident: AtIdentifier<'static>, 435 463 book_title: SmolStr, 436 - ) -> Result< 437 - Memo<Option<(NotebookView<'static>, Vec<weaver_api::com_atproto::repo::strong_ref::StrongRef<'static>>)>>, 438 - RenderError, 439 - > { 440 - let fetcher = use_context::<crate::fetch::CachedFetcher>(); 464 + ) -> Result<Memo<Option<(NotebookView<'static>, Vec<StrongRef<'static>>)>>, RenderError> { 465 + let fetcher = use_context::<crate::fetch::Fetcher>(); 441 466 let ident = use_signal(|| ident); 442 467 let book_title = use_signal(|| book_title); 443 468 let res = use_server_future(move || { ··· 454 479 })?; 455 480 Ok(use_memo(move || { 456 481 if let Some(Some(value)) = &*res.read_unchecked() { 457 - jacquard::from_json_value::<( 458 - NotebookView, 459 - Vec<weaver_api::com_atproto::repo::strong_ref::StrongRef>, 460 - )>(value.clone()) 461 - .ok() 482 + jacquard::from_json_value::<(NotebookView, Vec<StrongRef>)>(value.clone()).ok() 462 483 } else { 463 484 None 464 485 } ··· 470 491 pub fn use_notebook( 471 492 ident: AtIdentifier<'static>, 472 493 book_title: SmolStr, 473 - ) -> Result< 474 - Memo<Option<(NotebookView<'static>, Vec<weaver_api::com_atproto::repo::strong_ref::StrongRef<'static>>)>>, 475 - RenderError, 476 - > { 477 - let fetcher = use_context::<crate::fetch::CachedFetcher>(); 478 - let r = use_resource(use_reactive!(|(ident, book_title)| { 494 + ) -> Result<Memo<Option<(NotebookView<'static>, Vec<StrongRef<'static>>)>>, RenderError> { 495 + let fetcher = use_context::<crate::fetch::Fetcher>(); 496 + let ident = use_signal(|| ident); 497 + let book_title = use_signal(|| book_title); 498 + let res = use_resource(move || { 479 499 let fetcher = fetcher.clone(); 480 500 async move { 481 501 fetcher 482 - .get_notebook(ident, book_title) 502 + .get_notebook(ident(), book_title()) 483 503 .await 484 504 .ok() 485 505 .flatten() 486 - .map(|arc| (*arc).clone()) 506 + .map(|arc| serde_json::to_value(arc.as_ref()).ok()) 507 + .flatten() 487 508 } 488 - })); 509 + }); 489 510 Ok(use_memo(move || { 490 - r.read_unchecked().as_ref().and_then(|v| v.clone()) 511 + if let Some(Some(value)) = &*res.read_unchecked() { 512 + jacquard::from_json_value::<(NotebookView, Vec<StrongRef>)>(value.clone()).ok() 513 + } else { 514 + None 515 + } 491 516 })) 492 517 } 493 518 ··· 497 522 ident: AtIdentifier<'static>, 498 523 book_title: SmolStr, 499 524 ) -> Result<Memo<Option<Vec<BookEntryView<'static>>>>, RenderError> { 500 - let fetcher = use_context::<crate::fetch::CachedFetcher>(); 525 + let fetcher = use_context::<crate::fetch::Fetcher>(); 501 526 let ident = use_signal(|| ident); 502 527 let book_title = use_signal(|| book_title); 503 528 let res = use_server_future(move || { ··· 535 560 ident: AtIdentifier<'static>, 536 561 book_title: SmolStr, 537 562 ) -> Result<Memo<Option<Vec<BookEntryView<'static>>>>, RenderError> { 538 - let fetcher = use_context::<crate::fetch::CachedFetcher>(); 563 + let fetcher = use_context::<crate::fetch::Fetcher>(); 539 564 let r = use_resource(use_reactive!(|(ident, book_title)| { 540 565 let fetcher = fetcher.clone(); 541 566 async move {
+3 -3
crates/weaver-app/src/env.rs
··· 1 1 // This file is automatically generated by build.rs 2 2 3 3 #[allow(unused)] 4 - pub const WEAVER_APP_ENV: &'static str = "dev"; 4 + pub const WEAVER_APP_ENV: &'static str = "prod"; 5 5 #[allow(unused)] 6 - pub const WEAVER_APP_HOST: &'static str = "http://localhost"; 6 + pub const WEAVER_APP_HOST: &'static str = "https://alpha.weaver.sh"; 7 7 #[allow(unused)] 8 - pub const WEAVER_APP_DOMAIN: &'static str = ""; 8 + pub const WEAVER_APP_DOMAIN: &'static str = "https://alpha.weaver.sh"; 9 9 #[allow(unused)] 10 10 pub const WEAVER_PORT: &'static str = "8080"; 11 11 #[allow(unused)]
+319 -67
crates/weaver-app/src/fetch.rs
··· 326 326 } 327 327 } 328 328 329 + //#[cfg(not(feature = "server"))] 329 330 #[derive(Clone)] 330 - pub struct CachedFetcher { 331 + pub struct Fetcher { 331 332 pub client: Arc<Client>, 332 - book_cache: cache_impl::Cache< 333 - (AtIdentifier<'static>, SmolStr), 334 - Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>, 335 - >, 336 - entry_cache: cache_impl::Cache< 337 - (AtIdentifier<'static>, SmolStr), 338 - Arc<(BookEntryView<'static>, Entry<'static>)>, 339 - >, 340 - profile_cache: cache_impl::Cache<AtIdentifier<'static>, Arc<ProfileDataView<'static>>>, 341 333 } 342 334 343 - /// SAFETY: This isn't thread-safe on WASM, but we aren't multithreaded on WASM 344 - unsafe impl Sync for CachedFetcher {} 345 - 346 - /// SAFETY: This isn't thread-safe on WASM, but we aren't multithreaded on WASM 347 - unsafe impl Send for CachedFetcher {} 348 - 349 - impl CachedFetcher { 335 + //#[cfg(not(feature = "server"))] 336 + impl Fetcher { 350 337 pub fn new(client: OAuthClient<JacquardResolver, AuthStore>) -> Self { 351 338 Self { 352 339 client: Arc::new(Client::new(client)), 353 - book_cache: cache_impl::new_cache(100, Duration::from_secs(1200)), 354 - entry_cache: cache_impl::new_cache(100, Duration::from_secs(600)), 355 - profile_cache: cache_impl::new_cache(100, Duration::from_secs(1800)), 356 340 } 357 341 } 358 342 ··· 390 374 ident: AtIdentifier<'static>, 391 375 title: SmolStr, 392 376 ) -> Result<Option<Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> { 393 - if let Some(entry) = cache_impl::get(&self.book_cache, &(ident.clone(), title.clone())) { 394 - Ok(Some(entry)) 377 + let client = self.get_client(); 378 + if let Some((notebook, entries)) = client 379 + .notebook_by_title(&ident, &title) 380 + .await 381 + .map_err(|e| dioxus::CapturedError::from_display(e))? 382 + { 383 + let stored = Arc::new((notebook, entries)); 384 + Ok(Some(stored)) 395 385 } else { 396 - let client = self.get_client(); 397 - if let Some((notebook, entries)) = client 398 - .notebook_by_title(&ident, &title) 399 - .await 400 - .map_err(|e| dioxus::CapturedError::from_display(e))? 401 - { 402 - let stored = Arc::new((notebook, entries)); 403 - cache_impl::insert(&self.book_cache, (ident, title), stored.clone()); 404 - Ok(Some(stored)) 405 - } else { 406 - Ok(None) 407 - } 386 + Ok(None) 408 387 } 409 388 } 410 389 ··· 416 395 ) -> Result<Option<Arc<(BookEntryView<'static>, Entry<'static>)>>> { 417 396 if let Some(result) = self.get_notebook(ident.clone(), book_title).await? { 418 397 let (notebook, entries) = result.as_ref(); 419 - if let Some(entry) = 420 - cache_impl::get(&self.entry_cache, &(ident.clone(), entry_title.clone())) 398 + let client = self.get_client(); 399 + if let Some(entry) = client 400 + .entry_by_title(notebook, entries.as_ref(), &entry_title) 401 + .await 402 + .map_err(|e| dioxus::CapturedError::from_display(e))? 421 403 { 422 - Ok(Some(entry)) 404 + let stored = Arc::new(entry); 405 + Ok(Some(stored)) 423 406 } else { 424 - let client = self.get_client(); 425 - if let Some(entry) = client 426 - .entry_by_title(notebook, entries.as_ref(), &entry_title) 427 - .await 428 - .map_err(|e| dioxus::CapturedError::from_display(e))? 429 - { 430 - let stored = Arc::new(entry); 431 - cache_impl::insert(&self.entry_cache, (ident, entry_title), stored.clone()); 432 - Ok(Some(stored)) 433 - } else { 434 - Ok(None) 435 - } 407 + Ok(None) 436 408 } 437 409 } else { 438 410 Ok(None) ··· 477 449 .unwrap_or_else(|| SmolStr::new("Untitled")); 478 450 479 451 let result = Arc::new((notebook, entries)); 480 - // Cache it 481 - cache_impl::insert(&self.book_cache, (ident, title), result.clone()); 482 452 notebooks.push(result); 483 453 } 484 454 Err(_) => continue, // Skip notebooks that fail to load ··· 546 516 .unwrap_or_else(|| SmolStr::new("Untitled")); 547 517 548 518 let result = Arc::new((notebook, entries)); 549 - // Cache it 550 - cache_impl::insert(&self.book_cache, (ident, title), result.clone()); 551 519 notebooks.push(result); 552 520 } 553 521 Err(_) => continue, // Skip notebooks that fail to load ··· 585 553 &self, 586 554 ident: &AtIdentifier<'_>, 587 555 ) -> Result<Arc<ProfileDataView<'static>>> { 588 - use jacquard::IntoStatic; 589 - 590 - let ident_static = ident.clone().into_static(); 591 - 592 - if let Some(cached) = cache_impl::get(&self.profile_cache, &ident_static) { 593 - return Ok(cached); 594 - } 595 - 596 556 let client = self.get_client(); 597 557 598 558 let did = match ident { ··· 609 569 .map_err(|e| dioxus::CapturedError::from_display(e))?; 610 570 611 571 let result = Arc::new(profile_view); 612 - cache_impl::insert(&self.profile_cache, ident_static, result.clone()); 613 572 614 573 Ok(result) 615 574 } 616 575 } 617 576 618 - impl HttpClient for CachedFetcher { 577 + // //#[cfg(feature = "server")] 578 + // #[derive(Clone)] 579 + // pub struct Fetcher { 580 + // pub client: Arc<Client>, 581 + // book_cache: cache_impl::Cache< 582 + // (AtIdentifier<'static>, SmolStr), 583 + // Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>, 584 + // >, 585 + // entry_cache: cache_impl::Cache< 586 + // (AtIdentifier<'static>, SmolStr), 587 + // Arc<(BookEntryView<'static>, Entry<'static>)>, 588 + // >, 589 + // profile_cache: cache_impl::Cache<AtIdentifier<'static>, Arc<ProfileDataView<'static>>>, 590 + // } 591 + 592 + // // /// SAFETY: This isn't thread-safe on WASM, but we aren't multithreaded on WASM 593 + // //#[cfg(feature = "server")] 594 + // unsafe impl Sync for Fetcher {} 595 + 596 + // // /// SAFETY: This isn't thread-safe on WASM, but we aren't multithreaded on WASM 597 + // //#[cfg(feature = "server")] 598 + // unsafe impl Send for Fetcher {} 599 + 600 + // //#[cfg(feature = "server")] 601 + // impl Fetcher { 602 + // pub fn new(client: OAuthClient<JacquardResolver, AuthStore>) -> Self { 603 + // Self { 604 + // client: Arc::new(Client::new(client)), 605 + // book_cache: cache_impl::new_cache(100, Duration::from_secs(30)), 606 + // entry_cache: cache_impl::new_cache(100, Duration::from_secs(30)), 607 + // profile_cache: cache_impl::new_cache(100, Duration::from_secs(1800)), 608 + // } 609 + // } 610 + 611 + // pub async fn upgrade_to_authenticated( 612 + // &self, 613 + // session: OAuthSession<JacquardResolver, crate::auth::AuthStore>, 614 + // ) { 615 + // let mut session_slot = self.client.session.write().await; 616 + // *session_slot = Some(Arc::new(Agent::new(session))); 617 + // } 618 + 619 + // pub async fn downgrade_to_unauthenticated(&self) { 620 + // let mut session_slot = self.client.session.write().await; 621 + // if let Some(session) = session_slot.take() { 622 + // session.inner().logout().await.ok(); 623 + // } 624 + // } 625 + 626 + // #[allow(dead_code)] 627 + // pub async fn current_did(&self) -> Option<Did<'static>> { 628 + // let session_slot = self.client.session.read().await; 629 + // if let Some(session) = session_slot.as_ref() { 630 + // session.info().await.map(|(d, _)| d) 631 + // } else { 632 + // None 633 + // } 634 + // } 635 + 636 + // pub fn get_client(&self) -> Arc<Client> { 637 + // self.client.clone() 638 + // } 639 + 640 + // pub async fn get_notebook( 641 + // &self, 642 + // ident: AtIdentifier<'static>, 643 + // title: SmolStr, 644 + // ) -> Result<Option<Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> { 645 + // if let Some(entry) = cache_impl::get(&self.book_cache, &(ident.clone(), title.clone())) { 646 + // Ok(Some(entry)) 647 + // } else { 648 + // let client = self.get_client(); 649 + // if let Some((notebook, entries)) = client 650 + // .notebook_by_title(&ident, &title) 651 + // .await 652 + // .map_err(|e| dioxus::CapturedError::from_display(e))? 653 + // { 654 + // let stored = Arc::new((notebook, entries)); 655 + // cache_impl::insert(&self.book_cache, (ident, title), stored.clone()); 656 + // Ok(Some(stored)) 657 + // } else { 658 + // Ok(None) 659 + // } 660 + // } 661 + // } 662 + 663 + // pub async fn get_entry( 664 + // &self, 665 + // ident: AtIdentifier<'static>, 666 + // book_title: SmolStr, 667 + // entry_title: SmolStr, 668 + // ) -> Result<Option<Arc<(BookEntryView<'static>, Entry<'static>)>>> { 669 + // if let Some(result) = self.get_notebook(ident.clone(), book_title).await? { 670 + // let (notebook, entries) = result.as_ref(); 671 + // if let Some(entry) = 672 + // cache_impl::get(&self.entry_cache, &(ident.clone(), entry_title.clone())) 673 + // { 674 + // Ok(Some(entry)) 675 + // } else { 676 + // let client = self.get_client(); 677 + // if let Some(entry) = client 678 + // .entry_by_title(notebook, entries.as_ref(), &entry_title) 679 + // .await 680 + // .map_err(|e| dioxus::CapturedError::from_display(e))? 681 + // { 682 + // let stored = Arc::new(entry); 683 + // cache_impl::insert(&self.entry_cache, (ident, entry_title), stored.clone()); 684 + // Ok(Some(stored)) 685 + // } else { 686 + // Ok(None) 687 + // } 688 + // } 689 + // } else { 690 + // Ok(None) 691 + // } 692 + // } 693 + 694 + // pub async fn fetch_notebooks_from_ufos( 695 + // &self, 696 + // ) -> Result<Vec<Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> { 697 + // use jacquard::{IntoStatic, types::aturi::AtUri}; 698 + 699 + // let url = "https://ufos-api.microcosm.blue/records?collection=sh.weaver.notebook.book"; 700 + // let response = reqwest::get(url) 701 + // .await 702 + // .map_err(|e| dioxus::CapturedError::from_display(e))?; 703 + 704 + // let records: Vec<UfosRecord> = response 705 + // .json() 706 + // .await 707 + // .map_err(|e| dioxus::CapturedError::from_display(e))?; 708 + 709 + // let mut notebooks = Vec::new(); 710 + // let client = self.get_client(); 711 + 712 + // for ufos_record in records { 713 + // // Construct URI 714 + // let uri_str = format!( 715 + // "at://{}/{}/{}", 716 + // ufos_record.did, ufos_record.collection, ufos_record.rkey 717 + // ); 718 + // let uri = AtUri::new_owned(uri_str) 719 + // .map_err(|e| dioxus::CapturedError::from_display(format!("Invalid URI: {}", e)))?; 720 + 721 + // // Fetch the full notebook view (which hydrates authors) 722 + // match client.view_notebook(&uri).await { 723 + // Ok((notebook, entries)) => { 724 + // let ident = uri.authority().clone().into_static(); 725 + // let title = notebook 726 + // .title 727 + // .as_ref() 728 + // .map(|t| SmolStr::new(t.as_ref())) 729 + // .unwrap_or_else(|| SmolStr::new("Untitled")); 730 + 731 + // let result = Arc::new((notebook, entries)); 732 + // // Cache it 733 + // cache_impl::insert(&self.book_cache, (ident, title), result.clone()); 734 + // notebooks.push(result); 735 + // } 736 + // Err(_) => continue, // Skip notebooks that fail to load 737 + // } 738 + // } 739 + 740 + // Ok(notebooks) 741 + // } 742 + 743 + // pub async fn fetch_notebooks_for_did( 744 + // &self, 745 + // ident: &AtIdentifier<'_>, 746 + // ) -> Result<Vec<Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>>> { 747 + // use jacquard::{ 748 + // IntoStatic, 749 + // types::{collection::Collection, nsid::Nsid}, 750 + // xrpc::XrpcExt, 751 + // }; 752 + // use weaver_api::{ 753 + // com_atproto::repo::list_records::ListRecords, sh_weaver::notebook::book::Book, 754 + // }; 755 + 756 + // let client = self.get_client(); 757 + 758 + // // Resolve DID and PDS 759 + // let (repo_did, pds_url) = match ident { 760 + // AtIdentifier::Did(did) => { 761 + // let pds = client 762 + // .pds_for_did(did) 763 + // .await 764 + // .map_err(|e| dioxus::CapturedError::from_display(e))?; 765 + // (did.clone(), pds) 766 + // } 767 + // AtIdentifier::Handle(handle) => client 768 + // .pds_for_handle(handle) 769 + // .await 770 + // .map_err(|e| dioxus::CapturedError::from_display(e))?, 771 + // }; 772 + 773 + // // Fetch all notebook records for this repo 774 + // let resp = client 775 + // .xrpc(pds_url) 776 + // .send( 777 + // &ListRecords::new() 778 + // .repo(repo_did) 779 + // .collection(Nsid::raw(Book::NSID)) 780 + // .limit(100) 781 + // .build(), 782 + // ) 783 + // .await 784 + // .map_err(|e| dioxus::CapturedError::from_display(e))?; 785 + 786 + // let mut notebooks = Vec::new(); 787 + 788 + // if let Ok(list) = resp.parse() { 789 + // for record in list.records { 790 + // // View the notebook (which hydrates authors) 791 + // match client.view_notebook(&record.uri).await { 792 + // Ok((notebook, entries)) => { 793 + // let ident = record.uri.authority().clone().into_static(); 794 + // let title = notebook 795 + // .title 796 + // .as_ref() 797 + // .map(|t| SmolStr::new(t.as_ref())) 798 + // .unwrap_or_else(|| SmolStr::new("Untitled")); 799 + 800 + // let result = Arc::new((notebook, entries)); 801 + // // Cache it 802 + // cache_impl::insert(&self.book_cache, (ident, title), result.clone()); 803 + // notebooks.push(result); 804 + // } 805 + // Err(_) => continue, // Skip notebooks that fail to load 806 + // } 807 + // } 808 + // } 809 + 810 + // Ok(notebooks) 811 + // } 812 + 813 + // pub async fn list_notebook_entries( 814 + // &self, 815 + // ident: AtIdentifier<'static>, 816 + // book_title: SmolStr, 817 + // ) -> Result<Option<Vec<BookEntryView<'static>>>> { 818 + // if let Some(result) = self.get_notebook(ident.clone(), book_title).await? { 819 + // let (notebook, entries) = result.as_ref(); 820 + // let mut book_entries = Vec::new(); 821 + // let client = self.get_client(); 822 + 823 + // for index in 0..entries.len() { 824 + // match client.view_entry(notebook, entries, index).await { 825 + // Ok(book_entry) => book_entries.push(book_entry), 826 + // Err(_) => continue, // Skip entries that fail to load 827 + // } 828 + // } 829 + 830 + // Ok(Some(book_entries)) 831 + // } else { 832 + // Ok(None) 833 + // } 834 + // } 835 + 836 + // pub async fn fetch_profile( 837 + // &self, 838 + // ident: &AtIdentifier<'_>, 839 + // ) -> Result<Arc<ProfileDataView<'static>>> { 840 + // use jacquard::IntoStatic; 841 + 842 + // let ident_static = ident.clone().into_static(); 843 + 844 + // if let Some(cached) = cache_impl::get(&self.profile_cache, &ident_static) { 845 + // return Ok(cached); 846 + // } 847 + 848 + // let client = self.get_client(); 849 + 850 + // let did = match ident { 851 + // AtIdentifier::Did(d) => d.clone(), 852 + // AtIdentifier::Handle(h) => client 853 + // .resolve_handle(h) 854 + // .await 855 + // .map_err(|e| dioxus::CapturedError::from_display(e))?, 856 + // }; 857 + 858 + // let (_uri, profile_view) = client 859 + // .hydrate_profile_view(&did) 860 + // .await 861 + // .map_err(|e| dioxus::CapturedError::from_display(e))?; 862 + 863 + // let result = Arc::new(profile_view); 864 + // cache_impl::insert(&self.profile_cache, ident_static, result.clone()); 865 + 866 + // Ok(result) 867 + // } 868 + // } 869 + 870 + impl HttpClient for Fetcher { 619 871 #[doc = " Error type returned by the HTTP client"] 620 872 type Error = reqwest::Error; 621 873 ··· 645 897 } 646 898 } 647 899 648 - impl XrpcClient for CachedFetcher { 900 + impl XrpcClient for Fetcher { 649 901 #[doc = " Get the base URI for the client."] 650 902 fn base_uri(&self) -> impl Future<Output = CowStr<'static>> + Send { 651 903 self.client.base_uri() ··· 717 969 } 718 970 } 719 971 720 - impl IdentityResolver for CachedFetcher { 972 + impl IdentityResolver for Fetcher { 721 973 #[doc = " Access options for validation decisions in default methods"] 722 974 fn options(&self) -> &ResolverOptions { 723 975 self.client.options() ··· 766 1018 } 767 1019 } 768 1020 769 - impl LexiconSchemaResolver for CachedFetcher { 1021 + impl LexiconSchemaResolver for Fetcher { 770 1022 #[cfg(not(target_arch = "wasm32"))] 771 1023 async fn resolve_lexicon_schema( 772 1024 &self, ··· 784 1036 } 785 1037 } 786 1038 787 - impl AgentSession for CachedFetcher { 1039 + impl AgentSession for Fetcher { 788 1040 #[doc = " Identify the kind of session."] 789 1041 fn session_kind(&self) -> AgentKind { 790 1042 self.client.session_kind()
+21 -25
crates/weaver-app/src/main.rs
··· 1 1 // The dioxus prelude contains a ton of common items used in dioxus apps. It's a good idea to import wherever you 2 2 // need dioxus 3 - use components::{Entry, Repository, RepositoryIndex}; 3 + use components::{EntryPage, Repository, RepositoryIndex}; 4 4 #[allow(unused)] 5 5 use dioxus::{CapturedError, prelude::*}; 6 6 ··· 14 14 smol_str::SmolStr, 15 15 types::{cid::Cid, string::AtIdentifier}, 16 16 }; 17 - #[cfg(feature = "server")] 18 - use std::sync::Arc; 19 - use std::sync::LazyLock; 17 + use std::sync::{Arc, LazyLock}; 20 18 #[allow(unused)] 21 19 use views::{ 22 - Callback, Home, Navbar, Notebook, NotebookIndex, NotebookPage, RecordIndex, RecordView, 20 + Callback, Home, Navbar, Notebook, NotebookIndex, NotebookPage, RecordIndex, RecordPage, 23 21 }; 24 22 25 23 use crate::{ ··· 37 35 mod data; 38 36 mod env; 39 37 mod fetch; 38 + mod record_utils; 40 39 mod service_worker; 41 40 /// Define a views module that contains the UI for all Layouts and Routes for our app. 42 41 mod views; ··· 59 58 #[nest("/record")] 60 59 #[layout(RecordIndex)] 61 60 #[route("/:..uri")] 62 - RecordView { uri: Vec<String> }, 61 + RecordPage { uri: Vec<String> }, 63 62 #[end_layout] 64 63 #[end_nest] 65 64 #[route("/callback?:state&:iss&:code")] ··· 73 72 #[route("/")] 74 73 NotebookIndex { ident: AtIdentifier<'static>, book_title: SmolStr }, 75 74 #[route("/:title")] 76 - Entry { ident: AtIdentifier<'static>, book_title: SmolStr, title: SmolStr } 75 + EntryPage { ident: AtIdentifier<'static>, book_title: SmolStr, title: SmolStr } 77 76 78 77 } 79 78 const FAVICON: Asset = asset!("/assets/weaver_photo_sm.jpg"); ··· 122 121 #[cfg(feature = "fullstack-server")] 123 122 let router = { 124 123 use jacquard::client::UnauthenticatedSession; 125 - let fetcher = Arc::new(fetch::CachedFetcher::new(OAuthClient::new( 124 + let fetcher = Arc::new(fetch::Fetcher::new(OAuthClient::new( 126 125 AuthStore::new(), 127 126 ClientData::new_public(CONFIG.oauth.clone()), 128 127 ))); ··· 132 131 axum::Router::new() 133 132 // Server side render the application, serve static assets, and register server functions 134 133 .serve_dioxus_application( 135 - ServeConfig::builder() 136 - // Enable incremental rendering 137 - .incremental( 138 - dioxus::server::IncrementalRendererConfig::new() 139 - .pre_render(true) 140 - .clear_cache(false), 141 - ) 142 - .enable_out_of_order_streaming(), 134 + ServeConfig::builder(), // Enable incremental rendering 135 + // .incremental( 136 + // dioxus::server::IncrementalRendererConfig::new() 137 + // .pre_render(true) 138 + // .clear_cache(false), 139 + // ) 140 + //.enable_out_of_order_streaming(), 143 141 App, 144 142 ) 145 143 .layer(middleware::from_fn({ ··· 165 163 166 164 #[component] 167 165 fn App() -> Element { 168 - use_context_provider(|| { 169 - fetch::CachedFetcher::new(OAuthClient::new( 166 + let fetcher = use_context_provider(|| { 167 + fetch::Fetcher::new(OAuthClient::new( 170 168 AuthStore::new(), 171 169 ClientData::new_public(CONFIG.oauth.clone()), 172 170 )) 173 171 }); 174 172 use_context_provider(|| Signal::new(AuthState::default())); 175 - 176 173 use_effect(move || { 174 + let fetcher = fetcher.clone(); 177 175 spawn(async move { 178 - if let Err(e) = auth::restore_session().await { 179 - tracing::warn!("Session restoration failed: {}", e); 176 + if let Err(e) = auth::restore_session(fetcher).await { 177 + tracing::debug!("Session restoration failed: {}", e); 180 178 } 181 179 }); 182 180 }); ··· 186 184 target_os = "unknown", 187 185 not(feature = "fullstack-server") 188 186 ))] 189 - use_effect(move || { 190 - spawn(async move { 191 - let _ = service_worker::register_service_worker().await; 192 - }); 187 + spawn(async move { 188 + let _ = service_worker::register_service_worker().await; 193 189 }); 194 190 195 191 rsx! {
+361
crates/weaver-app/src/record_utils.rs
··· 1 + use dioxus::prelude::*; 2 + use jacquard::bytes::Bytes; 3 + use jacquard::common::{Data, IntoStatic}; 4 + use jacquard::types::LexiconStringType; 5 + use jacquard::types::string::AtprotoStr; 6 + use jacquard_lexicon::validation::{ 7 + ConstraintError, StructuralError, ValidationError, ValidationResult, 8 + }; 9 + 10 + // ============================================================================ 11 + // Validation Helper Functions 12 + // ============================================================================ 13 + 14 + /// Parse UI path into segments (fields and indices only) 15 + fn parse_ui_path(ui_path: &str) -> Vec<UiPathSegment> { 16 + if ui_path.is_empty() { 17 + return vec![]; 18 + } 19 + 20 + let mut segments = Vec::new(); 21 + let mut current = String::new(); 22 + 23 + for ch in ui_path.chars() { 24 + match ch { 25 + '.' => { 26 + if !current.is_empty() { 27 + segments.push(UiPathSegment::Field(current.clone())); 28 + current.clear(); 29 + } 30 + } 31 + '[' => { 32 + if !current.is_empty() { 33 + segments.push(UiPathSegment::Field(current.clone())); 34 + current.clear(); 35 + } 36 + } 37 + ']' => { 38 + if !current.is_empty() { 39 + if let Ok(idx) = current.parse::<usize>() { 40 + segments.push(UiPathSegment::Index(idx)); 41 + } 42 + current.clear(); 43 + } 44 + } 45 + c => current.push(c), 46 + } 47 + } 48 + 49 + if !current.is_empty() { 50 + segments.push(UiPathSegment::Field(current)); 51 + } 52 + 53 + segments 54 + } 55 + 56 + #[derive(Debug, PartialEq)] 57 + enum UiPathSegment { 58 + Field(String), 59 + Index(usize), 60 + } 61 + 62 + /// Get all validation errors at exactly this path (not children) 63 + pub fn get_errors_at_exact_path( 64 + validation_result: &Option<ValidationResult>, 65 + ui_path: &str, 66 + ) -> Vec<String> { 67 + use jacquard_lexicon::validation::PathSegment; 68 + 69 + if let Some(result) = validation_result { 70 + let ui_segments = parse_ui_path(ui_path); 71 + 72 + result 73 + .all_errors() 74 + .filter_map(|err| { 75 + let validation_path = match &err { 76 + ValidationError::Structural(s) => match s { 77 + StructuralError::TypeMismatch { path, .. } => Some(path), 78 + StructuralError::MissingRequiredField { path, .. } => Some(path), 79 + StructuralError::MissingUnionDiscriminator { path } => Some(path), 80 + StructuralError::UnionNoMatch { path, .. } => Some(path), 81 + StructuralError::UnresolvedRef { path, .. } => Some(path), 82 + StructuralError::RefCycle { path, .. } => Some(path), 83 + StructuralError::MaxDepthExceeded { path, .. } => Some(path), 84 + }, 85 + ValidationError::Constraint(c) => match c { 86 + ConstraintError::MaxLength { path, .. } => Some(path), 87 + ConstraintError::MaxGraphemes { path, .. } => Some(path), 88 + ConstraintError::MinLength { path, .. } => Some(path), 89 + ConstraintError::MinGraphemes { path, .. } => Some(path), 90 + ConstraintError::Maximum { path, .. } => Some(path), 91 + ConstraintError::Minimum { path, .. } => Some(path), 92 + }, 93 + }; 94 + 95 + if let Some(path) = validation_path { 96 + // Convert validation path to UI segments 97 + let validation_ui_segments: Vec<_> = path 98 + .segments() 99 + .iter() 100 + .filter_map(|seg| match seg { 101 + PathSegment::Field(name) => { 102 + Some(UiPathSegment::Field(name.to_string())) 103 + } 104 + PathSegment::Index(idx) => Some(UiPathSegment::Index(*idx)), 105 + PathSegment::UnionVariant(_) => None, 106 + }) 107 + .collect(); 108 + 109 + // Exact match only 110 + if validation_ui_segments == ui_segments { 111 + return Some(err.to_string()); 112 + } 113 + } 114 + None 115 + }) 116 + .collect() 117 + } else { 118 + Vec::new() 119 + } 120 + } 121 + 122 + // ============================================================================ 123 + // Pretty Editor: Helper Functions 124 + // ============================================================================ 125 + 126 + /// Infer Data type from text input 127 + pub fn infer_data_from_text(text: &str) -> Result<Data<'static>, String> { 128 + let trimmed = text.trim(); 129 + 130 + if trimmed == "true" || trimmed == "false" { 131 + Ok(Data::Boolean(trimmed == "true")) 132 + } else if trimmed == "{}" { 133 + use jacquard::types::value::Object; 134 + use std::collections::BTreeMap; 135 + Ok(Data::Object(Object(BTreeMap::new()))) 136 + } else if trimmed == "[]" { 137 + use jacquard::types::value::Array; 138 + Ok(Data::Array(Array(Vec::new()))) 139 + } else if trimmed == "null" { 140 + Ok(Data::Null) 141 + } else if let Ok(num) = trimmed.parse::<i64>() { 142 + Ok(Data::Integer(num)) 143 + } else { 144 + // Smart string parsing 145 + use jacquard::types::value::parsing; 146 + Ok(Data::String(parsing::parse_string(trimmed).into_static())) 147 + } 148 + } 149 + 150 + /// Parse text as specific AtprotoStr type, preserving type information 151 + pub fn try_parse_as_type( 152 + text: &str, 153 + string_type: LexiconStringType, 154 + ) -> Result<AtprotoStr<'static>, String> { 155 + use jacquard::types::string::*; 156 + use std::str::FromStr; 157 + 158 + match string_type { 159 + LexiconStringType::Datetime => Datetime::from_str(text) 160 + .map(AtprotoStr::Datetime) 161 + .map_err(|e| format!("Invalid datetime: {}", e)), 162 + LexiconStringType::Did => Did::new(text) 163 + .map(|v| AtprotoStr::Did(v.into_static())) 164 + .map_err(|e| format!("Invalid DID: {}", e)), 165 + LexiconStringType::Handle => Handle::new(text) 166 + .map(|v| AtprotoStr::Handle(v.into_static())) 167 + .map_err(|e| format!("Invalid handle: {}", e)), 168 + LexiconStringType::AtUri => AtUri::new(text) 169 + .map(|v| AtprotoStr::AtUri(v.into_static())) 170 + .map_err(|e| format!("Invalid AT-URI: {}", e)), 171 + LexiconStringType::AtIdentifier => AtIdentifier::new(text) 172 + .map(|v| AtprotoStr::AtIdentifier(v.into_static())) 173 + .map_err(|e| format!("Invalid identifier: {}", e)), 174 + LexiconStringType::Nsid => Nsid::new(text) 175 + .map(|v| AtprotoStr::Nsid(v.into_static())) 176 + .map_err(|e| format!("Invalid NSID: {}", e)), 177 + LexiconStringType::Tid => Tid::new(text) 178 + .map(|v| AtprotoStr::Tid(v.into_static())) 179 + .map_err(|e| format!("Invalid TID: {}", e)), 180 + LexiconStringType::RecordKey => Rkey::new(text) 181 + .map(|rk| AtprotoStr::RecordKey(RecordKey::from(rk))) 182 + .map_err(|e| format!("Invalid record key: {}", e)), 183 + LexiconStringType::Cid => Cid::new(text.as_bytes()) 184 + .map(|v| AtprotoStr::Cid(v.into_static())) 185 + .map_err(|_| "Invalid CID".to_string()), 186 + LexiconStringType::Language => Language::new(text) 187 + .map(AtprotoStr::Language) 188 + .map_err(|e| format!("Invalid language: {}", e)), 189 + LexiconStringType::Uri(_) => Uri::new(text) 190 + .map(|u| AtprotoStr::Uri(u.into_static())) 191 + .map_err(|e| format!("Invalid URI: {}", e)), 192 + LexiconStringType::String => { 193 + // Plain strings: use smart inference 194 + use jacquard::types::value::parsing; 195 + Ok(parsing::parse_string(text).into_static()) 196 + } 197 + } 198 + } 199 + 200 + /// Create default value for new array item by cloning structure of existing items 201 + pub fn create_array_item_default(arr: &jacquard::types::value::Array) -> Data<'static> { 202 + if let Some(existing) = arr.0.first() { 203 + clone_structure(existing) 204 + } else { 205 + // Empty array, default to null (user can change type) 206 + Data::Null 207 + } 208 + } 209 + 210 + /// Clone structure of Data, setting sensible defaults for leaf values 211 + pub fn clone_structure(data: &Data) -> Data<'static> { 212 + use jacquard::types::string::*; 213 + use jacquard::types::value::{Array, Object}; 214 + use jacquard::types::{LexiconStringType, blob::*}; 215 + use std::collections::BTreeMap; 216 + 217 + match data { 218 + Data::Object(obj) => { 219 + let mut new_obj = BTreeMap::new(); 220 + for (key, value) in obj.0.iter() { 221 + new_obj.insert(key.clone(), clone_structure(value)); 222 + } 223 + Data::Object(Object(new_obj)) 224 + } 225 + Data::Array(_) => Data::Array(Array(Vec::new())), 226 + Data::String(s) => match s.string_type() { 227 + LexiconStringType::Datetime => { 228 + // Sensible default: now 229 + Data::String(AtprotoStr::Datetime(Datetime::now())) 230 + } 231 + LexiconStringType::Tid => Data::String(AtprotoStr::Tid(Tid::now_0())), 232 + _ => { 233 + // Empty string, type inference will handle it 234 + Data::String(AtprotoStr::String("".into())) 235 + } 236 + }, 237 + Data::Integer(_) => Data::Integer(0), 238 + Data::Boolean(_) => Data::Boolean(false), 239 + Data::Blob(blob) => { 240 + // Placeholder blob 241 + Data::Blob( 242 + Blob { 243 + r#ref: CidLink::str( 244 + "bafkreiaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 245 + ), 246 + mime_type: blob.mime_type.clone(), 247 + size: 0, 248 + } 249 + .into_static(), 250 + ) 251 + } 252 + Data::CidLink(_) => Data::CidLink(Cid::str( 253 + "bafkreiaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 254 + )), 255 + Data::Bytes(_) => Data::Bytes(Bytes::new()), 256 + Data::Null => Data::Null, 257 + } 258 + } 259 + 260 + /// Get expected string format from schema by navigating path 261 + pub fn get_expected_string_format( 262 + root_data: &Data<'_>, 263 + current_path: &str, 264 + ) -> Option<jacquard_lexicon::lexicon::LexStringFormat> { 265 + use jacquard_lexicon::lexicon::*; 266 + 267 + // Get root type discriminator 268 + let root_nsid = root_data.type_discriminator()?; 269 + 270 + // Look up schema in global registry 271 + let validator = jacquard_lexicon::validation::SchemaValidator::global(); 272 + let registry = validator.registry(); 273 + let schema = registry.get(root_nsid)?; 274 + 275 + // Navigate to the property at this path 276 + let segments: Vec<&str> = if current_path.is_empty() { 277 + vec![] 278 + } else { 279 + current_path.split('.').collect() 280 + }; 281 + 282 + // Start with the record's main object definition 283 + let main_obj = match schema.defs.get("main")? { 284 + LexUserType::Record(rec) => match &rec.record { 285 + LexRecordRecord::Object(obj) => obj, 286 + }, 287 + _ => return None, 288 + }; 289 + 290 + // Track current position in schema 291 + enum SchemaType<'a> { 292 + ObjectProp(&'a LexObjectProperty<'a>), 293 + ArrayItem(&'a LexArrayItem<'a>), 294 + } 295 + 296 + let mut current_type: Option<SchemaType> = None; 297 + let mut current_obj = Some(main_obj); 298 + 299 + for segment in segments { 300 + // Handle array indices - strip them to get field name 301 + let field_name = segment.trim_end_matches(|c: char| c.is_numeric() || c == '[' || c == ']'); 302 + 303 + if field_name.is_empty() { 304 + continue; // Pure array index like [0], skip 305 + } 306 + 307 + if let Some(obj) = current_obj.take() { 308 + if let Some(prop) = obj.properties.get(field_name) { 309 + current_type = Some(SchemaType::ObjectProp(prop)); 310 + } 311 + } 312 + 313 + // Process current type 314 + match current_type { 315 + Some(SchemaType::ObjectProp(LexObjectProperty::Array(arr))) => { 316 + // Array - unwrap to item type 317 + current_type = Some(SchemaType::ArrayItem(&arr.items)); 318 + } 319 + Some(SchemaType::ObjectProp(LexObjectProperty::Object(obj))) => { 320 + // Nested object - descend into it 321 + current_obj = Some(obj); 322 + current_type = None; 323 + } 324 + Some(SchemaType::ArrayItem(LexArrayItem::Object(obj))) => { 325 + // Array of objects - descend into object 326 + current_obj = Some(obj); 327 + current_type = None; 328 + } 329 + _ => {} 330 + } 331 + } 332 + 333 + // Check if final type is a string with format 334 + match current_type? { 335 + SchemaType::ObjectProp(LexObjectProperty::String(lex_string)) => lex_string.format, 336 + SchemaType::ArrayItem(LexArrayItem::String(lex_string)) => lex_string.format, 337 + _ => None, 338 + } 339 + } 340 + 341 + pub fn get_hex_rep(byte_array: &mut [u8]) -> String { 342 + let build_string_vec: Vec<String> = byte_array 343 + .chunks(2) 344 + .enumerate() 345 + .map(|(i, c)| { 346 + let sep = if i % 16 == 0 && i > 0 { 347 + "\n" 348 + } else if i == 0 { 349 + "" 350 + } else { 351 + " " 352 + }; 353 + if c.len() == 2 { 354 + format!("{}{:02x}{:02x}", sep, c[0], c[1]) 355 + } else { 356 + format!("{}{:02x}", sep, c[0]) 357 + } 358 + }) 359 + .collect(); 360 + build_string_vec.join("") 361 + }
+1 -1
crates/weaver-app/src/service_worker.rs
··· 25 25 ident: &jacquard::types::ident::AtIdentifier<'_>, 26 26 book_title: &str, 27 27 images: &weaver_api::sh_weaver::embed::images::Images<'_>, 28 - fetcher: &crate::fetch::CachedFetcher, 28 + fetcher: &crate::fetch::Fetcher, 29 29 ) -> Result<(), JsValue> { 30 30 use jacquard::prelude::IdentityResolver; 31 31 use std::collections::HashMap;
+5 -3
crates/weaver-app/src/views/callback.rs
··· 1 1 use crate::auth::AuthState; 2 - use crate::fetch::CachedFetcher; 2 + use crate::fetch::Fetcher; 3 3 use dioxus::prelude::*; 4 4 use jacquard::{ 5 5 IntoStatic, ··· 15 15 iss: ReadSignal<SmolStr>, 16 16 code: ReadSignal<SmolStr>, 17 17 ) -> Element { 18 - let fetcher = use_context::<CachedFetcher>(); 18 + let fetcher = use_context::<Fetcher>(); 19 19 let mut auth = use_context::<Signal<AuthState>>(); 20 20 #[cfg(feature = "web")] 21 21 let result = { ··· 43 43 }; 44 44 #[cfg(not(feature = "web"))] 45 45 let result = { use_resource(move || async { Ok::<(), OAuthError>(()) }) }; 46 + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] 47 + let nav = use_navigator(); 46 48 47 49 match &*result.read_unchecked() { 48 50 Some(Ok(())) => { ··· 52 54 let mut prev = gloo_storage::LocalStorage::get::<String>("cached_route").ok(); 53 55 if let Some(prev) = prev.take() { 54 56 tracing::info!("Navigating to previous page"); 55 - let nav = use_navigator(); 57 + 56 58 gloo_storage::LocalStorage::delete("cached_route"); 57 59 nav.replace(prev); 58 60 }
crates/weaver-app/src/views/editor.rs

This is a binary file and will not be displayed.

+17 -21
crates/weaver-app/src/views/home.rs
··· 8 8 #[component] 9 9 pub fn Home() -> Element { 10 10 // Fetch notebooks from UFOS with SSR support 11 - let notebooks = data::use_notebooks_from_ufos().ok(); 11 + let notebooks = data::use_notebooks_from_ufos()?; 12 12 let navigator = use_navigator(); 13 13 let mut uri_input = use_signal(|| String::new()); 14 14 ··· 16 16 let input_uri = uri_input.read().clone(); 17 17 if !input_uri.is_empty() { 18 18 if let Ok(parsed) = AtUri::new(&input_uri) { 19 - navigator.push(Route::RecordView { 19 + navigator.push(Route::RecordPage { 20 20 uri: vec![parsed.to_string()], 21 21 }); 22 22 } ··· 44 44 } 45 45 } 46 46 div { class: "notebooks-list", 47 - if let Some(notebooks_memo) = &notebooks { 48 - match &*notebooks_memo.read_unchecked() { 49 - Some(notebook_list) => rsx! { 50 - for notebook in notebook_list.iter() { 51 - { 52 - let view = &notebook.0; 53 - let entries = &notebook.1; 54 - rsx! { 55 - div { 56 - key: "{view.cid}", 57 - NotebookCard { 58 - notebook: view.clone(), 59 - entry_refs: entries.clone() 60 - } 47 + match &*notebooks.read() { 48 + Some(notebook_list) => rsx! { 49 + for notebook in notebook_list.iter() { 50 + { 51 + let view = &notebook.0; 52 + let entries = &notebook.1; 53 + rsx! { 54 + div { 55 + key: "{view.cid}", 56 + NotebookCard { 57 + notebook: view.clone(), 58 + entry_refs: entries.clone() 61 59 } 62 60 } 63 61 } 64 62 } 65 - }, 66 - None => rsx! { 67 - div { "Loading notebooks..." } 68 63 } 64 + }, 65 + _ => rsx! { 66 + div { "Loading notebooks..." } 69 67 } 70 - } else { 71 - div { "Loading notebooks..." } 72 68 } 73 69 } 74 70 }
+3 -1
crates/weaver-app/src/views/mod.rs
··· 21 21 pub use notebook::{Notebook, NotebookIndex}; 22 22 23 23 mod record; 24 - pub use record::{RecordIndex, RecordView}; 24 + pub use record::{RecordIndex, RecordPage, RecordView}; 25 25 26 26 mod callback; 27 27 pub use callback::Callback; 28 + 29 + mod editor;
+44 -30
crates/weaver-app/src/views/navbar.rs
··· 1 1 use crate::Route; 2 2 use crate::components::button::{Button, ButtonVariant}; 3 3 use crate::components::login::LoginModal; 4 - use crate::data::use_handle; 5 - use crate::fetch::CachedFetcher; 4 + use crate::data::{get_handle, use_notebook_handle}; 5 + use crate::fetch::Fetcher; 6 6 use dioxus::prelude::*; 7 - use jacquard::types::string::AtIdentifier; 8 7 9 8 const THEME_DEFAULTS_CSS: Asset = asset!("/assets/styling/theme-defaults.css"); 10 9 const NAVBAR_CSS: Asset = asset!("/assets/styling/navbar.css"); ··· 19 18 let route = use_route::<Route>(); 20 19 let mut auth_state = use_context::<Signal<crate::auth::AuthState>>(); 21 20 let mut show_login_modal = use_signal(|| false); 22 - let fetcher = use_context::<CachedFetcher>(); 21 + let fetcher = use_context::<Fetcher>(); 22 + let route_handle = use_signal(|| match &route { 23 + Route::EntryPage { ident, .. } => Some(ident.clone()), 24 + Route::RepositoryIndex { ident } => Some(ident.clone()), 25 + Route::NotebookIndex { ident, .. } => Some(ident.clone()), 26 + _ => None, 27 + }); 28 + let notebook_handle = use_notebook_handle(route_handle); 23 29 24 30 rsx! { 25 31 document::Link { rel: "stylesheet", href: THEME_DEFAULTS_CSS } ··· 36 42 37 43 // Show repository breadcrumb if we're on a repository page 38 44 match route { 39 - Route::RepositoryIndex { ident } => rsx! { 40 - span { class: "breadcrumb-separator", " > " } 41 - span { class: "breadcrumb breadcrumb-current", "@{use_handle(ident.clone())?}" } 42 - }, 43 - Route::NotebookIndex { ident, book_title } => rsx! { 44 - span { class: "breadcrumb-separator", " > " } 45 - Link { 46 - to: Route::RepositoryIndex { ident: ident.clone() }, 47 - class: "breadcrumb", 48 - "@{use_handle(ident.clone())?}" 45 + Route::RepositoryIndex { .. } => { 46 + let handle = notebook_handle.as_ref().unwrap(); 47 + rsx! { 48 + span { class: "breadcrumb-separator", " > " } 49 + span { class: "breadcrumb breadcrumb-current", "@{handle}" } 49 50 } 50 - span { class: "breadcrumb-separator", " > " } 51 - span { class: "breadcrumb breadcrumb-current", "{book_title}" } 52 51 }, 53 - Route::Entry { ident, book_title, .. } => rsx! { 54 - span { class: "breadcrumb-separator", " > " } 55 - Link { 56 - to: Route::RepositoryIndex { ident: ident.clone() }, 57 - class: "breadcrumb", 58 - "@{use_handle(ident.clone())?}" 52 + Route::NotebookIndex { ident, book_title } => { 53 + let handle = notebook_handle.as_ref().unwrap(); 54 + rsx! { 55 + span { class: "breadcrumb-separator", " > " } 56 + Link { 57 + to: Route::RepositoryIndex { ident: ident.clone() }, 58 + class: "breadcrumb", 59 + "@{handle}" 60 + } 61 + span { class: "breadcrumb-separator", " > " } 62 + span { class: "breadcrumb breadcrumb-current", "{book_title}" } 59 63 } 60 - span { class: "breadcrumb-separator", " > " } 61 - Link { 62 - to: Route::NotebookIndex { ident: ident.clone(), book_title: book_title.clone() }, 63 - class: "breadcrumb", 64 - "{book_title}" 64 + }, 65 + Route::EntryPage { ident, book_title, .. } => { 66 + let handle = notebook_handle.as_ref().unwrap(); 67 + rsx! { 68 + span { class: "breadcrumb-separator", " > " } 69 + Link { 70 + to: Route::RepositoryIndex { ident: ident.clone() }, 71 + class: "breadcrumb", 72 + "@{handle}" 73 + } 74 + span { class: "breadcrumb-separator", " > " } 75 + Link { 76 + to: Route::NotebookIndex { ident: ident.clone(), book_title: book_title.clone() }, 77 + class: "breadcrumb", 78 + "{book_title}" 79 + } 65 80 } 66 81 }, 67 82 _ => rsx! {} ··· 78 93 fetcher.downgrade_to_unauthenticated().await; 79 94 } 80 95 }, 81 - span { class: "auth-handle", "@{use_handle(AtIdentifier::Did(did.clone()))?}" } 96 + span { class: "auth-handle", "@{get_handle(did.clone())}" } 82 97 } 83 98 } 99 + 84 100 } else { 85 101 div { 86 102 class: "auth-button", ··· 97 113 } 98 114 } 99 115 100 - // The `Outlet` component is used to render the next component inside the layout. In this case, it will render either 101 - // the [`Home`] or [`Blog`] component depending on the current route. 102 116 Outlet::<Route> {} 103 117 } 104 118 }
+23 -27
crates/weaver-app/src/views/notebook.rs
··· 29 29 book_title: ReadSignal<SmolStr>, 30 30 ) -> Element { 31 31 // Fetch full notebook metadata with SSR support 32 - let notebook_data = data::use_notebook(ident(), book_title()).ok(); 32 + let notebook_data = data::use_notebook(ident(), book_title())?; 33 33 34 34 // Fetch entries with SSR support 35 - let entries_resource = data::use_notebook_entries(ident(), book_title()).ok(); 35 + let entries_resource = data::use_notebook_entries(ident(), book_title())?; 36 36 37 37 rsx! { 38 38 document::Link { rel: "stylesheet", href: ENTRY_CARD_CSS } 39 39 40 - if let (Some(notebook_memo), Some(entries_memo)) = (&notebook_data, &entries_resource) { 41 - match (&*notebook_memo.read_unchecked(), &*entries_memo.read_unchecked()) { 42 - (Some(data), Some(entries)) => { 43 - let (notebook_view, _) = data; 44 - let author_count = notebook_view.authors.len(); 40 + match (&*notebook_data.read(), &*entries_resource.read()) { 41 + (Some(data), Some(entries)) => { 42 + let (notebook_view, _) = data; 43 + let author_count = notebook_view.authors.len(); 45 44 46 - rsx! { 47 - div { class: "notebook-layout", 48 - aside { class: "notebook-sidebar", 49 - NotebookCover { 50 - notebook: notebook_view.clone(), 51 - title: book_title().to_string() 52 - } 45 + rsx! { 46 + div { class: "notebook-layout", 47 + aside { class: "notebook-sidebar", 48 + NotebookCover { 49 + notebook: notebook_view.clone(), 50 + title: book_title().to_string() 53 51 } 52 + } 54 53 55 - main { class: "notebook-main", 56 - div { class: "entries-list", 57 - for entry in entries { 58 - EntryCard { 59 - entry: entry.clone(), 60 - book_title: book_title(), 61 - author_count 62 - } 54 + main { class: "notebook-main", 55 + div { class: "entries-list", 56 + for entry in entries { 57 + EntryCard { 58 + entry: entry.clone(), 59 + book_title: book_title(), 60 + author_count 63 61 } 64 62 } 65 63 } 66 64 } 67 65 } 68 - }, 69 - _ => rsx! { div { class: "loading", "Loading..." } } 70 - } 71 - } else { 72 - div { class: "loading", "Loading..." } 66 + } 67 + }, 68 + _ => rsx! { div { class: "loading", "Loading..." } } 73 69 } 74 70 } 75 71 }
+17 -2615
crates/weaver-app/src/views/record.rs
··· 1 1 use crate::Route; 2 2 use crate::auth::AuthState; 3 - use crate::components::accordion::{Accordion, AccordionContent, AccordionItem, AccordionTrigger}; 4 - use crate::components::dialog::{DialogContent, DialogDescription, DialogRoot, DialogTitle}; 5 - use crate::fetch::CachedFetcher; 6 - use dioxus::prelude::Event; 7 - use dioxus::{CapturedError, prelude::*}; 8 - use humansize::format_size; 9 - use jacquard::api::com_atproto::repo::get_record::GetRecordOutput; 10 - use jacquard::bytes::Bytes; 11 - use jacquard::client::AgentError; 3 + use crate::components::record_editor::EditableRecordContent; 4 + use crate::components::record_view::{ 5 + CodeView, PrettyRecordView, RecordViewLayout, SchemaView, ViewMode, 6 + }; 7 + use crate::fetch::Fetcher; 8 + use dioxus::prelude::*; 12 9 use jacquard::common::to_data; 13 10 use jacquard::smol_str::ToSmolStr; 14 - use jacquard::types::LexiconStringType; 15 - use jacquard::types::string::AtprotoStr; 16 - use jacquard::{atproto, prelude::*}; 17 11 use jacquard::{ 18 12 client::AgentSessionExt, 19 - common::{Data, IntoStatic}, 13 + common::IntoStatic, 20 14 identity::lexicon_resolver::LexiconSchemaResolver, 21 - types::{aturi::AtUri, cid::Cid, ident::AtIdentifier, string::Nsid}, 15 + types::{aturi::AtUri, ident::AtIdentifier, string::Nsid}, 22 16 }; 23 17 use jacquard_lexicon::lexicon::LexiconDoc; 24 - use jacquard_lexicon::validation::{ 25 - ConstraintError, StructuralError, ValidationError, ValidationPath, ValidationResult, 26 - }; 27 - use mime_sniffer::MimeTypeSniffer; 28 - use weaver_api::com_atproto::repo::{ 29 - create_record::CreateRecord, delete_record::DeleteRecord, put_record::PutRecord, 30 - }; 31 - use weaver_renderer::{code_pretty::highlight_code, css::generate_default_css}; 32 - 33 - #[derive(Clone, Copy, PartialEq)] 34 - enum ViewMode { 35 - Pretty, 36 - Json, 37 - Schema, 38 - } 39 18 40 19 #[component] 41 20 pub fn RecordIndex() -> Element { ··· 78 57 } 79 58 80 59 #[component] 60 + pub fn RecordPage(uri: ReadSignal<Vec<String>>) -> Element { 61 + rsx! { 62 + {std::iter::once(rsx! {RecordView {uri}})} 63 + } 64 + } 65 + 66 + #[component] 81 67 pub fn RecordView(uri: ReadSignal<Vec<String>>) -> Element { 82 - let fetcher = use_context::<CachedFetcher>(); 68 + let fetcher = use_context::<Fetcher>(); 83 69 info!("Uri:{:?}", uri().join("/")); 84 70 let at_uri = AtUri::new_owned(&*uri.read().join("/")); 85 71 if at_uri.is_err() { ··· 109 95 110 96 // Helper to recursively resolve a schema and its refs 111 97 fn resolve_schema_with_refs<'a>( 112 - fetcher: &'a CachedFetcher, 98 + fetcher: &'a Fetcher, 113 99 type_str: &'a str, 114 100 validator: &'a jacquard_lexicon::validation::SchemaValidator, 115 101 resolved: &'a mut std::collections::HashSet<String>, ··· 278 264 rsx! {} 279 265 } 280 266 } 281 - 282 - #[component] 283 - fn PrettyRecordView( 284 - record: Data<'static>, 285 - uri: AtUri<'static>, 286 - schema: ReadSignal<Option<LexiconDoc<'static>>>, 287 - ) -> Element { 288 - let did = uri.authority().to_string(); 289 - let root_data = use_signal(|| record.clone()); 290 - 291 - // Validate the record and provide via context - only after schema is loaded 292 - let mut validation_result = use_signal(|| None); 293 - use_effect(move || { 294 - // Wait for schema to be loaded 295 - if schema().is_some() { 296 - if let Some(nsid_str) = root_data.read().type_discriminator() { 297 - let validator = jacquard_lexicon::validation::SchemaValidator::global(); 298 - let result = validator.validate_by_nsid(nsid_str, &*root_data.read()); 299 - validation_result.set(Some(result)); 300 - } 301 - } 302 - }); 303 - use_context_provider(|| validation_result); 304 - 305 - rsx! { 306 - div { 307 - class: "pretty-record", 308 - DataView { data: record, root_data, path: String::new(), did } 309 - } 310 - } 311 - } 312 - 313 - #[component] 314 - fn SchemaView(schema: ReadSignal<Option<LexiconDoc<'static>>>) -> Element { 315 - if let Some(schema_doc) = schema() { 316 - // Convert LexiconDoc to Data for display 317 - let schema_data = use_memo(move || to_data(&schema_doc).ok().map(|d| d.into_static())); 318 - 319 - if let Some(data) = schema_data() { 320 - let root_data = use_signal(|| data.clone()); 321 - rsx! { 322 - div { 323 - class: "pretty-record", 324 - DataView { data: data, root_data, path: String::new(), did: String::new() } 325 - } 326 - } 327 - } else { 328 - rsx! { 329 - div { class: "schema-error", "Failed to convert schema to displayable format" } 330 - } 331 - } 332 - } else { 333 - rsx! { 334 - div { class: "schema-loading", "Loading schema..." } 335 - } 336 - } 337 - } 338 - fn get_hex_rep(byte_array: &mut [u8]) -> String { 339 - let build_string_vec: Vec<String> = byte_array 340 - .chunks(2) 341 - .enumerate() 342 - .map(|(i, c)| { 343 - let sep = if i % 16 == 0 && i > 0 { 344 - "\n" 345 - } else if i == 0 { 346 - "" 347 - } else { 348 - " " 349 - }; 350 - if c.len() == 2 { 351 - format!("{}{:02x}{:02x}", sep, c[0], c[1]) 352 - } else { 353 - format!("{}{:02x}", sep, c[0]) 354 - } 355 - }) 356 - .collect(); 357 - build_string_vec.join("") 358 - } 359 - 360 - #[component] 361 - fn PathLabel(path: String) -> Element { 362 - if path.is_empty() { 363 - return rsx! {}; 364 - } 365 - 366 - // Find the last separator 367 - let last_sep = path.rfind(|c| c == '.'); 368 - 369 - if let Some(idx) = last_sep { 370 - let prefix = &path[..idx + 1]; 371 - let final_segment = &path[idx + 1..]; 372 - rsx! { 373 - span { class: "field-label", 374 - span { class: "path-prefix", "{prefix}" } 375 - span { class: "path-final", "{final_segment}" } 376 - } 377 - } 378 - } else { 379 - rsx! { 380 - span { class: "field-label","{path}" } 381 - } 382 - } 383 - } 384 - 385 - /// Get expected string format from schema by navigating path 386 - fn get_expected_string_format( 387 - root_data: &Data<'_>, 388 - current_path: &str, 389 - ) -> Option<jacquard_lexicon::lexicon::LexStringFormat> { 390 - use jacquard_lexicon::lexicon::*; 391 - 392 - // Get root type discriminator 393 - let root_nsid = root_data.type_discriminator()?; 394 - 395 - // Look up schema in global registry 396 - let validator = jacquard_lexicon::validation::SchemaValidator::global(); 397 - let registry = validator.registry(); 398 - let schema = registry.get(root_nsid)?; 399 - 400 - // Navigate to the property at this path 401 - let segments: Vec<&str> = if current_path.is_empty() { 402 - vec![] 403 - } else { 404 - current_path.split('.').collect() 405 - }; 406 - 407 - // Start with the record's main object definition 408 - let main_obj = match schema.defs.get("main")? { 409 - LexUserType::Record(rec) => match &rec.record { 410 - LexRecordRecord::Object(obj) => obj, 411 - }, 412 - _ => return None, 413 - }; 414 - 415 - // Track current position in schema 416 - enum SchemaType<'a> { 417 - ObjectProp(&'a LexObjectProperty<'a>), 418 - ArrayItem(&'a LexArrayItem<'a>), 419 - } 420 - 421 - let mut current_type: Option<SchemaType> = None; 422 - let mut current_obj = Some(main_obj); 423 - 424 - for segment in segments { 425 - // Handle array indices - strip them to get field name 426 - let field_name = segment.trim_end_matches(|c: char| c.is_numeric() || c == '[' || c == ']'); 427 - 428 - if field_name.is_empty() { 429 - continue; // Pure array index like [0], skip 430 - } 431 - 432 - if let Some(obj) = current_obj.take() { 433 - if let Some(prop) = obj.properties.get(field_name) { 434 - current_type = Some(SchemaType::ObjectProp(prop)); 435 - } 436 - } 437 - 438 - // Process current type 439 - match current_type { 440 - Some(SchemaType::ObjectProp(LexObjectProperty::Array(arr))) => { 441 - // Array - unwrap to item type 442 - current_type = Some(SchemaType::ArrayItem(&arr.items)); 443 - } 444 - Some(SchemaType::ObjectProp(LexObjectProperty::Object(obj))) => { 445 - // Nested object - descend into it 446 - current_obj = Some(obj); 447 - current_type = None; 448 - } 449 - Some(SchemaType::ArrayItem(LexArrayItem::Object(obj))) => { 450 - // Array of objects - descend into object 451 - current_obj = Some(obj); 452 - current_type = None; 453 - } 454 - _ => {} 455 - } 456 - } 457 - 458 - // Check if final type is a string with format 459 - match current_type? { 460 - SchemaType::ObjectProp(LexObjectProperty::String(lex_string)) => lex_string.format, 461 - SchemaType::ArrayItem(LexArrayItem::String(lex_string)) => lex_string.format, 462 - _ => None, 463 - } 464 - } 465 - 466 - #[component] 467 - fn DataView( 468 - data: Data<'static>, 469 - root_data: ReadSignal<Data<'static>>, 470 - path: String, 471 - did: String, 472 - ) -> Element { 473 - // Try to get validation result from context and get errors exactly at this path 474 - let validation_result = try_use_context::<Signal<Option<ValidationResult>>>(); 475 - 476 - let errors = if let Some(vr_signal) = validation_result { 477 - get_errors_at_exact_path(&*vr_signal.read(), &path) 478 - } else { 479 - Vec::new() 480 - }; 481 - 482 - let has_errors = !errors.is_empty(); 483 - 484 - match &data { 485 - Data::Null => rsx! { 486 - div { class: if has_errors { "record-field field-error" } else { "record-field" }, 487 - PathLabel { path: path.clone() } 488 - span { class: "field-value muted", "null" } 489 - if has_errors { 490 - for error in &errors { 491 - div { class: "field-error-message", "{error}" } 492 - } 493 - } 494 - } 495 - }, 496 - Data::Boolean(b) => rsx! { 497 - div { class: if has_errors { "record-field field-error" } else { "record-field" }, 498 - PathLabel { path: path.clone() } 499 - span { class: "field-value", "{b}" } 500 - if has_errors { 501 - for error in &errors { 502 - div { class: "field-error-message", "{error}" } 503 - } 504 - } 505 - } 506 - }, 507 - Data::Integer(i) => rsx! { 508 - div { class: if has_errors { "record-field field-error" } else { "record-field" }, 509 - PathLabel { path: path.clone() } 510 - span { class: "field-value", "{i}" } 511 - if has_errors { 512 - for error in &errors { 513 - div { class: "field-error-message", "{error}" } 514 - } 515 - } 516 - } 517 - }, 518 - Data::String(s) => { 519 - use jacquard::types::string::AtprotoStr; 520 - use jacquard_lexicon::lexicon::LexStringFormat; 521 - 522 - // Get expected format from schema 523 - let expected_format = get_expected_string_format(&*root_data.read(), &path); 524 - 525 - // Get actual type from data 526 - let actual_type_label = match s { 527 - AtprotoStr::Datetime(_) => "datetime", 528 - AtprotoStr::Language(_) => "language", 529 - AtprotoStr::Tid(_) => "tid", 530 - AtprotoStr::Nsid(_) => "nsid", 531 - AtprotoStr::Did(_) => "did", 532 - AtprotoStr::Handle(_) => "handle", 533 - AtprotoStr::AtIdentifier(_) => "at-identifier", 534 - AtprotoStr::AtUri(_) => "at-uri", 535 - AtprotoStr::Uri(_) => "uri", 536 - AtprotoStr::Cid(_) => "cid", 537 - AtprotoStr::RecordKey(_) => "record-key", 538 - AtprotoStr::String(_) => "string", 539 - }; 540 - 541 - // Prefer schema format if available, otherwise use actual type 542 - let type_label = if let Some(fmt) = expected_format { 543 - match fmt { 544 - LexStringFormat::Datetime => "datetime", 545 - LexStringFormat::Uri => "uri", 546 - LexStringFormat::AtUri => "at-uri", 547 - LexStringFormat::Did => "did", 548 - LexStringFormat::Handle => "handle", 549 - LexStringFormat::AtIdentifier => "at-identifier", 550 - LexStringFormat::Nsid => "nsid", 551 - LexStringFormat::Cid => "cid", 552 - LexStringFormat::Language => "language", 553 - LexStringFormat::Tid => "tid", 554 - LexStringFormat::RecordKey => "record-key", 555 - } 556 - } else { 557 - actual_type_label 558 - }; 559 - 560 - rsx! { 561 - div { class: if has_errors { "record-field field-error" } else { "record-field" }, 562 - PathLabel { path: path.clone() } 563 - span { class: "field-value", 564 - 565 - HighlightedString { string_type: s.clone() } 566 - if type_label != "string" { 567 - span { class: "string-type-tag", " [{type_label}]" } 568 - } 569 - } 570 - if has_errors { 571 - for error in &errors { 572 - div { class: "field-error-message", "{error}" } 573 - } 574 - } 575 - } 576 - } 577 - } 578 - Data::Bytes(b) => { 579 - let hex_string = get_hex_rep(&mut b.to_vec()); 580 - let byte_size = if b.len() > 128 { 581 - format_size(b.len(), humansize::BINARY) 582 - } else { 583 - format!("{} bytes", b.len()) 584 - }; 585 - rsx! { 586 - div { class: if has_errors { "record-field field-error" } else { "record-field" }, 587 - PathLabel { path: path.clone() } 588 - pre { class: "field-value bytes", "{hex_string} [{byte_size}]" } 589 - if has_errors { 590 - for error in &errors { 591 - div { class: "field-error-message", "{error}" } 592 - } 593 - } 594 - } 595 - } 596 - } 597 - Data::CidLink(cid) => rsx! { 598 - div { class: if has_errors { "record-field field-error" } else { "record-field" }, 599 - span { class: "field-label", "{path}" } 600 - span { class: "field-value", "{cid}" } 601 - if has_errors { 602 - for error in &errors { 603 - div { class: "field-error-message", "{error}" } 604 - } 605 - } 606 - } 607 - }, 608 - Data::Array(arr) => { 609 - let label = path.split('.').last().unwrap_or(&path); 610 - rsx! { 611 - div { class: "record-section", 612 - Accordion { 613 - id: "array-{path}", 614 - collapsible: true, 615 - AccordionItem { 616 - default_open: true, 617 - index: 0, 618 - AccordionTrigger { 619 - div { class: "section-label", "{label}" span { class: "array-len", "[{arr.len()}]" } } 620 - } 621 - AccordionContent { 622 - if has_errors { 623 - for error in &errors { 624 - div { class: "field-error-message", "{error}" } 625 - } 626 - } 627 - div { class: "section-content", 628 - for (idx, item) in arr.iter().enumerate() { 629 - { 630 - let item_path = format!("{}[{}]", label, idx); 631 - let is_object = matches!(item, Data::Object(_)); 632 - 633 - if is_object { 634 - rsx! { 635 - div { 636 - class: "array-item", 637 - div { class: "record-section", 638 - div { class: "section-label", "{item_path}" } 639 - div { class: "section-content", 640 - DataView { 641 - data: item.clone(), 642 - root_data, 643 - path: item_path.clone(), 644 - did: did.clone() 645 - } 646 - } 647 - } 648 - } 649 - } 650 - } else { 651 - rsx! { 652 - div { 653 - class: "array-item", 654 - DataView { 655 - data: item.clone(), 656 - root_data, 657 - path: item_path, 658 - did: did.clone() 659 - } 660 - } 661 - } 662 - } 663 - } 664 - } 665 - } 666 - } 667 - } 668 - } 669 - } 670 - } 671 - } 672 - Data::Object(obj) => { 673 - let is_root = path.is_empty(); 674 - let is_array_item = path.split('.').last().unwrap_or(&path).contains('['); 675 - 676 - if is_root || is_array_item { 677 - // Root object or array item: just render children (array items already wrapped) 678 - rsx! { 679 - div { class: if !is_root { "record-section" } else {""}, 680 - if has_errors { 681 - for error in &errors { 682 - div { class: "field-error-message", "{error}" } 683 - } 684 - } 685 - for (key, value) in obj.iter() { 686 - { 687 - let new_path = if is_root { 688 - key.to_string() 689 - } else { 690 - format!("{}.{}", path, key) 691 - }; 692 - let did_clone = did.clone(); 693 - rsx! { 694 - DataView { data: value.clone(), root_data, path: new_path, did: did_clone } 695 - } 696 - } 697 - } 698 - } 699 - } 700 - } else { 701 - // Nested object (not array item): wrap in section 702 - let label = path.split('.').last().unwrap_or(&path); 703 - rsx! { 704 - div { class: "record-section", 705 - Accordion { 706 - id: "object-{path}", 707 - collapsible: true, 708 - AccordionItem { 709 - default_open: true, 710 - index: 0, 711 - AccordionTrigger { 712 - div { class: "section-label", "{label}" } 713 - } 714 - AccordionContent { 715 - if has_errors { 716 - for error in &errors { 717 - div { class: "field-error-message", "{error}" } 718 - } 719 - } 720 - div { class: "section-content", 721 - for (key, value) in obj.iter() { 722 - { 723 - let new_path = format!("{}.{}", path, key); 724 - let did_clone = did.clone(); 725 - rsx! { 726 - DataView { data: value.clone(), root_data, path: new_path, did: did_clone } 727 - } 728 - } 729 - } 730 - } 731 - } 732 - } 733 - } 734 - } 735 - } 736 - } 737 - } 738 - Data::Blob(blob) => { 739 - let is_image = blob.mime_type.starts_with("image/"); 740 - let format = blob.mime_type.strip_prefix("image/").unwrap_or("jpeg"); 741 - let image_url = format!( 742 - "https://cdn.bsky.app/img/feed_fullsize/plain/{}/{}@{}", 743 - did, 744 - blob.cid(), 745 - format 746 - ); 747 - 748 - let blob_size = format_size(blob.size, humansize::BINARY); 749 - rsx! { 750 - div { class: "record-field", 751 - span { class: "field-label", "{path}" } 752 - span { class: "field-value mime", "[mimeType: {blob.mime_type}, size: {blob_size}]" } 753 - if is_image { 754 - img { 755 - src: "{image_url}", 756 - alt: "Blob image", 757 - class: "blob-image", 758 - } 759 - } 760 - } 761 - } 762 - } 763 - } 764 - } 765 - 766 - #[component] 767 - fn HighlightedUri(uri: AtUri<'static>) -> Element { 768 - let s = uri.as_str(); 769 - let link = format!("{}/record/{}", crate::env::WEAVER_APP_DOMAIN, s); 770 - 771 - if let Some(rest) = s.strip_prefix("at://") { 772 - let parts: Vec<&str> = rest.splitn(3, '/').collect(); 773 - return rsx! { 774 - a { 775 - href: link, 776 - class: "uri-link", 777 - span { class: "string-at-uri", 778 - span { class: "aturi-scheme", "at://" } 779 - span { class: "aturi-authority", "{uri.authority()}" } 780 - 781 - if parts.len() > 1 { 782 - span { class: "aturi-slash", "/" } 783 - if let Some(collection) = uri.collection() { 784 - span { class: "aturi-collection", "{collection.as_ref()}" } 785 - } 786 - } 787 - if parts.len() > 2 { 788 - span { class: "aturi-slash", "/" } 789 - if let Some(rkey) = uri.rkey() { 790 - span { class: "aturi-rkey", "{rkey.as_ref()}" } 791 - } 792 - } 793 - } 794 - } 795 - }; 796 - } 797 - 798 - rsx! { a { class: "string-at-uri", href: s } } 799 - } 800 - 801 - #[component] 802 - fn HighlightedString(string_type: AtprotoStr<'static>) -> Element { 803 - use jacquard::types::string::AtprotoStr; 804 - 805 - match &string_type { 806 - AtprotoStr::Nsid(nsid) => { 807 - let parts: Vec<&str> = nsid.as_str().split('.').collect(); 808 - rsx! { 809 - span { class: "string-nsid", 810 - for (i, part) in parts.iter().enumerate() { 811 - span { class: "nsid-segment nsid-segment-{i % 3}", "{part}" } 812 - if i < parts.len() - 1 { 813 - span { class: "nsid-dot", "." } 814 - } 815 - } 816 - } 817 - } 818 - } 819 - AtprotoStr::Did(did) => { 820 - let s = did.as_str(); 821 - if let Some(rest) = s.strip_prefix("did:") { 822 - if let Some((method, identifier)) = rest.split_once(':') { 823 - return rsx! { 824 - span { class: "string-did", 825 - span { class: "did-scheme", "did:" } 826 - span { class: "did-method", "{method}" } 827 - span { class: "did-separator", ":" } 828 - span { class: "did-identifier", "{identifier}" } 829 - } 830 - }; 831 - } 832 - } 833 - rsx! { span { class: "string-did", "{s}" } } 834 - } 835 - AtprotoStr::Handle(handle) => { 836 - let parts: Vec<&str> = handle.as_str().split('.').collect(); 837 - rsx! { 838 - span { class: "string-handle", 839 - for (i, part) in parts.iter().enumerate() { 840 - span { class: "handle-segment handle-segment-{i % 2}", "{part}" } 841 - if i < parts.len() - 1 { 842 - span { class: "handle-dot", "." } 843 - } 844 - } 845 - } 846 - } 847 - } 848 - AtprotoStr::AtUri(uri) => { 849 - rsx! { 850 - HighlightedUri { uri: uri.clone().into_static() } 851 - } 852 - } 853 - AtprotoStr::Uri(uri) => { 854 - let s = uri.as_str(); 855 - if let Ok(at_uri) = AtUri::new(s) { 856 - return rsx! { 857 - HighlightedUri { uri: at_uri.into_static() } 858 - }; 859 - } 860 - 861 - // Try to parse scheme 862 - if let Some((scheme, rest)) = s.split_once("://") { 863 - // Split authority and path 864 - let (authority, path) = if let Some(idx) = rest.find('/') { 865 - (&rest[..idx], &rest[idx..]) 866 - } else { 867 - (rest, "") 868 - }; 869 - 870 - return rsx! { 871 - a { 872 - href: "{s}", 873 - target: "_blank", 874 - rel: "noopener noreferrer", 875 - class: "uri-link", 876 - span { class: "string-uri", 877 - span { class: "uri-scheme", "{scheme}" } 878 - span { class: "uri-separator", "://" } 879 - span { class: "uri-authority", "{authority}" } 880 - if !path.is_empty() { 881 - span { class: "uri-path", "{path}" } 882 - } 883 - } 884 - } 885 - }; 886 - } 887 - 888 - rsx! { span { class: "string-uri", "{s}" } } 889 - } 890 - _ => { 891 - let value = string_type.as_str(); 892 - rsx! { "{value}" } 893 - } 894 - } 895 - } 896 - 897 - #[derive(Props, Clone, PartialEq)] 898 - pub struct CodeViewProps { 899 - #[props(default)] 900 - id: Signal<String>, 901 - #[props(default)] 902 - class: Signal<String>, 903 - code: ReadSignal<String>, 904 - lang: Option<String>, 905 - } 906 - 907 - #[component] 908 - fn JsonEditor( 909 - data: Signal<Data<'static>>, 910 - nsid: ReadSignal<Option<String>>, 911 - schema: ReadSignal<Option<LexiconDoc<'static>>>, 912 - ) -> Element { 913 - let mut json_text = 914 - use_signal(|| serde_json::to_string_pretty(&*data.read()).unwrap_or_default()); 915 - 916 - let height = use_memo(move || { 917 - let line_count = json_text().lines().count(); 918 - let min_lines = 10; 919 - let lines = line_count.max(min_lines); 920 - // line-height is 1.5, font-size is 0.9rem (approx 14.4px), so each line is ~21.6px 921 - // Add padding (1rem top + 1rem bottom = 2rem = 32px) 922 - format!("{}px", lines * 22 + 32) 923 - }); 924 - 925 - let validation = use_resource(move || { 926 - let text = json_text(); 927 - let nsid_val = nsid(); 928 - let _ = schema(); // Track schema changes 929 - 930 - async move { 931 - // Only validate if we have an NSID 932 - let nsid_str = nsid_val?; 933 - 934 - // Parse JSON to Data 935 - let parsed = match serde_json::from_str::<Data>(&text) { 936 - Ok(val) => val.into_static(), 937 - Err(e) => { 938 - return Some((None, Some(e.to_string()))); 939 - } 940 - }; 941 - 942 - // Use global validator (schema already registered) 943 - let validator = jacquard_lexicon::validation::SchemaValidator::global(); 944 - let result = validator.validate_by_nsid(&nsid_str, &parsed); 945 - 946 - Some((Some(result), None)) 947 - } 948 - }); 949 - 950 - rsx! { 951 - div { class: "json-editor", 952 - textarea { 953 - class: "json-textarea", 954 - style: "height: {height};", 955 - value: "{json_text}", 956 - oninput: move |evt| { 957 - json_text.set(evt.value()); 958 - // Update data signal on successful parse 959 - if let Ok(parsed) = serde_json::from_str::<Data>(&evt.value()) { 960 - data.set(parsed.into_static()); 961 - } 962 - }, 963 - } 964 - 965 - ValidationPanel { 966 - validation: validation, 967 - } 968 - } 969 - } 970 - } 971 - 972 - #[component] 973 - fn ActionButtons( 974 - on_update: EventHandler<()>, 975 - on_save_new: EventHandler<()>, 976 - on_replace: EventHandler<()>, 977 - on_delete: EventHandler<()>, 978 - on_cancel: EventHandler<()>, 979 - ) -> Element { 980 - let mut show_save_dropdown = use_signal(|| false); 981 - let mut show_replace_warning = use_signal(|| false); 982 - let mut show_delete_confirm = use_signal(|| false); 983 - 984 - rsx! { 985 - div { class: "action-buttons-group", 986 - button { 987 - class: "tab-button action-button", 988 - onclick: move |_| on_update.call(()), 989 - "Update" 990 - } 991 - 992 - div { class: "dropdown-wrapper", 993 - button { 994 - class: "tab-button action-button", 995 - onclick: move |_| show_save_dropdown.toggle(), 996 - "Save as New ▼" 997 - } 998 - if show_save_dropdown() { 999 - div { class: "dropdown-menu", 1000 - button { 1001 - onclick: move |_| { 1002 - show_save_dropdown.set(false); 1003 - on_save_new.call(()); 1004 - }, 1005 - "Save as New" 1006 - } 1007 - button { 1008 - onclick: move |_| { 1009 - show_save_dropdown.set(false); 1010 - show_replace_warning.set(true); 1011 - }, 1012 - "Replace" 1013 - } 1014 - } 1015 - } 1016 - } 1017 - 1018 - if show_replace_warning() { 1019 - div { class: "inline-warning", 1020 - "⚠️ This will delete the current record and create a new one with a different rkey. " 1021 - button { 1022 - onclick: move |_| { 1023 - show_replace_warning.set(false); 1024 - on_replace.call(()); 1025 - }, 1026 - "Yes" 1027 - } 1028 - button { 1029 - onclick: move |_| show_replace_warning.set(false), 1030 - "No" 1031 - } 1032 - } 1033 - } 1034 - 1035 - button { 1036 - class: "tab-button action-button action-button-danger", 1037 - onclick: move |_| show_delete_confirm.set(true), 1038 - "Delete" 1039 - } 1040 - 1041 - DialogRoot { 1042 - open: Some(show_delete_confirm()), 1043 - on_open_change: move |open: bool| { 1044 - show_delete_confirm.set(open); 1045 - }, 1046 - DialogContent { 1047 - DialogTitle { "Delete Record?" } 1048 - DialogDescription { 1049 - "This action cannot be undone." 1050 - } 1051 - div { class: "dialog-actions", 1052 - button { 1053 - onclick: move |_| { 1054 - show_delete_confirm.set(false); 1055 - on_delete.call(()); 1056 - }, 1057 - "Delete" 1058 - } 1059 - button { 1060 - onclick: move |_| show_delete_confirm.set(false), 1061 - "Cancel" 1062 - } 1063 - } 1064 - } 1065 - } 1066 - 1067 - button { 1068 - class: "tab-button action-button", 1069 - onclick: move |_| on_cancel.call(()), 1070 - "Cancel" 1071 - } 1072 - } 1073 - } 1074 - } 1075 - 1076 - #[component] 1077 - fn ValidationPanel( 1078 - validation: Resource<Option<(Option<ValidationResult>, Option<String>)>>, 1079 - ) -> Element { 1080 - rsx! { 1081 - div { class: "validation-panel", 1082 - if let Some(Some((result_opt, parse_error_opt))) = validation.read().as_ref() { 1083 - if let Some(parse_err) = parse_error_opt { 1084 - div { class: "parse-error", 1085 - "❌ Invalid JSON: {parse_err}" 1086 - } 1087 - } 1088 - 1089 - if let Some(result) = result_opt { 1090 - // Structural validity 1091 - if result.is_structurally_valid() { 1092 - div { class: "validation-success", "✓ Structurally valid" } 1093 - } else { 1094 - div { class: "parse-error", "❌ Structurally invalid" } 1095 - } 1096 - 1097 - // Overall validity 1098 - if result.is_valid() { 1099 - div { class: "validation-success", "✓ Fully valid" } 1100 - } else { 1101 - div { class: "validation-warning", "⚠ Has errors" } 1102 - } 1103 - 1104 - // Show errors if any 1105 - if !result.is_valid() { 1106 - div { class: "validation-errors", 1107 - h4 { "Validation Errors:" } 1108 - for error in result.all_errors() { 1109 - div { class: "error", "{error}" } 1110 - } 1111 - } 1112 - } 1113 - } 1114 - } else { 1115 - div { "Validating..." } 1116 - } 1117 - } 1118 - } 1119 - } 1120 - 1121 - // ============================================================================ 1122 - // Validation Helper Functions 1123 - // ============================================================================ 1124 - 1125 - /// Parse UI path into segments (fields and indices only) 1126 - fn parse_ui_path(ui_path: &str) -> Vec<UiPathSegment> { 1127 - if ui_path.is_empty() { 1128 - return vec![]; 1129 - } 1130 - 1131 - let mut segments = Vec::new(); 1132 - let mut current = String::new(); 1133 - 1134 - for ch in ui_path.chars() { 1135 - match ch { 1136 - '.' => { 1137 - if !current.is_empty() { 1138 - segments.push(UiPathSegment::Field(current.clone())); 1139 - current.clear(); 1140 - } 1141 - } 1142 - '[' => { 1143 - if !current.is_empty() { 1144 - segments.push(UiPathSegment::Field(current.clone())); 1145 - current.clear(); 1146 - } 1147 - } 1148 - ']' => { 1149 - if !current.is_empty() { 1150 - if let Ok(idx) = current.parse::<usize>() { 1151 - segments.push(UiPathSegment::Index(idx)); 1152 - } 1153 - current.clear(); 1154 - } 1155 - } 1156 - c => current.push(c), 1157 - } 1158 - } 1159 - 1160 - if !current.is_empty() { 1161 - segments.push(UiPathSegment::Field(current)); 1162 - } 1163 - 1164 - segments 1165 - } 1166 - 1167 - #[derive(Debug, PartialEq)] 1168 - enum UiPathSegment { 1169 - Field(String), 1170 - Index(usize), 1171 - } 1172 - 1173 - /// Get all validation errors at exactly this path (not children) 1174 - fn get_errors_at_exact_path( 1175 - validation_result: &Option<ValidationResult>, 1176 - ui_path: &str, 1177 - ) -> Vec<String> { 1178 - use jacquard_lexicon::validation::PathSegment; 1179 - 1180 - if let Some(result) = validation_result { 1181 - let ui_segments = parse_ui_path(ui_path); 1182 - 1183 - result 1184 - .all_errors() 1185 - .filter_map(|err| { 1186 - let validation_path = match &err { 1187 - ValidationError::Structural(s) => match s { 1188 - StructuralError::TypeMismatch { path, .. } => Some(path), 1189 - StructuralError::MissingRequiredField { path, .. } => Some(path), 1190 - StructuralError::MissingUnionDiscriminator { path } => Some(path), 1191 - StructuralError::UnionNoMatch { path, .. } => Some(path), 1192 - StructuralError::UnresolvedRef { path, .. } => Some(path), 1193 - StructuralError::RefCycle { path, .. } => Some(path), 1194 - StructuralError::MaxDepthExceeded { path, .. } => Some(path), 1195 - }, 1196 - ValidationError::Constraint(c) => match c { 1197 - ConstraintError::MaxLength { path, .. } => Some(path), 1198 - ConstraintError::MaxGraphemes { path, .. } => Some(path), 1199 - ConstraintError::MinLength { path, .. } => Some(path), 1200 - ConstraintError::MinGraphemes { path, .. } => Some(path), 1201 - ConstraintError::Maximum { path, .. } => Some(path), 1202 - ConstraintError::Minimum { path, .. } => Some(path), 1203 - }, 1204 - }; 1205 - 1206 - if let Some(path) = validation_path { 1207 - // Convert validation path to UI segments 1208 - let validation_ui_segments: Vec<_> = path 1209 - .segments() 1210 - .iter() 1211 - .filter_map(|seg| match seg { 1212 - PathSegment::Field(name) => { 1213 - Some(UiPathSegment::Field(name.to_string())) 1214 - } 1215 - PathSegment::Index(idx) => Some(UiPathSegment::Index(*idx)), 1216 - PathSegment::UnionVariant(_) => None, 1217 - }) 1218 - .collect(); 1219 - 1220 - // Exact match only 1221 - if validation_ui_segments == ui_segments { 1222 - return Some(err.to_string()); 1223 - } 1224 - } 1225 - None 1226 - }) 1227 - .collect() 1228 - } else { 1229 - Vec::new() 1230 - } 1231 - } 1232 - 1233 - // ============================================================================ 1234 - // Pretty Editor: Helper Functions 1235 - // ============================================================================ 1236 - 1237 - /// Infer Data type from text input 1238 - fn infer_data_from_text(text: &str) -> Result<Data<'static>, String> { 1239 - let trimmed = text.trim(); 1240 - 1241 - if trimmed == "true" || trimmed == "false" { 1242 - Ok(Data::Boolean(trimmed == "true")) 1243 - } else if trimmed == "{}" { 1244 - use jacquard::types::value::Object; 1245 - use std::collections::BTreeMap; 1246 - Ok(Data::Object(Object(BTreeMap::new()))) 1247 - } else if trimmed == "[]" { 1248 - use jacquard::types::value::Array; 1249 - Ok(Data::Array(Array(Vec::new()))) 1250 - } else if trimmed == "null" { 1251 - Ok(Data::Null) 1252 - } else if let Ok(num) = trimmed.parse::<i64>() { 1253 - Ok(Data::Integer(num)) 1254 - } else { 1255 - // Smart string parsing 1256 - use jacquard::types::value::parsing; 1257 - Ok(Data::String(parsing::parse_string(trimmed).into_static())) 1258 - } 1259 - } 1260 - 1261 - /// Parse text as specific AtprotoStr type, preserving type information 1262 - fn try_parse_as_type( 1263 - text: &str, 1264 - string_type: LexiconStringType, 1265 - ) -> Result<AtprotoStr<'static>, String> { 1266 - use jacquard::types::string::*; 1267 - use std::str::FromStr; 1268 - 1269 - match string_type { 1270 - LexiconStringType::Datetime => Datetime::from_str(text) 1271 - .map(AtprotoStr::Datetime) 1272 - .map_err(|e| format!("Invalid datetime: {}", e)), 1273 - LexiconStringType::Did => Did::new(text) 1274 - .map(|v| AtprotoStr::Did(v.into_static())) 1275 - .map_err(|e| format!("Invalid DID: {}", e)), 1276 - LexiconStringType::Handle => Handle::new(text) 1277 - .map(|v| AtprotoStr::Handle(v.into_static())) 1278 - .map_err(|e| format!("Invalid handle: {}", e)), 1279 - LexiconStringType::AtUri => AtUri::new(text) 1280 - .map(|v| AtprotoStr::AtUri(v.into_static())) 1281 - .map_err(|e| format!("Invalid AT-URI: {}", e)), 1282 - LexiconStringType::AtIdentifier => AtIdentifier::new(text) 1283 - .map(|v| AtprotoStr::AtIdentifier(v.into_static())) 1284 - .map_err(|e| format!("Invalid identifier: {}", e)), 1285 - LexiconStringType::Nsid => Nsid::new(text) 1286 - .map(|v| AtprotoStr::Nsid(v.into_static())) 1287 - .map_err(|e| format!("Invalid NSID: {}", e)), 1288 - LexiconStringType::Tid => Tid::new(text) 1289 - .map(|v| AtprotoStr::Tid(v.into_static())) 1290 - .map_err(|e| format!("Invalid TID: {}", e)), 1291 - LexiconStringType::RecordKey => Rkey::new(text) 1292 - .map(|rk| AtprotoStr::RecordKey(RecordKey::from(rk))) 1293 - .map_err(|e| format!("Invalid record key: {}", e)), 1294 - LexiconStringType::Cid => Cid::new(text.as_bytes()) 1295 - .map(|v| AtprotoStr::Cid(v.into_static())) 1296 - .map_err(|_| "Invalid CID".to_string()), 1297 - LexiconStringType::Language => Language::new(text) 1298 - .map(AtprotoStr::Language) 1299 - .map_err(|e| format!("Invalid language: {}", e)), 1300 - LexiconStringType::Uri(_) => Uri::new(text) 1301 - .map(|u| AtprotoStr::Uri(u.into_static())) 1302 - .map_err(|e| format!("Invalid URI: {}", e)), 1303 - LexiconStringType::String => { 1304 - // Plain strings: use smart inference 1305 - use jacquard::types::value::parsing; 1306 - Ok(parsing::parse_string(text).into_static()) 1307 - } 1308 - } 1309 - } 1310 - 1311 - /// Create default value for new array item by cloning structure of existing items 1312 - fn create_array_item_default(arr: &jacquard::types::value::Array) -> Data<'static> { 1313 - if let Some(existing) = arr.0.first() { 1314 - clone_structure(existing) 1315 - } else { 1316 - // Empty array, default to null (user can change type) 1317 - Data::Null 1318 - } 1319 - } 1320 - 1321 - /// Clone structure of Data, setting sensible defaults for leaf values 1322 - fn clone_structure(data: &Data) -> Data<'static> { 1323 - use jacquard::types::string::*; 1324 - use jacquard::types::value::{Array, Object}; 1325 - use jacquard::types::{LexiconStringType, blob::*}; 1326 - use std::collections::BTreeMap; 1327 - 1328 - match data { 1329 - Data::Object(obj) => { 1330 - let mut new_obj = BTreeMap::new(); 1331 - for (key, value) in obj.0.iter() { 1332 - new_obj.insert(key.clone(), clone_structure(value)); 1333 - } 1334 - Data::Object(Object(new_obj)) 1335 - } 1336 - Data::Array(_) => Data::Array(Array(Vec::new())), 1337 - Data::String(s) => match s.string_type() { 1338 - LexiconStringType::Datetime => { 1339 - // Sensible default: now 1340 - Data::String(AtprotoStr::Datetime(Datetime::now())) 1341 - } 1342 - LexiconStringType::Tid => Data::String(AtprotoStr::Tid(Tid::now_0())), 1343 - _ => { 1344 - // Empty string, type inference will handle it 1345 - Data::String(AtprotoStr::String("".into())) 1346 - } 1347 - }, 1348 - Data::Integer(_) => Data::Integer(0), 1349 - Data::Boolean(_) => Data::Boolean(false), 1350 - Data::Blob(blob) => { 1351 - // Placeholder blob 1352 - Data::Blob( 1353 - Blob { 1354 - r#ref: CidLink::str( 1355 - "bafkreiaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 1356 - ), 1357 - mime_type: blob.mime_type.clone(), 1358 - size: 0, 1359 - } 1360 - .into_static(), 1361 - ) 1362 - } 1363 - Data::CidLink(_) => Data::CidLink(Cid::str( 1364 - "bafkreiaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 1365 - )), 1366 - Data::Bytes(_) => Data::Bytes(Bytes::new()), 1367 - Data::Null => Data::Null, 1368 - } 1369 - } 1370 - 1371 - // ============================================================================ 1372 - // Pretty Editor: Component Hierarchy 1373 - // ============================================================================ 1374 - 1375 - /// Main dispatcher - routes to specific field editors based on Data type 1376 - #[component] 1377 - fn EditableDataView( 1378 - root: Signal<Data<'static>>, 1379 - path: String, 1380 - did: String, 1381 - #[props(default)] remove_button: Option<Element>, 1382 - ) -> Element { 1383 - let path_for_memo = path.clone(); 1384 - let root_read = root.read(); 1385 - 1386 - match root_read 1387 - .get_at_path(&path_for_memo) 1388 - .map(|d| d.clone().into_static()) 1389 - { 1390 - Some(Data::Object(_)) => { 1391 - rsx! { EditableObjectField { root, path: path.clone(), did, remove_button } } 1392 - } 1393 - Some(Data::Array(_)) => rsx! { EditableArrayField { root, path: path.clone(), did } }, 1394 - Some(Data::String(_)) => { 1395 - rsx! { EditableStringField { root, path: path.clone(), remove_button } } 1396 - } 1397 - Some(Data::Integer(_)) => { 1398 - rsx! { EditableIntegerField { root, path: path.clone(), remove_button } } 1399 - } 1400 - Some(Data::Boolean(_)) => { 1401 - rsx! { EditableBooleanField { root, path: path.clone(), remove_button } } 1402 - } 1403 - Some(Data::Null) => rsx! { EditableNullField { root, path: path.clone(), remove_button } }, 1404 - Some(Data::Blob(_)) => { 1405 - rsx! { EditableBlobField { root, path: path.clone(), did, remove_button } } 1406 - } 1407 - Some(Data::Bytes(_)) => { 1408 - rsx! { EditableBytesField { root, path: path.clone(), remove_button } } 1409 - } 1410 - Some(Data::CidLink(_)) => { 1411 - rsx! { EditableCidLinkField { root, path: path.clone(), remove_button } } 1412 - } 1413 - 1414 - None => rsx! { div { class: "field-error", "❌ Path not found: {path}" } }, 1415 - } 1416 - } 1417 - 1418 - // ============================================================================ 1419 - // Primitive Field Editors 1420 - // ============================================================================ 1421 - 1422 - /// String field with type preservation 1423 - #[component] 1424 - fn EditableStringField( 1425 - root: Signal<Data<'static>>, 1426 - path: String, 1427 - #[props(default)] remove_button: Option<Element>, 1428 - ) -> Element { 1429 - use jacquard::types::LexiconStringType; 1430 - 1431 - let path_for_text = path.clone(); 1432 - let path_for_type = path.clone(); 1433 - 1434 - // Get current string value 1435 - let current_text = use_memo(move || { 1436 - root.read() 1437 - .get_at_path(&path_for_text) 1438 - .and_then(|d| d.as_str()) 1439 - .map(|s| s.to_string()) 1440 - .unwrap_or_default() 1441 - }); 1442 - 1443 - // Get string type (Copy, cheap to store) 1444 - let string_type = use_memo(move || { 1445 - root.read() 1446 - .get_at_path(&path_for_type) 1447 - .and_then(|d| match d { 1448 - Data::String(s) => Some(s.string_type()), 1449 - _ => None, 1450 - }) 1451 - .unwrap_or(LexiconStringType::String) 1452 - }); 1453 - 1454 - // Local state for invalid input 1455 - let mut input_text = use_signal(|| current_text()); 1456 - let mut parse_error = use_signal(|| None::<String>); 1457 - 1458 - // Sync input when current changes 1459 - use_effect(move || { 1460 - input_text.set(current_text()); 1461 - }); 1462 - 1463 - let path_for_mutation = path.clone(); 1464 - let handle_input = move |evt: Event<FormData>| { 1465 - let new_text = evt.value(); 1466 - input_text.set(new_text.clone()); 1467 - 1468 - match try_parse_as_type(&new_text, string_type()) { 1469 - Ok(new_atproto_str) => { 1470 - parse_error.set(None); 1471 - let mut new_data = root.read().clone(); 1472 - new_data.set_at_path(&path_for_mutation, Data::String(new_atproto_str)); 1473 - root.set(new_data); 1474 - } 1475 - Err(e) => { 1476 - parse_error.set(Some(e)); 1477 - } 1478 - } 1479 - }; 1480 - 1481 - let type_label = format!("{:?}", string_type()).to_lowercase(); 1482 - let is_plain_string = string_type() == LexiconStringType::String; 1483 - 1484 - // Dynamic width based on content length 1485 - let input_width = use_memo(move || { 1486 - let len = input_text().len(); 1487 - let min_width = match string_type() { 1488 - LexiconStringType::Cid => 60, 1489 - LexiconStringType::Nsid => 40, 1490 - LexiconStringType::Did => 50, 1491 - LexiconStringType::AtUri => 50, 1492 - _ => 20, 1493 - }; 1494 - format!("{}ch", len.max(min_width)) 1495 - }); 1496 - 1497 - rsx! { 1498 - div { class: "record-field", 1499 - div { class: "field-header", 1500 - PathLabel { path: path.clone() } 1501 - if type_label != "string" { 1502 - span { class: "string-type-tag", " [{type_label}]" } 1503 - } 1504 - {remove_button} 1505 - } 1506 - if is_plain_string { 1507 - textarea { 1508 - value: "{input_text}", 1509 - oninput: handle_input, 1510 - class: if parse_error().is_some() { "invalid" } else { "" }, 1511 - rows: "1", 1512 - } 1513 - } else { 1514 - input { 1515 - r#type: "text", 1516 - value: "{input_text}", 1517 - style: "width: {input_width}", 1518 - oninput: handle_input, 1519 - class: if parse_error().is_some() { "invalid" } else { "" }, 1520 - } 1521 - } 1522 - if let Some(err) = parse_error() { 1523 - span { class: "field-error", " ❌ {err}" } 1524 - } 1525 - } 1526 - } 1527 - } 1528 - 1529 - /// Integer field with validation 1530 - #[component] 1531 - fn EditableIntegerField( 1532 - root: Signal<Data<'static>>, 1533 - path: String, 1534 - #[props(default)] remove_button: Option<Element>, 1535 - ) -> Element { 1536 - let path_for_memo = path.clone(); 1537 - let current_value = use_memo(move || { 1538 - root.read() 1539 - .get_at_path(&path_for_memo) 1540 - .and_then(|d| d.as_integer()) 1541 - .unwrap_or(0) 1542 - }); 1543 - 1544 - let mut input_text = use_signal(|| current_value().to_string()); 1545 - let mut parse_error = use_signal(|| None::<String>); 1546 - 1547 - use_effect(move || { 1548 - input_text.set(current_value().to_string()); 1549 - }); 1550 - 1551 - let path_for_mutation = path.clone(); 1552 - 1553 - rsx! { 1554 - div { class: "record-field", 1555 - div { class: "field-header", 1556 - PathLabel { path: path.clone() } 1557 - {remove_button} 1558 - } 1559 - input { 1560 - r#type: "number", 1561 - value: "{input_text}", 1562 - oninput: move |evt| { 1563 - let text = evt.value(); 1564 - input_text.set(text.clone()); 1565 - 1566 - match text.parse::<i64>() { 1567 - Ok(num) => { 1568 - parse_error.set(None); 1569 - let mut data_edit = root.write_unchecked(); 1570 - data_edit.set_at_path(&path_for_mutation, Data::Integer(num)); 1571 - } 1572 - Err(_) => { 1573 - parse_error.set(Some("Must be a valid integer".to_string())); 1574 - } 1575 - } 1576 - } 1577 - } 1578 - if let Some(err) = parse_error() { 1579 - span { class: "field-error", " ❌ {err}" } 1580 - } 1581 - } 1582 - } 1583 - } 1584 - 1585 - /// Boolean field (toggle button) 1586 - #[component] 1587 - fn EditableBooleanField( 1588 - root: Signal<Data<'static>>, 1589 - path: String, 1590 - #[props(default)] remove_button: Option<Element>, 1591 - ) -> Element { 1592 - let path_for_memo = path.clone(); 1593 - let current_value = use_memo(move || { 1594 - root.read() 1595 - .get_at_path(&path_for_memo) 1596 - .and_then(|d| d.as_boolean()) 1597 - .unwrap_or(false) 1598 - }); 1599 - 1600 - let path_for_mutation = path.clone(); 1601 - rsx! { 1602 - div { class: "record-field", 1603 - div { class: "field-header", 1604 - PathLabel { path: path.clone() } 1605 - {remove_button} 1606 - } 1607 - button { 1608 - class: if current_value() { "boolean-toggle boolean-toggle-true" } else { "boolean-toggle boolean-toggle-false" }, 1609 - onclick: move |_| { 1610 - root.with_mut(|data| { 1611 - if let Some(target) = data.get_at_path_mut(path_for_mutation.as_str()) { 1612 - if let Some(bool_val) = target.as_boolean() { 1613 - *target = Data::Boolean(!bool_val); 1614 - } 1615 - } 1616 - }); 1617 - }, 1618 - "{current_value()}" 1619 - } 1620 - } 1621 - } 1622 - } 1623 - 1624 - /// Null field with type inference 1625 - #[component] 1626 - fn EditableNullField( 1627 - root: Signal<Data<'static>>, 1628 - path: String, 1629 - #[props(default)] remove_button: Option<Element>, 1630 - ) -> Element { 1631 - let mut input_text = use_signal(|| String::new()); 1632 - let mut parse_error = use_signal(|| None::<String>); 1633 - 1634 - let path_for_mutation = path.clone(); 1635 - rsx! { 1636 - div { class: "record-field", 1637 - div { class: "field-header", 1638 - PathLabel { path: path.clone() } 1639 - span { class: "field-value muted", "null" } 1640 - {remove_button} 1641 - } 1642 - input { 1643 - r#type: "text", 1644 - placeholder: "Enter value (or {{}}, [], true, 123)...", 1645 - value: "{input_text}", 1646 - oninput: move |evt| { 1647 - input_text.set(evt.value()); 1648 - }, 1649 - onkeydown: move |evt| { 1650 - use dioxus::prelude::keyboard_types::Key; 1651 - if evt.key() == Key::Enter { 1652 - let text = input_text(); 1653 - match infer_data_from_text(&text) { 1654 - Ok(new_value) => { 1655 - root.with_mut(|data| { 1656 - if let Some(target) = data.get_at_path_mut(path_for_mutation.as_str()) { 1657 - *target = new_value; 1658 - } 1659 - }); 1660 - input_text.set(String::new()); 1661 - parse_error.set(None); 1662 - } 1663 - Err(e) => { 1664 - parse_error.set(Some(e)); 1665 - } 1666 - } 1667 - } 1668 - } 1669 - } 1670 - if let Some(err) = parse_error() { 1671 - span { class: "field-error", " ❌ {err}" } 1672 - } 1673 - } 1674 - } 1675 - } 1676 - 1677 - /// Blob field - shows CID, size (editable), mime type (read-only), file upload 1678 - #[component] 1679 - fn EditableBlobField( 1680 - root: Signal<Data<'static>>, 1681 - path: String, 1682 - did: String, 1683 - #[props(default)] remove_button: Option<Element>, 1684 - ) -> Element { 1685 - let path_for_memo = path.clone(); 1686 - let blob_data = use_memo(move || { 1687 - root.read() 1688 - .get_at_path(&path_for_memo) 1689 - .and_then(|d| match d { 1690 - Data::Blob(blob) => Some(( 1691 - blob.r#ref.to_string(), 1692 - blob.size, 1693 - blob.mime_type.as_str().to_string(), 1694 - )), 1695 - _ => None, 1696 - }) 1697 - }); 1698 - 1699 - let mut cid_input = use_signal(|| String::new()); 1700 - let mut size_input = use_signal(|| String::new()); 1701 - let mut cid_error = use_signal(|| None::<String>); 1702 - let mut size_error = use_signal(|| None::<String>); 1703 - let mut uploading = use_signal(|| false); 1704 - let mut upload_error = use_signal(|| None::<String>); 1705 - let mut preview_data_url = use_signal(|| None::<String>); 1706 - 1707 - // Sync inputs when blob data changes 1708 - use_effect(move || { 1709 - if let Some((cid, size, _)) = blob_data() { 1710 - cid_input.set(cid); 1711 - size_input.set(size.to_string()); 1712 - } 1713 - }); 1714 - 1715 - let fetcher = use_context::<CachedFetcher>(); 1716 - let path_for_upload = path.clone(); 1717 - let handle_file = move |evt: Event<dioxus::prelude::FormData>| { 1718 - let fetcher = fetcher.clone(); 1719 - let path_upload_clone = path_for_upload.clone(); 1720 - spawn(async move { 1721 - uploading.set(true); 1722 - upload_error.set(None); 1723 - 1724 - let files = evt.files(); 1725 - for file_data in files { 1726 - match file_data.read_bytes().await { 1727 - Ok(bytes_data) => { 1728 - // Convert to jacquard Bytes and sniff MIME type 1729 - let bytes = jacquard::bytes::Bytes::from(bytes_data.to_vec()); 1730 - let mime_str = bytes 1731 - .sniff_mime_type() 1732 - .unwrap_or("application/octet-stream"); 1733 - let mime_type = jacquard::types::blob::MimeType::new_owned(mime_str); 1734 - 1735 - // Create data URL for immediate preview if it's an image 1736 - if mime_str.starts_with("image/") { 1737 - let base64_data = base64::Engine::encode( 1738 - &base64::engine::general_purpose::STANDARD, 1739 - &bytes, 1740 - ); 1741 - let data_url = format!("data:{};base64,{}", mime_str, base64_data); 1742 - preview_data_url.set(Some(data_url.clone())); 1743 - 1744 - // Try to decode dimensions and populate aspectRatio field 1745 - #[cfg(target_arch = "wasm32")] 1746 - { 1747 - let path_clone = path_upload_clone.clone(); 1748 - spawn(async move { 1749 - if let Some((width, height)) = 1750 - decode_image_dimensions(&data_url).await 1751 - { 1752 - populate_aspect_ratio( 1753 - root, 1754 - &path_clone, 1755 - width as i64, 1756 - height as i64, 1757 - ); 1758 - } 1759 - }); 1760 - } 1761 - } 1762 - 1763 - // Upload blob 1764 - let client = fetcher.get_client(); 1765 - match client.upload_blob(bytes, mime_type).await { 1766 - Ok(new_blob) => { 1767 - // Update blob in record 1768 - let path_ref = path_upload_clone.clone(); 1769 - root.with_mut(|record_data| { 1770 - if let Some(Data::Blob(blob)) = 1771 - record_data.get_at_path_mut(&path_ref) 1772 - { 1773 - *blob = new_blob; 1774 - } 1775 - }); 1776 - upload_error.set(None); 1777 - } 1778 - Err(e) => { 1779 - upload_error.set(Some(format!("Upload failed: {:?}", e))); 1780 - } 1781 - } 1782 - } 1783 - Err(e) => { 1784 - upload_error.set(Some(format!("Failed to read file: {}", e))); 1785 - } 1786 - } 1787 - } 1788 - 1789 - uploading.set(false); 1790 - }); 1791 - }; 1792 - 1793 - let path_for_cid = path.clone(); 1794 - let handle_cid_change = move |evt: Event<dioxus::prelude::FormData>| { 1795 - let text = evt.value(); 1796 - cid_input.set(text.clone()); 1797 - 1798 - match jacquard::types::cid::CidLink::new_owned(text.as_bytes()) { 1799 - Ok(new_cid_link) => { 1800 - cid_error.set(None); 1801 - root.with_mut(|data| { 1802 - if let Some(Data::Blob(blob)) = data.get_at_path_mut(&path_for_cid) { 1803 - blob.r#ref = new_cid_link; 1804 - } 1805 - }); 1806 - } 1807 - Err(_) => { 1808 - cid_error.set(Some("Invalid CID format".to_string())); 1809 - } 1810 - } 1811 - }; 1812 - 1813 - let path_for_size = path.clone(); 1814 - let handle_size_change = move |evt: Event<dioxus::prelude::FormData>| { 1815 - let text = evt.value(); 1816 - size_input.set(text.clone()); 1817 - 1818 - match text.parse::<usize>() { 1819 - Ok(new_size) => { 1820 - size_input.set(format_size(new_size, humansize::BINARY)); 1821 - size_error.set(None); 1822 - root.with_mut(|data| { 1823 - if let Some(Data::Blob(blob)) = data.get_at_path_mut(&path_for_size) { 1824 - blob.size = new_size; 1825 - } 1826 - }); 1827 - } 1828 - Err(_) => { 1829 - size_error.set(Some("Must be a non-negative integer".to_string())); 1830 - } 1831 - } 1832 - }; 1833 - 1834 - let placeholder_cid = "bafkreiaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; 1835 - let is_placeholder = blob_data() 1836 - .map(|(cid, _, _)| cid == placeholder_cid) 1837 - .unwrap_or(true); 1838 - let is_image = blob_data() 1839 - .map(|(_, _, mime)| mime.starts_with("image/")) 1840 - .unwrap_or(false); 1841 - 1842 - // Use preview data URL if available (fresh upload), otherwise CDN 1843 - let image_url = if let Some(data_url) = preview_data_url() { 1844 - Some(data_url) 1845 - } else if !is_placeholder && is_image { 1846 - blob_data().map(|(cid, _, mime)| { 1847 - let format = mime.strip_prefix("image/").unwrap_or("jpeg"); 1848 - format!( 1849 - "https://cdn.bsky.app/img/feed_fullsize/plain/{}/{}@{}", 1850 - did, cid, format 1851 - ) 1852 - }) 1853 - } else { 1854 - None 1855 - }; 1856 - 1857 - rsx! { 1858 - div { class: "record-field blob-field", 1859 - div { class: "field-header", 1860 - PathLabel { path: path.clone() } 1861 - span { class: "string-type-tag", " [blob]" } 1862 - {remove_button} 1863 - } 1864 - div { class: "blob-fields", 1865 - div { class: "blob-field-row blob-field-cid", 1866 - label { "CID:" } 1867 - input { 1868 - r#type: "text", 1869 - value: "{cid_input}", 1870 - oninput: handle_cid_change, 1871 - class: if cid_error().is_some() { "invalid" } else { "" }, 1872 - } 1873 - if let Some(err) = cid_error() { 1874 - span { class: "field-error", " ❌ {err}" } 1875 - } 1876 - } 1877 - div { class: "blob-field-row", 1878 - label { "Size:" } 1879 - input { 1880 - r#type: "number", 1881 - value: "{size_input}", 1882 - oninput: handle_size_change, 1883 - class: if size_error().is_some() { "invalid" } else { "" }, 1884 - } 1885 - if let Some(err) = size_error() { 1886 - span { class: "field-error", " ❌ {err}" } 1887 - } 1888 - } 1889 - div { class: "blob-field-row", 1890 - label { "MIME Type:" } 1891 - span { class: "readonly", 1892 - "{blob_data().map(|(_, _, mime)| mime).unwrap_or_default()}" 1893 - } 1894 - } 1895 - if let Some(url) = image_url { 1896 - img { 1897 - src: "{url}", 1898 - alt: "Blob preview", 1899 - class: "blob-image", 1900 - } 1901 - } 1902 - div { class: "blob-upload-section", 1903 - input { 1904 - r#type: "file", 1905 - accept: if is_image { "image/*" } else { "*/*" }, 1906 - onchange: handle_file, 1907 - disabled: uploading(), 1908 - } 1909 - if uploading() { 1910 - span { class: "upload-status", "Uploading..." } 1911 - } 1912 - if let Some(err) = upload_error() { 1913 - div { class: "field-error", "❌ {err}" } 1914 - } 1915 - } 1916 - } 1917 - } 1918 - } 1919 - } 1920 - 1921 - /// Decode image dimensions from data URL using browser Image API 1922 - #[cfg(target_arch = "wasm32")] 1923 - async fn decode_image_dimensions(data_url: &str) -> Option<(u32, u32)> { 1924 - use wasm_bindgen::JsCast; 1925 - use wasm_bindgen::prelude::*; 1926 - use wasm_bindgen_futures::JsFuture; 1927 - 1928 - let window = web_sys::window()?; 1929 - let document = window.document()?; 1930 - 1931 - let img = document.create_element("img").ok()?; 1932 - let img = img.dyn_into::<web_sys::HtmlImageElement>().ok()?; 1933 - 1934 - img.set_src(data_url); 1935 - 1936 - // Wait for image to load 1937 - let promise = js_sys::Promise::new(&mut |resolve, _reject| { 1938 - let onload = Closure::wrap(Box::new(move || { 1939 - resolve.call0(&JsValue::NULL).ok(); 1940 - }) as Box<dyn FnMut()>); 1941 - 1942 - img.set_onload(Some(onload.as_ref().unchecked_ref())); 1943 - onload.forget(); 1944 - }); 1945 - 1946 - JsFuture::from(promise).await.ok()?; 1947 - 1948 - Some((img.natural_width(), img.natural_height())) 1949 - } 1950 - 1951 - /// Find and populate aspectRatio field for a blob 1952 - #[allow(unused)] 1953 - fn populate_aspect_ratio( 1954 - mut root: Signal<Data<'static>>, 1955 - blob_path: &str, 1956 - width: i64, 1957 - height: i64, 1958 - ) { 1959 - // Query for all aspectRatio fields and collect the path we want 1960 - let aspect_path_to_update = { 1961 - let data = root.read(); 1962 - let query_result = data.query("...aspectRatio"); 1963 - 1964 - query_result.multiple().and_then(|matches| { 1965 - // Find aspectRatio that's a sibling of our blob 1966 - // e.g. blob at "embed.images[0].image" -> look for "embed.images[0].aspectRatio" 1967 - let blob_parent = blob_path.rsplit_once('.').map(|(parent, _)| parent); 1968 - matches.iter().find_map(|query_match| { 1969 - let aspect_parent = query_match.path.rsplit_once('.').map(|(parent, _)| parent); 1970 - 1971 - // Check if they share the same parent 1972 - if blob_parent == aspect_parent { 1973 - Some(query_match.path.clone()) 1974 - } else { 1975 - None 1976 - } 1977 - }) 1978 - }) 1979 - }; 1980 - 1981 - // Update the aspectRatio if we found a matching field 1982 - if let Some(aspect_path) = aspect_path_to_update { 1983 - let aspect_obj = atproto! {{ 1984 - "width": width, 1985 - "height": height 1986 - }}; 1987 - 1988 - root.with_mut(|record_data| { 1989 - record_data.set_at_path(&aspect_path, aspect_obj); 1990 - }); 1991 - } 1992 - } 1993 - 1994 - /// Bytes field with hex/base64 auto-detection 1995 - #[component] 1996 - fn EditableBytesField( 1997 - root: Signal<Data<'static>>, 1998 - path: String, 1999 - #[props(default)] remove_button: Option<Element>, 2000 - ) -> Element { 2001 - let path_for_memo = path.clone(); 2002 - let current_bytes = use_memo(move || { 2003 - root.read() 2004 - .get_at_path(&path_for_memo) 2005 - .and_then(|d| match d { 2006 - Data::Bytes(b) => Some(bytes_to_hex(b)), 2007 - _ => None, 2008 - }) 2009 - }); 2010 - 2011 - let mut input_text = use_signal(|| String::new()); 2012 - let mut parse_error = use_signal(|| None::<String>); 2013 - let mut detected_format = use_signal(|| None::<String>); 2014 - 2015 - // Sync input when bytes change 2016 - use_effect(move || { 2017 - if let Some(hex) = current_bytes() { 2018 - input_text.set(hex); 2019 - } 2020 - }); 2021 - 2022 - let path_for_mutation = path.clone(); 2023 - let handle_input = move |evt: Event<dioxus::prelude::FormData>| { 2024 - let text = evt.value(); 2025 - input_text.set(text.clone()); 2026 - 2027 - match parse_bytes_input(&text) { 2028 - Ok((bytes, format)) => { 2029 - parse_error.set(None); 2030 - detected_format.set(Some(format)); 2031 - root.with_mut(|data| { 2032 - if let Some(target) = data.get_at_path_mut(&path_for_mutation) { 2033 - *target = Data::Bytes(bytes); 2034 - } 2035 - }); 2036 - } 2037 - Err(e) => { 2038 - parse_error.set(Some(e)); 2039 - detected_format.set(None); 2040 - } 2041 - } 2042 - }; 2043 - 2044 - let byte_count = current_bytes() 2045 - .map(|hex| hex.chars().filter(|c| c.is_ascii_hexdigit()).count() / 2) 2046 - .unwrap_or(0); 2047 - let size_label = if byte_count > 128 { 2048 - format_size(byte_count, humansize::BINARY) 2049 - } else { 2050 - format!("{} bytes", byte_count) 2051 - }; 2052 - 2053 - rsx! { 2054 - div { class: "record-field bytes-field", 2055 - div { class: "field-header", 2056 - PathLabel { path: path.clone() } 2057 - span { class: "string-type-tag", " [bytes: {size_label}]" } 2058 - if let Some(format) = detected_format() { 2059 - span { class: "bytes-format-tag", " ({format})" } 2060 - } 2061 - {remove_button} 2062 - } 2063 - textarea { 2064 - value: "{input_text}", 2065 - placeholder: "Paste hex (1a2b3c...) or base64 (YWJj...)", 2066 - oninput: handle_input, 2067 - class: if parse_error().is_some() { "invalid" } else { "" }, 2068 - rows: "3", 2069 - } 2070 - if let Some(err) = parse_error() { 2071 - span { class: "field-error", " ❌ {err}" } 2072 - } 2073 - } 2074 - } 2075 - } 2076 - 2077 - /// Parse bytes from hex or base64, auto-detecting format 2078 - fn parse_bytes_input(text: &str) -> Result<(jacquard::bytes::Bytes, String), String> { 2079 - let trimmed = text.trim(); 2080 - if trimmed.is_empty() { 2081 - return Err("Input is empty".to_string()); 2082 - } 2083 - 2084 - // Remove common whitespace/separators 2085 - let cleaned: String = trimmed 2086 - .chars() 2087 - .filter(|c| !c.is_whitespace() && *c != ':' && *c != '-') 2088 - .collect(); 2089 - 2090 - // Try hex first (more restrictive) 2091 - if cleaned.chars().all(|c| c.is_ascii_hexdigit()) { 2092 - parse_hex_bytes(&cleaned).map(|b| (b, "hex".to_string())) 2093 - } else { 2094 - // Try base64 2095 - parse_base64_bytes(&cleaned).map(|b| (b, "base64".to_string())) 2096 - } 2097 - } 2098 - 2099 - /// Parse hex string to bytes 2100 - fn parse_hex_bytes(hex: &str) -> Result<jacquard::bytes::Bytes, String> { 2101 - if hex.len() % 2 != 0 { 2102 - return Err("Hex string must have even length".to_string()); 2103 - } 2104 - 2105 - let mut bytes = Vec::with_capacity(hex.len() / 2); 2106 - for chunk in hex.as_bytes().chunks(2) { 2107 - let hex_byte = std::str::from_utf8(chunk).map_err(|e| format!("Invalid UTF-8: {}", e))?; 2108 - let byte = 2109 - u8::from_str_radix(hex_byte, 16).map_err(|e| format!("Invalid hex digit: {}", e))?; 2110 - bytes.push(byte); 2111 - } 2112 - 2113 - Ok(jacquard::bytes::Bytes::from(bytes)) 2114 - } 2115 - 2116 - /// Parse base64 string to bytes 2117 - fn parse_base64_bytes(b64: &str) -> Result<jacquard::bytes::Bytes, String> { 2118 - use base64::Engine; 2119 - let engine = base64::engine::general_purpose::STANDARD; 2120 - 2121 - engine 2122 - .decode(b64) 2123 - .map(jacquard::bytes::Bytes::from) 2124 - .map_err(|e| format!("Invalid base64: {}", e)) 2125 - } 2126 - 2127 - /// Convert bytes to hex display string (with spacing every 4 chars) 2128 - fn bytes_to_hex(bytes: &jacquard::bytes::Bytes) -> String { 2129 - bytes 2130 - .iter() 2131 - .enumerate() 2132 - .map(|(i, b)| { 2133 - let hex = format!("{:02x}", b); 2134 - if i > 0 && i % 2 == 0 { 2135 - format!(" {}", hex) 2136 - } else { 2137 - hex 2138 - } 2139 - }) 2140 - .collect() 2141 - } 2142 - 2143 - /// CidLink field with validation 2144 - #[component] 2145 - fn EditableCidLinkField( 2146 - root: Signal<Data<'static>>, 2147 - path: String, 2148 - #[props(default)] remove_button: Option<Element>, 2149 - ) -> Element { 2150 - let path_for_memo = path.clone(); 2151 - let current_cid = use_memo(move || { 2152 - root.read() 2153 - .get_at_path(&path_for_memo) 2154 - .map(|d| match d { 2155 - Data::CidLink(cid) => cid.to_string(), 2156 - _ => String::new(), 2157 - }) 2158 - .unwrap_or_default() 2159 - }); 2160 - 2161 - let mut input_text = use_signal(|| String::new()); 2162 - let mut parse_error = use_signal(|| None::<String>); 2163 - 2164 - use_effect(move || { 2165 - input_text.set(current_cid()); 2166 - }); 2167 - 2168 - let input_width = use_memo(move || { 2169 - let len = input_text().len(); 2170 - format!("{}ch", len.max(60)) 2171 - }); 2172 - 2173 - let path_for_mutation = path.clone(); 2174 - let handle_input = move |evt: Event<dioxus::prelude::FormData>| { 2175 - let text = evt.value(); 2176 - input_text.set(text.clone()); 2177 - 2178 - match jacquard::types::cid::Cid::new_owned(text.as_bytes()) { 2179 - Ok(new_cid) => { 2180 - parse_error.set(None); 2181 - root.with_mut(|data| { 2182 - if let Some(target) = data.get_at_path_mut(&path_for_mutation) { 2183 - *target = Data::CidLink(new_cid); 2184 - } 2185 - }); 2186 - } 2187 - Err(_) => { 2188 - parse_error.set(Some("Invalid CID format".to_string())); 2189 - } 2190 - } 2191 - }; 2192 - 2193 - rsx! { 2194 - div { class: "record-field cidlink-field", 2195 - div { class: "field-header", 2196 - PathLabel { path: path.clone() } 2197 - span { class: "string-type-tag", " [cid-link]" } 2198 - {remove_button} 2199 - } 2200 - input { 2201 - r#type: "text", 2202 - value: "{input_text}", 2203 - style: "width: {input_width}", 2204 - placeholder: "bafyrei...", 2205 - oninput: handle_input, 2206 - class: if parse_error().is_some() { "invalid" } else { "" }, 2207 - } 2208 - if let Some(err) = parse_error() { 2209 - span { class: "field-error", " ❌ {err}" } 2210 - } 2211 - } 2212 - } 2213 - } 2214 - 2215 - // ============================================================================ 2216 - // Field with Remove Button Wrapper 2217 - // ============================================================================ 2218 - 2219 - /// Wraps a field with an optional remove button in the header 2220 - #[component] 2221 - fn FieldWithRemove( 2222 - root: Signal<Data<'static>>, 2223 - path: String, 2224 - did: String, 2225 - is_removable: bool, 2226 - parent_path: String, 2227 - field_key: String, 2228 - ) -> Element { 2229 - let remove_button = if is_removable { 2230 - Some(rsx! { 2231 - button { 2232 - class: "field-remove-button", 2233 - onclick: move |_| { 2234 - let mut new_data = root.read().clone(); 2235 - if let Some(Data::Object(obj)) = new_data.get_at_path_mut(parent_path.as_str()) { 2236 - obj.0.remove(field_key.as_str()); 2237 - } 2238 - root.set(new_data); 2239 - }, 2240 - "Remove" 2241 - } 2242 - }) 2243 - } else { 2244 - None 2245 - }; 2246 - 2247 - rsx! { 2248 - EditableDataView { 2249 - root: root, 2250 - path: path.clone(), 2251 - did: did.clone(), 2252 - remove_button: remove_button, 2253 - } 2254 - } 2255 - } 2256 - 2257 - // ============================================================================ 2258 - // Array Field Editor (enables recursion) 2259 - // ============================================================================ 2260 - 2261 - /// Array field - iterates items and renders child EditableDataView for each 2262 - #[component] 2263 - fn EditableArrayField(root: Signal<Data<'static>>, path: String, did: String) -> Element { 2264 - let path_for_memo = path.clone(); 2265 - let array_len = use_memo(move || { 2266 - root.read() 2267 - .get_at_path(&path_for_memo) 2268 - .and_then(|d| d.as_array()) 2269 - .map(|arr| arr.0.len()) 2270 - .unwrap_or(0) 2271 - }); 2272 - 2273 - let path_for_add = path.clone(); 2274 - 2275 - rsx! { 2276 - div { class: "record-section array-section", 2277 - Accordion { 2278 - id: "edit-array-{path}", 2279 - collapsible: true, 2280 - AccordionItem { 2281 - default_open: true, 2282 - index: 0, 2283 - AccordionTrigger { 2284 - div { class: "section-header", 2285 - div { class: "section-label", 2286 - { 2287 - let parts: Vec<&str> = path.split('.').collect(); 2288 - let final_part = parts.last().unwrap_or(&""); 2289 - rsx! { "{final_part}" } 2290 - } 2291 - } 2292 - span { class: "array-length", "[{array_len}]" } 2293 - } 2294 - } 2295 - AccordionContent { 2296 - div { class: "section-content", 2297 - for idx in 0..array_len() { 2298 - { 2299 - let item_path = format!("{}[{}]", path, idx); 2300 - let path_for_remove = path.clone(); 2301 - 2302 - rsx! { 2303 - div { 2304 - class: "array-item", 2305 - key: "{item_path}", 2306 - 2307 - EditableDataView { 2308 - root: root, 2309 - path: item_path.clone(), 2310 - did: did.clone(), 2311 - remove_button: rsx! { 2312 - button { 2313 - class: "field-remove-button", 2314 - onclick: move |_| { 2315 - root.with_mut(|data| { 2316 - if let Some(Data::Array(arr)) = data.get_at_path_mut(&path_for_remove) { 2317 - arr.0.remove(idx); 2318 - } 2319 - }); 2320 - }, 2321 - "Remove" 2322 - } 2323 - } 2324 - } 2325 - } 2326 - } 2327 - } 2328 - } 2329 - div { 2330 - class: "array-item", 2331 - div { 2332 - class: "add-field-widget", 2333 - button { 2334 - onclick: move |_| { 2335 - root.with_mut(|data| { 2336 - if let Some(Data::Array(arr)) = data.get_at_path_mut(&path_for_add) { 2337 - let new_item = create_array_item_default(arr); 2338 - arr.0.push(new_item); 2339 - } 2340 - }); 2341 - }, 2342 - "+ Add Item" 2343 - } 2344 - } 2345 - } 2346 - } 2347 - } 2348 - } 2349 - } 2350 - } 2351 - } 2352 - } 2353 - 2354 - // ============================================================================ 2355 - // Object Field Editor (enables recursion) 2356 - // ============================================================================ 2357 - 2358 - /// Object field - iterates fields and renders child EditableDataView for each 2359 - #[component] 2360 - fn EditableObjectField( 2361 - root: Signal<Data<'static>>, 2362 - path: String, 2363 - did: String, 2364 - #[props(default)] remove_button: Option<Element>, 2365 - ) -> Element { 2366 - let path_for_memo = path.clone(); 2367 - let field_keys = use_memo(move || { 2368 - root.read() 2369 - .get_at_path(&path_for_memo) 2370 - .and_then(|d| d.as_object()) 2371 - .map(|obj| obj.0.keys().cloned().collect::<Vec<_>>()) 2372 - .unwrap_or_default() 2373 - }); 2374 - 2375 - let is_root = path.is_empty(); 2376 - 2377 - rsx! { 2378 - if !is_root { 2379 - div { class: "record-section object-section", 2380 - Accordion { 2381 - id: "edit-object-{path}", 2382 - collapsible: true, 2383 - AccordionItem { 2384 - default_open: true, 2385 - index: 0, 2386 - AccordionTrigger { 2387 - div { class: "section-header", 2388 - div { class: "section-label", 2389 - { 2390 - let parts: Vec<&str> = path.split('.').collect(); 2391 - let final_part = parts.last().unwrap_or(&""); 2392 - rsx! { "{final_part}" } 2393 - } 2394 - } 2395 - {remove_button} 2396 - } 2397 - } 2398 - AccordionContent { 2399 - div { class: "section-content", 2400 - for key in field_keys() { 2401 - { 2402 - let field_path = if path.is_empty() { 2403 - key.to_string() 2404 - } else { 2405 - format!("{}.{}", path, key) 2406 - }; 2407 - let is_type_field = key == "$type"; 2408 - 2409 - rsx! { 2410 - FieldWithRemove { 2411 - key: "{field_path}", 2412 - root: root, 2413 - path: field_path.clone(), 2414 - did: did.clone(), 2415 - is_removable: !is_type_field, 2416 - parent_path: path.clone(), 2417 - field_key: key.clone(), 2418 - } 2419 - } 2420 - } 2421 - } 2422 - 2423 - AddFieldWidget { root: root, path: path.clone() } 2424 - } 2425 - } 2426 - } 2427 - } 2428 - } 2429 - } else { 2430 - for key in field_keys() { 2431 - { 2432 - let field_path = key.to_string(); 2433 - let is_type_field = key == "$type"; 2434 - 2435 - rsx! { 2436 - FieldWithRemove { 2437 - key: "{field_path}", 2438 - root: root, 2439 - path: field_path.clone(), 2440 - did: did.clone(), 2441 - is_removable: !is_type_field, 2442 - parent_path: path.clone(), 2443 - field_key: key.clone(), 2444 - } 2445 - } 2446 - } 2447 - } 2448 - 2449 - AddFieldWidget { root: root, path: path.clone() } 2450 - } 2451 - } 2452 - } 2453 - 2454 - /// Widget for adding new fields to objects 2455 - #[component] 2456 - fn AddFieldWidget(root: Signal<Data<'static>>, path: String) -> Element { 2457 - let mut field_name = use_signal(|| String::new()); 2458 - let mut field_value = use_signal(|| String::new()); 2459 - let mut error = use_signal(|| None::<String>); 2460 - let mut show_form = use_signal(|| false); 2461 - 2462 - let path_for_enter = path.clone(); 2463 - let path_for_button = path.clone(); 2464 - 2465 - rsx! { 2466 - div { class: "add-field-widget", 2467 - if !show_form() { 2468 - button { 2469 - class: "add-button", 2470 - onclick: move |_| show_form.set(true), 2471 - "+ Add Field" 2472 - } 2473 - } else { 2474 - div { class: "add-field-form", 2475 - input { 2476 - r#type: "text", 2477 - placeholder: "Field name", 2478 - value: "{field_name}", 2479 - oninput: move |evt| field_name.set(evt.value()), 2480 - } 2481 - input { 2482 - r#type: "text", 2483 - placeholder: r#"Value: {{}}, [], true, 123, "text""#, 2484 - value: "{field_value}", 2485 - oninput: move |evt| field_value.set(evt.value()), 2486 - onkeydown: move |evt| { 2487 - use dioxus::prelude::keyboard_types::Key; 2488 - if evt.key() == Key::Enter { 2489 - let name = field_name(); 2490 - let value_text = field_value(); 2491 - 2492 - if name.is_empty() { 2493 - error.set(Some("Field name required".to_string())); 2494 - return; 2495 - } 2496 - 2497 - let new_value = match infer_data_from_text(&value_text) { 2498 - Ok(data) => data, 2499 - Err(e) => { 2500 - error.set(Some(e)); 2501 - return; 2502 - } 2503 - }; 2504 - 2505 - let mut new_data = root.read().clone(); 2506 - if let Some(Data::Object(obj)) = new_data.get_at_path_mut(path_for_enter.as_str()) { 2507 - obj.0.insert(name.into(), new_value); 2508 - } 2509 - root.set(new_data); 2510 - 2511 - // Reset form 2512 - field_name.set(String::new()); 2513 - field_value.set(String::new()); 2514 - show_form.set(false); 2515 - error.set(None); 2516 - } 2517 - } 2518 - } 2519 - button { 2520 - class: "add-field-widget-edit", 2521 - onclick: move |_| { 2522 - let name = field_name(); 2523 - let value_text = field_value(); 2524 - 2525 - if name.is_empty() { 2526 - error.set(Some("Field name required".to_string())); 2527 - return; 2528 - } 2529 - 2530 - let new_value = match infer_data_from_text(&value_text) { 2531 - Ok(data) => data, 2532 - Err(e) => { 2533 - error.set(Some(e)); 2534 - return; 2535 - } 2536 - }; 2537 - 2538 - let mut new_data = root.read().clone(); 2539 - if let Some(Data::Object(obj)) = new_data.get_at_path_mut(path_for_button.as_str()) { 2540 - obj.0.insert(name.into(), new_value); 2541 - } 2542 - root.set(new_data); 2543 - 2544 - // Reset form 2545 - field_name.set(String::new()); 2546 - field_value.set(String::new()); 2547 - show_form.set(false); 2548 - error.set(None); 2549 - }, 2550 - "Add" 2551 - } 2552 - button { 2553 - class: "add-field-widget-edit", 2554 - onclick: move |_| { 2555 - show_form.set(false); 2556 - field_name.set(String::new()); 2557 - field_value.set(String::new()); 2558 - error.set(None); 2559 - }, 2560 - "Cancel" 2561 - } 2562 - if let Some(err) = error() { 2563 - div { class: "field-error", "❌ {err}" } 2564 - } 2565 - } 2566 - } 2567 - } 2568 - } 2569 - } 2570 - 2571 - /// Layout component for record view - handles header, metadata, and wraps children 2572 - #[component] 2573 - fn RecordViewLayout( 2574 - uri: AtUri<'static>, 2575 - cid: Option<Cid<'static>>, 2576 - schema: ReadSignal<Option<LexiconDoc<'static>>>, 2577 - record_value: Data<'static>, 2578 - children: Element, 2579 - ) -> Element { 2580 - // Validate the record if schema is available 2581 - let validation_status = use_memo(move || { 2582 - let _schema_doc = schema()?; 2583 - let nsid_str = record_value.type_discriminator()?; 2584 - 2585 - let validator = jacquard_lexicon::validation::SchemaValidator::global(); 2586 - let result = validator.validate_by_nsid(nsid_str, &record_value); 2587 - 2588 - Some(result.is_valid()) 2589 - }); 2590 - 2591 - rsx! { 2592 - div { 2593 - class: "record-metadata", 2594 - div { class: "metadata-row", 2595 - span { class: "metadata-label", "URI" } 2596 - span { class: "metadata-value", 2597 - HighlightedUri { uri: uri.clone() } 2598 - } 2599 - } 2600 - if let Some(cid) = cid { 2601 - div { class: "metadata-row", 2602 - span { class: "metadata-label", "CID" } 2603 - code { class: "metadata-value", "{cid}" } 2604 - } 2605 - } 2606 - if let Some(is_valid) = validation_status() { 2607 - div { class: "metadata-row", 2608 - span { class: "metadata-label", "Schema" } 2609 - span { 2610 - class: if is_valid { "metadata-value schema-valid" } else { "metadata-value schema-invalid" }, 2611 - if is_valid { "Valid" } else { "Invalid" } 2612 - } 2613 - } 2614 - } 2615 - } 2616 - 2617 - {children} 2618 - 2619 - } 2620 - } 2621 - 2622 - /// Render some text as markdown. 2623 - #[component] 2624 - fn EditableRecordContent( 2625 - record_value: Data<'static>, 2626 - uri: ReadSignal<AtUri<'static>>, 2627 - view_mode: Signal<ViewMode>, 2628 - edit_mode: Signal<bool>, 2629 - record_resource: Resource<Result<GetRecordOutput<'static>, AgentError>>, 2630 - schema: ReadSignal<Option<LexiconDoc<'static>>>, 2631 - ) -> Element { 2632 - let mut edit_data = use_signal(use_reactive!(|record_value| record_value.clone())); 2633 - let nsid = use_memo(move || edit_data().type_discriminator().map(|s| s.to_string())); 2634 - let navigator = use_navigator(); 2635 - let fetcher = use_context::<CachedFetcher>(); 2636 - 2637 - // Validate edit_data whenever it changes and provide via context 2638 - let mut validation_result = use_signal(|| None); 2639 - use_effect(move || { 2640 - let _ = schema(); // Track schema changes 2641 - if let Some(nsid_str) = nsid() { 2642 - let data = edit_data(); 2643 - let validator = jacquard_lexicon::validation::SchemaValidator::global(); 2644 - let result = validator.validate_by_nsid(&nsid_str, &data); 2645 - validation_result.set(Some(result)); 2646 - } 2647 - }); 2648 - use_context_provider(|| validation_result); 2649 - 2650 - let update_fetcher = fetcher.clone(); 2651 - let create_fetcher = fetcher.clone(); 2652 - let replace_fetcher = fetcher.clone(); 2653 - let delete_fetcher = fetcher.clone(); 2654 - 2655 - rsx! { 2656 - div { 2657 - class: "tab-bar", 2658 - button { 2659 - class: if view_mode() == ViewMode::Pretty { "tab-button active" } else { "tab-button" }, 2660 - onclick: move |_| view_mode.set(ViewMode::Pretty), 2661 - "View" 2662 - } 2663 - button { 2664 - class: if view_mode() == ViewMode::Json { "tab-button active" } else { "tab-button" }, 2665 - onclick: move |_| view_mode.set(ViewMode::Json), 2666 - "JSON" 2667 - } 2668 - button { 2669 - class: if view_mode() == ViewMode::Schema { "tab-button active" } else { "tab-button" }, 2670 - onclick: move |_| view_mode.set(ViewMode::Schema), 2671 - "Schema" 2672 - } 2673 - ActionButtons { 2674 - on_update: move |_| { 2675 - let fetcher = update_fetcher.clone(); 2676 - let uri = uri(); 2677 - let data = edit_data(); 2678 - spawn(async move { 2679 - if let Some((did, _)) = fetcher.session_info().await { 2680 - if let (Some(collection_str), Some(rkey)) = (uri.collection(), uri.rkey()) { 2681 - let collection = Nsid::new(collection_str.as_str()).ok(); 2682 - if let Some(collection) = collection { 2683 - let request = PutRecord::new() 2684 - .repo(AtIdentifier::Did(did)) 2685 - .collection(collection) 2686 - .rkey(rkey.clone()) 2687 - .record(data.clone()) 2688 - .build(); 2689 - 2690 - match fetcher.send(request).await { 2691 - Ok(output) => { 2692 - if output.status() == StatusCode::OK { 2693 - tracing::info!("Record updated successfully"); 2694 - edit_data.set(data.clone()); 2695 - edit_mode.set(false); 2696 - } else { 2697 - tracing::error!("Unexpected status code: {:?}", output.status()); 2698 - } 2699 - } 2700 - Err(e) => { 2701 - tracing::error!("Failed to update record: {:?}", e); 2702 - } 2703 - } 2704 - } 2705 - } 2706 - } 2707 - }); 2708 - }, 2709 - on_save_new: move |_| { 2710 - let fetcher = create_fetcher.clone(); 2711 - let data = edit_data(); 2712 - let nav = navigator.clone(); 2713 - spawn(async move { 2714 - if let Some((did, _)) = fetcher.session_info().await { 2715 - if let Some(collection_str) = data.type_discriminator() { 2716 - let collection = Nsid::new(collection_str).ok(); 2717 - if let Some(collection) = collection { 2718 - let request = CreateRecord::new() 2719 - .repo(AtIdentifier::Did(did)) 2720 - .collection(collection) 2721 - .record(data.clone()) 2722 - .build(); 2723 - 2724 - match fetcher.send(request).await { 2725 - Ok(response) => { 2726 - if let Ok(output) = response.into_output() { 2727 - tracing::info!("Record created: {}", output.uri); 2728 - let link = format!("{}/record/{}", crate::env::WEAVER_APP_DOMAIN, output.uri); 2729 - nav.push(link); 2730 - } 2731 - } 2732 - Err(e) => { 2733 - tracing::error!("Failed to create record: {:?}", e); 2734 - } 2735 - } 2736 - } 2737 - } 2738 - } 2739 - }); 2740 - }, 2741 - on_replace: move |_| { 2742 - let fetcher = replace_fetcher.clone(); 2743 - let uri = uri(); 2744 - let data = edit_data(); 2745 - let nav = navigator.clone(); 2746 - spawn(async move { 2747 - if let Some((did, _)) = fetcher.session_info().await { 2748 - if let Some(new_collection_str) = data.type_discriminator() { 2749 - let new_collection = Nsid::new(new_collection_str).ok(); 2750 - if let Some(new_collection) = new_collection { 2751 - // Create new record 2752 - let create_req = CreateRecord::new() 2753 - .repo(AtIdentifier::Did(did.clone())) 2754 - .collection(new_collection) 2755 - .record(data.clone()) 2756 - .build(); 2757 - 2758 - match fetcher.send(create_req).await { 2759 - Ok(response) => { 2760 - if let Ok(create_output) = response.into_output() { 2761 - // Delete old record 2762 - if let (Some(old_collection_str), Some(old_rkey)) = (uri.collection(), uri.rkey()) { 2763 - let old_collection = Nsid::new(old_collection_str.as_str()).ok(); 2764 - if let Some(old_collection) = old_collection { 2765 - let delete_req = DeleteRecord::new() 2766 - .repo(AtIdentifier::Did(did)) 2767 - .collection(old_collection) 2768 - .rkey(old_rkey.clone()) 2769 - .build(); 2770 - 2771 - if let Err(e) = fetcher.send(delete_req).await { 2772 - tracing::warn!("Created new record but failed to delete old: {:?}", e); 2773 - } 2774 - } 2775 - } 2776 - 2777 - tracing::info!("Record replaced: {}", create_output.uri); 2778 - let link = format!("{}/record/{}", crate::env::WEAVER_APP_DOMAIN, create_output.uri); 2779 - nav.push(link); 2780 - } 2781 - } 2782 - Err(e) => { 2783 - tracing::error!("Failed to replace record: {:?}", e); 2784 - } 2785 - } 2786 - } 2787 - } 2788 - } 2789 - }); 2790 - }, 2791 - on_delete: move |_| { 2792 - let fetcher = delete_fetcher.clone(); 2793 - let uri = uri(); 2794 - let nav = navigator.clone(); 2795 - spawn(async move { 2796 - if let Some((did, _)) = fetcher.session_info().await { 2797 - if let (Some(collection_str), Some(rkey)) = (uri.collection(), uri.rkey()) { 2798 - let collection = Nsid::new(collection_str.as_str()).ok(); 2799 - if let Some(collection) = collection { 2800 - let request = DeleteRecord::new() 2801 - .repo(AtIdentifier::Did(did)) 2802 - .collection(collection) 2803 - .rkey(rkey.clone()) 2804 - .build(); 2805 - 2806 - match fetcher.send(request).await { 2807 - Ok(_) => { 2808 - tracing::info!("Record deleted"); 2809 - nav.push(Route::Home {}); 2810 - } 2811 - Err(e) => { 2812 - tracing::error!("Failed to delete record: {:?}", e); 2813 - } 2814 - } 2815 - } 2816 - } 2817 - } 2818 - }); 2819 - }, 2820 - on_cancel: move |_| { 2821 - edit_data.set(record_value.clone()); 2822 - edit_mode.set(false); 2823 - }, 2824 - } 2825 - } 2826 - div { 2827 - class: "tab-content", 2828 - match view_mode() { 2829 - ViewMode::Pretty => rsx! { 2830 - div { class: "pretty-record", 2831 - EditableDataView { 2832 - root: edit_data, 2833 - path: String::new(), 2834 - did: uri().authority().to_string(), 2835 - } 2836 - } 2837 - }, 2838 - ViewMode::Json => rsx! { 2839 - JsonEditor { data: edit_data, nsid, schema } 2840 - }, 2841 - ViewMode::Schema => rsx! { 2842 - SchemaView { schema } 2843 - }, 2844 - } 2845 - } 2846 - } 2847 - } 2848 - 2849 - #[component] 2850 - pub fn CodeView(props: CodeViewProps) -> Element { 2851 - let code = &*props.code.read(); 2852 - 2853 - let mut html_buf = String::new(); 2854 - highlight_code(props.lang.as_deref(), code, &mut html_buf).unwrap(); 2855 - 2856 - rsx! { 2857 - document::Style { {generate_default_css().unwrap()}} 2858 - div { 2859 - id: "{&*props.id.read()}", 2860 - class: "{&*props.class.read()}", 2861 - dangerous_inner_html: "{html_buf}" 2862 - } 2863 - } 2864 - }
-1
crates/weaver-cli/Cargo.toml
··· 17 17 miette = { workspace = true, features = ["fancy"] } 18 18 19 19 jacquard = { workspace = true, features = ["loopback", "dns"] } 20 - jacquard-api = { workspace = true, features = ["sh_weaver"] } 21 20 22 21 markdown-weaver = { workspace = true } 23 22 markdown-weaver-escape = { workspace = true }
+9 -23
crates/weaver-common/Cargo.toml
··· 14 14 n0-future = { workspace = true } 15 15 weaver-api = { version = "0.1", path = "../weaver-api" } 16 16 markdown-weaver = { workspace = true } 17 - #libsqlite3-sys = { version = "0.30.1", features = ["bundled"] } 18 - 19 17 http = "1.3.1" 20 - 21 18 jacquard = { workspace = true } 22 19 jacquard-common = { workspace = true } 23 20 trait-variant = "0.1" 24 - 25 21 serde = { workspace = true } 26 22 serde_json = { version = "1.0.140", features = ["preserve_order", "raw_value"] } 27 - serde_ipld_dagcbor = { version = "0.6.1", features = ["codec"] } 28 - serde_html_form = "0.2.7" 29 - serde_bytes = "0.11.17" 30 23 tokio = { version = "1.44", features = ["sync"] } 31 - 32 - minijinja = { workspace = true, features = [ 33 - "builtins", 34 - "debug", 35 - "deserialization", 36 - "macros", 37 - "multi_template", 38 - "adjacent_loop_items", 39 - "std_collections", 40 - "serde", 41 - ] } 42 - 43 - 44 24 miette = { workspace = true } 45 - owo-colors = { workspace = true } 46 25 thiserror = { workspace = true } 47 26 tracing = { workspace = true } 48 - reqwest = "0.12.15" 49 - regex = "1.11.1" 27 + reqwest = { version = "0.12", default-features = false, features = [ 28 + "json", 29 + "rustls-tls", 30 + ] } 31 + 50 32 markdown-weaver-escape = { workspace = true, features = ["std"] } 51 33 mime-sniffer = "^0.1" 52 34 35 + [target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dependencies] 36 + regex = "1.11.1" 37 + 53 38 # wasm-in-browser dependencies 54 39 [target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies] 40 + regex-lite = { version = "0.1" } 55 41 futures-util = "0.3" 56 42 js-sys = "0.3" 57 43 pin-project = "1"
+4 -4
crates/weaver-common/src/agent.rs
··· 624 624 .map(|blob| { 625 625 let cid = blob.blob().cid(); 626 626 jacquard::types::string::Uri::new_owned(format!( 627 - "https://cdn.bsky.app/img/avatar/plain/{}/{}", 627 + "https://cdn.bsky.app/img/avatar/plain/{}/{}@jpeg", 628 628 did, cid 629 629 )) 630 630 }) ··· 639 639 .map(|blob| { 640 640 let cid = blob.blob().cid(); 641 641 jacquard::types::string::Uri::new_owned(format!( 642 - "https://cdn.bsky.app/img/banner/plain/{}/{}", 642 + "https://cdn.bsky.app/img/banner/plain/{}/{}@jpeg", 643 643 did, cid 644 644 )) 645 645 }) ··· 728 728 .map(|blob| { 729 729 let cid = blob.blob().cid(); 730 730 jacquard::types::string::Uri::new_owned(format!( 731 - "https://cdn.bsky.app/img/avatar/plain/{}/{}", 731 + "https://cdn.bsky.app/img/avatar/plain/{}/{}@jpeg", 732 732 did, cid 733 733 )) 734 734 }) ··· 743 743 .map(|blob| { 744 744 let cid = blob.blob().cid(); 745 745 jacquard::types::string::Uri::new_owned(format!( 746 - "https://cdn.bsky.app/img/feed_fullsize/plain/{}/{}", 746 + "https://cdn.bsky.app/img/banner/plain/{}/{}@jpeg", 747 747 did, cid 748 748 )) 749 749 })
-6
crates/weaver-common/src/error.rs
··· 103 103 pub enum ParseErrorKind { 104 104 #[error(transparent)] 105 105 SerdeError(#[from] SerDeError), 106 - #[error(transparent)] 107 - MiniJinjaError(#[from] minijinja::Error), 108 106 #[error("error in markdown parsing or rendering: {0}")] 109 107 MarkdownError(markdown_weaver::CowStr<'static>), 110 108 } ··· 116 114 #[error(transparent)] 117 115 #[diagnostic_source] 118 116 Json(#[from] serde_json::Error), 119 - #[error(transparent)] 120 - DagCbor(#[from] serde_ipld_dagcbor::error::CodecError), 121 - #[error(transparent)] 122 - HtmlForm(#[from] serde_html_form::ser::Error), 123 117 } 124 118 125 119 impl From<serde_json::Error> for ParseError {
-1
crates/weaver-index/Cargo.toml
··· 24 24 uuid = { version = "1.8.0", features = ["v7", "serde"] } 25 25 26 26 jacquard = { workspace = true, features = ["websocket", "zstd"] } 27 - jacquard-api = { workspace = true, features = ["sh_weaver"] } 28 27 jacquard-axum = { workspace = true } 29 28 30 29 axum = "0.8"
+5 -5
crates/weaver-renderer/Cargo.toml
··· 11 11 weaver-api = { path = "../weaver-api" } 12 12 jacquard.workspace = true 13 13 markdown-weaver = { workspace = true } 14 - compact_string = "0.1.0" 15 14 http = "1.3.1" 16 15 url = "2.5.4" 17 16 syntect = { workspace = true, default-features = false, features = ["default-fancy"]} 18 17 markdown-weaver-escape = { workspace = true, features = ["std"] } 19 18 thiserror.workspace = true 19 + tracing.workspace = true 20 20 miette.workspace = true 21 21 unicode-normalization = "0.1.24" 22 22 yaml-rust2 = { version = "0.10.2" } 23 23 bitflags = "2.9.1" 24 - tracing = "0.1" 25 - 26 24 dashmap = "6.1.0" 27 - regex = "1.11.1" 28 25 pin-utils = "0.1.0" 29 26 pin-project = "1.1.10" 30 - dynosaur = "0.2.0" 31 27 smol_str = { version = "0.3", features = ["serde"] } 32 28 mime-sniffer = "0.1.3" 33 29 reqwest = { version = "0.12.7", default-features = false, features = [ ··· 36 32 ] } 37 33 38 34 [target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dependencies] 35 + regex = { version = "1.12" } 39 36 tokio = { version = "1.28", features = ["rt", "time"] } 40 37 tokio-util = { version = "0.7.14", features = ["rt"] } 41 38 ignore = "0.4.23" 39 + 40 + [target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies] 41 + regex-lite = { version = "0.1" } 42 42 43 43 [dev-dependencies] 44 44 insta = { version = "1.40", features = ["yaml"] }
+5 -7
crates/weaver-renderer/src/atproto/client.rs
··· 203 203 if let Some(embeds) = &entry.embeds { 204 204 if let Some(images) = &embeds.images { 205 205 for img in &images.images { 206 + tracing::info!("Image: {:?}", img); 206 207 if let Some(name) = &img.name { 207 208 let blob_name = BlobName::from_filename(name.as_ref()); 208 209 map.insert(blob_name, img.image.blob().cid().clone().into_static()); ··· 227 228 /// - Bluesky list: `at://{actor}/app.bsky.graph.list/{rkey}` → `https://bsky.app/profile/{actor}/lists/{rkey}` 228 229 /// - Bluesky feed: `at://{actor}/app.bsky.feed.generator/{rkey}` → `https://bsky.app/profile/{actor}/feed/{rkey}` 229 230 /// - Bluesky starterpack: `at://{actor}/app.bsky.graph.starterpack/{rkey}` → `https://bsky.app/starter-pack/{actor}/{rkey}` 230 - /// - Weaver/other: `at://{actor}/{collection}/{rkey}` → `https://weaver.sh/{actor}/{collection}/{rkey}` 231 + /// - Weaver/other: `at://{actor}/{collection}/{rkey}` → `https://weaver.sh/record/{at_uri}` 231 232 fn at_uri_to_web_url(at_uri: &AtUri<'_>) -> String { 232 233 let authority = at_uri.authority().as_ref(); 233 234 234 235 // Profile-only link (no collection/rkey) 235 236 if at_uri.collection().is_none() && at_uri.rkey().is_none() { 236 - return format!("https://weaver.sh/{}", authority); 237 + return format!("https://alpha.weaver.sh/{}", authority); 237 238 } 238 239 239 240 // Record link ··· 257 258 } 258 259 // Weaver records and unknown collections go to weaver.sh 259 260 _ => { 260 - format!( 261 - "https://weaver.sh/{}/{}/{}", 262 - authority, collection_str, rkey_str 263 - ) 261 + format!("https://alpha.weaver.sh/record/{}", at_uri) 264 262 } 265 263 } 266 264 } else { 267 265 // Fallback for malformed URIs 268 - format!("https://weaver.sh/{}", authority) 266 + format!("https://alpha.weaver.sh/{}", authority) 269 267 } 270 268 } 271 269
+8 -4
crates/weaver-renderer/src/atproto/writer.rs
··· 50 50 Body, 51 51 } 52 52 53 - impl<'a, I: Iterator<Item = Event<'a>>, W: StrWrite, E: EmbedContentProvider> ClientWriter<'a, I, W, E> { 53 + impl<'a, I: Iterator<Item = Event<'a>>, W: StrWrite, E: EmbedContentProvider> 54 + ClientWriter<'a, I, W, E> 55 + { 54 56 pub fn new(events: I, writer: W) -> Self { 55 57 Self { 56 58 events, ··· 382 384 Tag::Superscript => self.write("<sup>"), 383 385 Tag::Emphasis => self.write("<em>"), 384 386 Tag::Strong => self.write("<strong>"), 385 - Tag::Strikethrough => self.write("<del>"), 387 + Tag::Strikethrough => self.write("<s>"), 386 388 Tag::Link { 387 389 link_type: LinkType::Email, 388 390 dest_url, ··· 551 553 TagEnd::Superscript => self.write("</sup>"), 552 554 TagEnd::Subscript => self.write("</sub>"), 553 555 TagEnd::Strong => self.write("</strong>"), 554 - TagEnd::Strikethrough => self.write("</del>"), 556 + TagEnd::Strikethrough => self.write("</s>"), 555 557 TagEnd::Link => self.write("</a>"), 556 558 TagEnd::Image => Ok(()), // No-op: raw_text() already consumed the End(Image) event 557 559 TagEnd::Embed => Ok(()), ··· 568 570 } 569 571 } 570 572 571 - impl<'a, I: Iterator<Item = Event<'a>>, W: StrWrite, E: EmbedContentProvider> ClientWriter<'a, I, W, E> { 573 + impl<'a, I: Iterator<Item = Event<'a>>, W: StrWrite, E: EmbedContentProvider> 574 + ClientWriter<'a, I, W, E> 575 + { 572 576 fn write_embed( 573 577 &mut self, 574 578 embed_type: EmbedType,
+4
crates/weaver-renderer/src/lib.rs
··· 10 10 use yaml_rust2::Yaml; 11 11 use yaml_rust2::YamlLoader; 12 12 13 + #[cfg(not(all(target_family = "wasm", target_os = "unknown")))] 13 14 use regex::Regex; 15 + #[cfg(all(target_family = "wasm", target_os = "unknown"))] 16 + use regex_lite::Regex; 17 + 14 18 use std::iter::Iterator; 15 19 use std::path::PathBuf; 16 20 use std::pin::Pin;
+2 -2
crates/weaver-renderer/src/types.rs
··· 1 - use compact_string::CompactString; 2 1 use http::Uri; 2 + use smol_str::SmolStr; 3 3 use weaver_common::jacquard::types::string::{Cid, Did}; 4 4 5 5 pub struct Link<'a> { ··· 7 7 pub blob: BlobLink<'a>, 8 8 } 9 9 10 - pub type MimeType = CompactString; 10 + pub type MimeType = SmolStr; 11 11 12 12 pub enum BlobLink<'a> { 13 13 PDS {
+5
crates/weaver-renderer/src/utils.rs
··· 2 2 use markdown_weaver::{CodeBlockKind, CowStr, Event, Tag}; 3 3 use miette::IntoDiagnostic; 4 4 use n0_future::TryFutureExt; 5 + 6 + #[cfg(not(all(target_family = "wasm", target_os = "unknown")))] 5 7 use regex::Regex; 8 + #[cfg(all(target_family = "wasm", target_os = "unknown"))] 9 + use regex_lite::Regex; 10 + 6 11 use markdown_weaver::BrokenLink; 7 12 use std::path::PathBuf; 8 13 use std::sync::Arc;
+1
flake.nix
··· 267 267 dioxus-cli 268 268 wasm-bindgen-cli 269 269 wasm-pack 270 + twiggy 270 271 binaryen.out 271 272 ]; 272 273 };
+6 -5
weaver_notes/.obsidian/workspace.json
··· 41 41 "state": { 42 42 "type": "markdown", 43 43 "state": { 44 - "file": "bug notes.md", 44 + "file": "Why I rewrote pdsls in Rust (tm).md", 45 45 "mode": "source", 46 46 "source": false 47 47 }, 48 48 "icon": "lucide-file", 49 - "title": "bug notes" 49 + "title": "Why I rewrote pdsls in Rust (tm)" 50 50 } 51 51 } 52 52 ], ··· 201 201 }, 202 202 "active": "6029beecc3d03bce", 203 203 "lastOpenFiles": [ 204 + "light_mode_excerpt.png", 205 + "notebook_entry_preview.png", 206 + "xkcd_345_excerpt.png", 204 207 "bug notes.md", 205 208 "Why I rewrote pdsls in Rust (tm).md", 206 209 "meta.png", ··· 212 215 "pretty_editor.png", 213 216 "Arch.md", 214 217 "Weaver - Long-form writing.md", 215 - "weaver_photo_med.jpg", 216 - "xkcd_345_excerpt.png", 217 - "light_mode_excerpt.png" 218 + "weaver_photo_med.jpg" 218 219 ] 219 220 }
-3
weaver_notes/bug notes.md
··· 1 - - renderer/processor doesn't add http(s):// to urls without them (or handle autolinks currently) 2 - - mobile view doesn't narrow correctly 3 - -
weaver_notes/light_mode_excerpt.png

This is a binary file and will not be displayed.

weaver_notes/notebook_entry_preview.png

This is a binary file and will not be displayed.

weaver_notes/xkcd_345_excerpt.png

This is a binary file and will not be displayed.