From b7a020767ef67ed11a652b0cbfb7270cd5526547 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Thu, 10 Sep 2026 20:26:17 +0700 Subject: [PATCH] wip --- Cargo.lock | 348 +++++++++------- crates/signed_core/Cargo.toml | 4 + crates/signed_core/src/filters.rs | 190 ++++++++- crates/signed_core/src/inbox.rs | 645 ++++++++++++++++++++++++++++++ crates/signed_core/src/lib.rs | 5 + docs/inbox-plan.md | 515 ++++++++++++++++++++++++ 6 files changed, 1554 insertions(+), 153 deletions(-) create mode 100644 crates/signed_core/src/inbox.rs create mode 100644 docs/inbox-plan.md diff --git a/Cargo.lock b/Cargo.lock index 271f8bc..189ee38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -127,7 +127,6 @@ checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", "const-random", - "getrandom 0.3.4", "once_cell", "version_check", "zerocopy", @@ -321,9 +320,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.44" +version = "0.4.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "515a1f282e33d55983c499d7e9e87082e81cbc32974825bf9032f928392d5844" +checksum = "4f10dafd0c8d2e51ae9a748805777613ed0bbe17bf586b76c8311f45c020a32f" dependencies = [ "compression-codecs", "compression-core", @@ -630,9 +629,9 @@ dependencies = [ [[package]] name = "base62" -version = "2.2.4" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd637ac531c60eb7fbc4684dc061c2d7d90d73d758181aa02eeff0464b9eee4b" +checksum = "a3a647dce9631f152a99ec16cfc09267ae55fe280d5dc288964027d458076323" [[package]] name = "base64" @@ -640,6 +639,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "bech32" version = "0.12.0" @@ -661,7 +666,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "cexpr", "clang-sys", "itertools 0.13.0", @@ -778,9 +783,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.13.1" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" dependencies = [ "serde_core", ] @@ -950,7 +955,7 @@ version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "polling", "rustix", "slab", @@ -1166,7 +1171,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "block", "cocoa-foundation 0.2.1", "core-foundation 0.10.1", @@ -1196,7 +1201,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81411967c50ee9a1fc11365f8c585f863a22a9697c89239c452292c40ba79b0d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "block", "core-foundation 0.10.1", "core-graphics-types 0.2.0", @@ -1217,7 +1222,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870e269ad88802147f2baec3086abb67d17260a" +source = "git+https://github.com/zed-industries/zed#6ad3c7f278e513875e0bbde5ee935f6c9a342b9c" dependencies = [ "gpui_util", "indexmap", @@ -1242,9 +1247,9 @@ dependencies = [ [[package]] name = "compression-codecs" -version = "0.4.39" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fe67f2944eef52fc7b106b8c9450d243a88701a0c065f7f57235e76abaed7df" +checksum = "58a6d0db8759036a783bc7c3f7a07f8cef3bf9470eb1db3bc86e8bcd1c5d0fe8" dependencies = [ "bzip2", "compression-core", @@ -1366,7 +1371,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "core-foundation 0.10.1", "core-graphics-types 0.2.0", "foreign-types", @@ -1379,7 +1384,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32eb7c354ae9f6d437a6039099ce7ecd049337a8109b23d73e48e8ffba8e9cd5" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "core-foundation 0.9.4", "core-graphics-types 0.1.3", "foreign-types", @@ -1403,7 +1408,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "core-foundation 0.10.1", "libc", ] @@ -1414,7 +1419,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4416167a69126e617f8d0a214af0e3c1dbdeffcb100ddf72dcd1a1ac9893c146" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "block", "cfg-if", "core-foundation 0.10.1", @@ -1447,6 +1452,12 @@ dependencies = [ "metal", ] +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + [[package]] name = "core_maths" version = "0.1.1" @@ -1462,7 +1473,7 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be17b688510d934ce13f48a2beba700e11583e281e0fda99c22bb256a14eda73" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "fontdb", "harfrust", "linebender_resource_handle", @@ -1683,7 +1694,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870e269ad88802147f2baec3086abb67d17260a" +source = "git+https://github.com/zed-industries/zed#6ad3c7f278e513875e0bbde5ee935f6c9a342b9c" dependencies = [ "proc-macro2", "quote", @@ -1755,7 +1766,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "block2 0.6.2", "libc", "objc2 0.6.4", @@ -1861,11 +1872,17 @@ dependencies = [ [[package]] name = "encoding_rs" -version = "0.8.35" +version = "0.8.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +checksum = "7b5ef0006ac9ab233c38522f5ae99cae3625151de8f706cacee1cba4b8e2832a" dependencies = [ "cfg-if", + "core_detect", + "multiversion", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", ] [[package]] @@ -2122,7 +2139,7 @@ version = "25.12.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35f6839d7b3b98adde531effaf34f0c2badc6f4735d26fe74709d8e513a96ef3" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "rustc_version", ] @@ -2216,9 +2233,9 @@ dependencies = [ [[package]] name = "font-types" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64eb721ca85a34323425f4041adc5d82704d3782d5f8f03793bc012419dce23" +checksum = "b8eb065f3251655b3c90e22e5e363f310fc5332fb3402e37bbc94752283248f6" dependencies = [ "bytemuck", ] @@ -2676,7 +2693,7 @@ version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f6af5321bfd3711a279d6b244d58532ba1cfabf9eb6374791f19929d8970082" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "bstr", "gix-path", "libc", @@ -2841,7 +2858,7 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b417cf515fd8c91468b578071f76d6cba716f8a1eccd853906bff4908b2c1413" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "bstr", "gix-features", "gix-path", @@ -2899,7 +2916,7 @@ version = "0.55.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "632e16cb48b0e88a747e106924cb2765194105d8070a7a961b0d080ed806c632" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "bstr", "filetime", "fnv", @@ -2949,7 +2966,7 @@ version = "0.35.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91068ecee956121d663635b873ae87ad9c5d3f2d6788facfd12829140b39c897" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "gix-commitgraph", "gix-date", "gix-hash", @@ -3064,7 +3081,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c92e44c63ba53bb55aa88ee48847664beae9446417621582fb14dfc69b2f8c72" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "bstr", "gix-attributes", "gix-config-value", @@ -3162,7 +3179,7 @@ version = "0.49.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4577b864c3e134697e91564553e43230c2e401bb1bcc51cfa998edca21c95078" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "bstr", "gix-commitgraph", "gix-date", @@ -3197,7 +3214,7 @@ version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af4fe6c152c1d50aea36f299825702cd37e303307832fec1d0fdd5844e47ce2f" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "gix-path", "libc", "windows-sys 0.61.2", @@ -3281,7 +3298,7 @@ version = "0.59.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3f41a64939953ff49117df4eb91334d299abc59ff57a90fb2193d8ffa5886f1" dependencies = [ - "base64", + "base64 0.22.1", "bstr", "gix-command", "gix-credentials", @@ -3302,7 +3319,7 @@ version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9af3503668739f4de5dba57fe15cfd9adc443b08bcbbf195e5de16b648872245" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "gix-commitgraph", "gix-date", "gix-hash", @@ -3505,7 +3522,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "gpu-descriptor-types", "hashbrown 0.15.5", ] @@ -3516,13 +3533,13 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", ] [[package]] name = "gpui" version = "0.2.2" -source = "git+https://github.com/zed-industries/zed#1870e269ad88802147f2baec3086abb67d17260a" +source = "git+https://github.com/zed-industries/zed#6ad3c7f278e513875e0bbde5ee935f6c9a342b9c" dependencies = [ "accesskit", "anyhow", @@ -3530,7 +3547,7 @@ dependencies = [ "async-task", "backtrace", "bindgen", - "bitflags 2.13.1", + "bitflags 2.13.2", "chrono", "collections", "core-video", @@ -3745,7 +3762,7 @@ dependencies = [ [[package]] name = "gpui_apple" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870e269ad88802147f2baec3086abb67d17260a" +source = "git+https://github.com/zed-industries/zed#6ad3c7f278e513875e0bbde5ee935f6c9a342b9c" dependencies = [ "anyhow", "block", @@ -3768,14 +3785,14 @@ dependencies = [ [[package]] name = "gpui_linux" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870e269ad88802147f2baec3086abb67d17260a" +source = "git+https://github.com/zed-industries/zed#6ad3c7f278e513875e0bbde5ee935f6c9a342b9c" dependencies = [ "accesskit", "accesskit_unix", "anyhow", "as-raw-xcb-connection", "ashpd", - "bitflags 2.13.1", + "bitflags 2.13.2", "bytemuck", "calloop", "calloop-wayland-source", @@ -3814,7 +3831,7 @@ dependencies = [ [[package]] name = "gpui_macos" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870e269ad88802147f2baec3086abb67d17260a" +source = "git+https://github.com/zed-industries/zed#6ad3c7f278e513875e0bbde5ee935f6c9a342b9c" dependencies = [ "accesskit", "accesskit_macos", @@ -3860,7 +3877,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870e269ad88802147f2baec3086abb67d17260a" +source = "git+https://github.com/zed-industries/zed#6ad3c7f278e513875e0bbde5ee935f6c9a342b9c" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -3871,7 +3888,7 @@ dependencies = [ [[package]] name = "gpui_platform" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870e269ad88802147f2baec3086abb67d17260a" +source = "git+https://github.com/zed-industries/zed#6ad3c7f278e513875e0bbde5ee935f6c9a342b9c" dependencies = [ "console_error_panic_hook", "gpui", @@ -3884,7 +3901,7 @@ dependencies = [ [[package]] name = "gpui_shared_string" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870e269ad88802147f2baec3086abb67d17260a" +source = "git+https://github.com/zed-industries/zed#6ad3c7f278e513875e0bbde5ee935f6c9a342b9c" dependencies = [ "schemars", "serde", @@ -3894,7 +3911,7 @@ dependencies = [ [[package]] name = "gpui_util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870e269ad88802147f2baec3086abb67d17260a" +source = "git+https://github.com/zed-industries/zed#6ad3c7f278e513875e0bbde5ee935f6c9a342b9c" dependencies = [ "anyhow", "log", @@ -3904,7 +3921,7 @@ dependencies = [ [[package]] name = "gpui_web" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870e269ad88802147f2baec3086abb67d17260a" +source = "git+https://github.com/zed-industries/zed#6ad3c7f278e513875e0bbde5ee935f6c9a342b9c" dependencies = [ "anyhow", "console_error_panic_hook", @@ -3928,7 +3945,7 @@ dependencies = [ [[package]] name = "gpui_wgpu" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870e269ad88802147f2baec3086abb67d17260a" +source = "git+https://github.com/zed-industries/zed#6ad3c7f278e513875e0bbde5ee935f6c9a342b9c" dependencies = [ "anyhow", "bytemuck", @@ -3954,7 +3971,7 @@ dependencies = [ [[package]] name = "gpui_windows" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870e269ad88802147f2baec3086abb67d17260a" +source = "git+https://github.com/zed-industries/zed#6ad3c7f278e513875e0bbde5ee935f6c9a342b9c" dependencies = [ "accesskit", "accesskit_windows", @@ -3981,9 +3998,9 @@ dependencies = [ [[package]] name = "granit-parser" -version = "0.0.7" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d03f81ad4732830d85cfd417a9f62cde6dadda4354d37d078a6084a19560aa2d" +checksum = "65ec0d45986cd51c847c75c5b69a00852c4fc84d0e5e79f041173f73437d0cdf" dependencies = [ "arraydeque", "smallvec", @@ -4026,7 +4043,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9da2e5ae821f6e96664977bf974d6d6a2d6682f9ccee23e62ec1d134246845f9" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "bytemuck", "core_maths", "read-fonts 0.37.0", @@ -4127,7 +4144,7 @@ version = "0.20.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d4f449bab7320c56003d37732a917e18798e2f1709d80263face2b4f9436ddb" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "byteorder", "heed-traits", "heed-types", @@ -4268,7 +4285,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870e269ad88802147f2baec3086abb67d17260a" +source = "git+https://github.com/zed-industries/zed#6ad3c7f278e513875e0bbde5ee935f6c9a342b9c" dependencies = [ "anyhow", "async-compression", @@ -4288,7 +4305,7 @@ dependencies = [ [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870e269ad88802147f2baec3086abb67d17260a" +source = "git+https://github.com/zed-industries/zed#6ad3c7f278e513875e0bbde5ee935f6c9a342b9c" dependencies = [ "log", "rustls", @@ -4304,9 +4321,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hybrid-array" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" dependencies = [ "typenum", ] @@ -4354,7 +4371,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-util", @@ -4894,7 +4911,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "libc", ] @@ -5244,7 +5261,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870e269ad88802147f2baec3086abb67d17260a" +source = "git+https://github.com/zed-industries/zed#6ad3c7f278e513875e0bbde5ee935f6c9a342b9c" dependencies = [ "anyhow", "bindgen", @@ -5285,7 +5302,7 @@ version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7047791b5bc903b8cd963014b355f71dc9864a9a0b727057676c1dcae5cbc15" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "block", "core-graphics-types 0.2.0", "foreign-types", @@ -5358,6 +5375,33 @@ dependencies = [ "pxfm", ] +[[package]] +name = "multiversion" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ca4bea16ffc3f443cf7d866912118196bfef4c6a1556ca00f9f9b00bb43f7c" +dependencies = [ + "multiversion-macros", +] + +[[package]] +name = "multiversion-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d416831a7317ef4b08bee00b69cbbb9c8763da7959a7026244d6266869f9c83" +dependencies = [ + "proc-macro2", + "quote", + "rustversion", + "syn 3.0.5", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + [[package]] name = "naga" version = "29.0.4" @@ -5366,7 +5410,7 @@ checksum = "b2bf919621e7975acb27d881bae2fb993e0d45c8e0446e85e6272971e00dc8df" dependencies = [ "arrayvec", "bit-set 0.9.1", - "bitflags 2.13.1", + "bitflags 2.13.2", "cfg-if", "cfg_aliases", "codespan-reporting", @@ -5469,10 +5513,10 @@ dependencies = [ [[package]] name = "nostr" version = "0.45.4" -source = "git+https://github.com/rust-nostr/nostr#0c6fad2ac8ce934747096953f6dba355e3532614" +source = "git+https://github.com/rust-nostr/nostr#5c669a498e5f8dd1ec5713b11fdb92bcd858ad73" dependencies = [ "aes", - "base64", + "base64 0.22.1", "bech32", "bip39", "bitcoin_hashes 1.2.0", @@ -5495,7 +5539,7 @@ dependencies = [ [[package]] name = "nostr-connect" version = "0.45.1" -source = "git+https://github.com/rust-nostr/nostr#0c6fad2ac8ce934747096953f6dba355e3532614" +source = "git+https://github.com/rust-nostr/nostr#5c669a498e5f8dd1ec5713b11fdb92bcd858ad73" dependencies = [ "async-utility", "futures-core", @@ -5509,7 +5553,7 @@ dependencies = [ [[package]] name = "nostr-database" version = "0.45.1" -source = "git+https://github.com/rust-nostr/nostr#0c6fad2ac8ce934747096953f6dba355e3532614" +source = "git+https://github.com/rust-nostr/nostr#5c669a498e5f8dd1ec5713b11fdb92bcd858ad73" dependencies = [ "nostr", "opaquerr", @@ -5518,7 +5562,7 @@ dependencies = [ [[package]] name = "nostr-gossip" version = "0.45.0" -source = "git+https://github.com/rust-nostr/nostr#0c6fad2ac8ce934747096953f6dba355e3532614" +source = "git+https://github.com/rust-nostr/nostr#5c669a498e5f8dd1ec5713b11fdb92bcd858ad73" dependencies = [ "nostr", "opaquerr", @@ -5527,7 +5571,7 @@ dependencies = [ [[package]] name = "nostr-gossip-memory" version = "0.45.0" -source = "git+https://github.com/rust-nostr/nostr#0c6fad2ac8ce934747096953f6dba355e3532614" +source = "git+https://github.com/rust-nostr/nostr#5c669a498e5f8dd1ec5713b11fdb92bcd858ad73" dependencies = [ "indexmap", "lru", @@ -5539,7 +5583,7 @@ dependencies = [ [[package]] name = "nostr-lmdb" version = "0.45.2" -source = "git+https://github.com/rust-nostr/nostr#0c6fad2ac8ce934747096953f6dba355e3532614" +source = "git+https://github.com/rust-nostr/nostr#5c669a498e5f8dd1ec5713b11fdb92bcd858ad73" dependencies = [ "async-utility", "flatbuffers", @@ -5554,7 +5598,7 @@ dependencies = [ [[package]] name = "nostr-memory" version = "0.45.1" -source = "git+https://github.com/rust-nostr/nostr#0c6fad2ac8ce934747096953f6dba355e3532614" +source = "git+https://github.com/rust-nostr/nostr#5c669a498e5f8dd1ec5713b11fdb92bcd858ad73" dependencies = [ "btreecap", "nostr", @@ -5565,7 +5609,7 @@ dependencies = [ [[package]] name = "nostr-sdk" version = "0.45.2" -source = "git+https://github.com/rust-nostr/nostr#0c6fad2ac8ce934747096953f6dba355e3532614" +source = "git+https://github.com/rust-nostr/nostr#5c669a498e5f8dd1ec5713b11fdb92bcd858ad73" dependencies = [ "async-utility", "async-wsocket", @@ -5590,7 +5634,7 @@ version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "filetime", "fsevent-sys", "inotify", @@ -5814,7 +5858,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "block2 0.5.1", "libc", "objc2 0.5.2", @@ -5830,7 +5874,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "block2 0.6.2", "libc", "objc2 0.6.4", @@ -5851,7 +5895,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "objc2 0.6.4", "objc2-foundation 0.3.2", ] @@ -5862,7 +5906,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -5874,7 +5918,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "objc2 0.6.4", "objc2-foundation 0.3.2", ] @@ -5885,7 +5929,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "dispatch2", "objc2 0.6.4", ] @@ -5896,7 +5940,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "dispatch2", "objc2 0.6.4", "objc2-core-foundation", @@ -5941,7 +5985,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "objc2 0.6.4", "objc2-core-foundation", "objc2-core-graphics", @@ -5953,7 +5997,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "objc2 0.6.4", "objc2-core-foundation", "objc2-core-graphics", @@ -5972,7 +6016,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "block2 0.5.1", "libc", "objc2 0.5.2", @@ -5984,7 +6028,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "block2 0.6.2", "libc", "objc2 0.6.4", @@ -6007,7 +6051,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "objc2 0.6.4", "objc2-core-foundation", ] @@ -6018,7 +6062,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -6030,7 +6074,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "block2 0.6.2", "objc2 0.6.4", "objc2-foundation 0.3.2", @@ -6042,7 +6086,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -6055,7 +6099,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "objc2 0.6.4", "objc2-core-foundation", "objc2-foundation 0.3.2", @@ -6068,7 +6112,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "block2 0.6.2", "objc2 0.6.4", "objc2-core-location", @@ -6157,9 +6201,9 @@ checksum = "4f933a4265d5cdad61d19bbdfc972ea5726d56cd8d3d57b8f2d3c365dd42bee9" [[package]] name = "open" -version = "5.4.3" +version = "5.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c603ab8300cf18bc3b14146b19fe3dfcc4843ae5a400cd0e7a30b95aa366634" +checksum = "aa576c76302b7b808eecc68061e67336c47833ef9d22caa74dda10fa9675eebc" dependencies = [ "is-wsl", "libc", @@ -6302,7 +6346,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perf" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870e269ad88802147f2baec3086abb67d17260a" +source = "git+https://github.com/zed-industries/zed#6ad3c7f278e513875e0bbde5ee935f6c9a342b9c" dependencies = [ "collections", "serde", @@ -6472,7 +6516,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "crc32fast", "fdeflate", "flate2", @@ -6656,7 +6700,7 @@ source = "git+https://github.com/proptest-rs/proptest?rev=3dca198a8fef1b32e3a66f dependencies = [ "bit-set 0.8.0", "bit-vec 0.8.0", - "bitflags 2.13.1", + "bitflags 2.13.2", "num-traits", "proptest-macro", "rand 0.9.5", @@ -6978,7 +7022,7 @@ version = "11.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", ] [[package]] @@ -7037,7 +7081,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709" dependencies = [ "bytemuck", - "font-types 0.12.4", + "font-types 0.12.5", "once_cell", ] @@ -7053,7 +7097,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", ] [[package]] @@ -7090,7 +7134,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870e269ad88802147f2baec3086abb67d17260a" +source = "git+https://github.com/zed-industries/zed#6ad3c7f278e513875e0bbde5ee935f6c9a342b9c" dependencies = [ "derive_refineable", ] @@ -7132,11 +7176,11 @@ checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" [[package]] name = "reqwest" -version = "0.13.4" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" dependencies = [ - "base64", + "base64 0.23.1", "bytes", "encoding_rs", "futures-channel", @@ -7173,7 +7217,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870e269ad88802147f2baec3086abb67d17260a" +source = "git+https://github.com/zed-industries/zed#6ad3c7f278e513875e0bbde5ee935f6c9a342b9c" dependencies = [ "anyhow", "bytes", @@ -7307,9 +7351,9 @@ dependencies = [ [[package]] name = "rust-i18n" -version = "4.2.1" +version = "4.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f10cee36dd3b1f7929ea12b759de9eea9eff83bfccbc71f387ef4d41a57c64a4" +checksum = "7c43fd69c20da13756643a5bf51ae799965465c78e05c57a1242a736aaa56527" dependencies = [ "globwalk", "regex", @@ -7320,9 +7364,9 @@ dependencies = [ [[package]] name = "rust-i18n-macro" -version = "4.2.1" +version = "4.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0bb1ed4e04fe26c2a2652cad1c6595efaf7196f4445c0d6e13c67154347fb7e" +checksum = "086ec8a9eaa6afda33919b9bc1661c1f4e017c40e99bbd33ca98f6e1cb6ca91f" dependencies = [ "glob", "proc-macro2", @@ -7335,9 +7379,9 @@ dependencies = [ [[package]] name = "rust-i18n-support" -version = "4.2.1" +version = "4.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba1c083408a2733180ae0acf2897612f8ceec7b0c0dcd065a0a87103bfb3b1d9" +checksum = "ac02dcb9a01ec145d0a5534ea9554a966050ca250b346bb2a9ab0fcdafab5979" dependencies = [ "arc-swap", "base62", @@ -7385,7 +7429,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "errno", "libc", "linux-raw-sys", @@ -7394,9 +7438,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.43" +version = "0.23.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +checksum = "6725596c3f2c3a0aef021139e145d4eafe314a6623e4680ca83852b2c67ab2ba" dependencies = [ "aws-lc-rs", "log", @@ -7502,7 +7546,7 @@ version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "bytemuck", "core_maths", "log", @@ -7551,7 +7595,7 @@ dependencies = [ [[package]] name = "scheduler" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870e269ad88802147f2baec3086abb67d17260a" +source = "git+https://github.com/zed-industries/zed#6ad3c7f278e513875e0bbde5ee935f6c9a342b9c" dependencies = [ "async-task", "backtrace", @@ -7669,7 +7713,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -7714,15 +7758,13 @@ dependencies = [ [[package]] name = "serde-saphyr" -version = "0.0.29" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd22781911de0ca6debda95f073c8f18bec65d1a94f1fa9573f3102e514cea4" +checksum = "3afb591f9cdb6223c88ba39269aff895620c7f0716dc42b705b5733d5c7c0823" dependencies = [ - "ahash", "annotate-snippets", - "base64", + "base64 0.23.1", "encoding_rs_io", - "getrandom 0.3.4", "granit-parser", "nohash-hasher", "num-traits", @@ -7960,6 +8002,8 @@ name = "signed_core" version = "0.1.0-alpha" dependencies = [ "nostr", + "serde", + "serde_json", ] [[package]] @@ -8108,9 +8152,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.15.2" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f" [[package]] name = "smol" @@ -8173,7 +8217,7 @@ version = "0.4.0+sdk-1.4.341.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", ] [[package]] @@ -8264,7 +8308,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870e269ad88802147f2baec3086abb67d17260a" +source = "git+https://github.com/zed-industries/zed#6ad3c7f278e513875e0bbde5ee935f6c9a342b9c" dependencies = [ "heapless 0.9.3", "log", @@ -8501,7 +8545,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -8512,7 +8556,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "501336eb7ba9e417300a6a0fa985721065467aa83a6dcf0422a8e43e4c0328fa" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "core-foundation 0.10.1", "system-configuration-sys", ] @@ -8949,7 +8993,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "bytes", "futures-util", "http", @@ -9599,7 +9643,7 @@ version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" dependencies = [ - "base64", + "base64 0.22.1", "data-url", "flate2", "fontdb", @@ -9626,7 +9670,7 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e419dff010bb12512b0ae9e3d2f318dfbdf0167fde7eb05465134d4e8756076f" dependencies = [ - "base64", + "base64 0.22.1", "data-url", "flate2", "fontdb", @@ -9662,7 +9706,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "util_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870e269ad88802147f2baec3086abb67d17260a" +source = "git+https://github.com/zed-industries/zed#6ad3c7f278e513875e0bbde5ee935f6c9a342b9c" dependencies = [ "perf", "quote", @@ -9678,9 +9722,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.26.0" +version = "1.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -9917,7 +9961,7 @@ version = "0.31.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "rustix", "wayland-backend", "wayland-scanner", @@ -9940,7 +9984,7 @@ version = "0.32.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "wayland-backend", "wayland-client", "wayland-scanner", @@ -9952,7 +9996,7 @@ version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "wayland-backend", "wayland-client", "wayland-protocols", @@ -9965,7 +10009,7 @@ version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "wayland-backend", "wayland-client", "wayland-protocols", @@ -10071,7 +10115,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76e8840e1ba2881d4cbb18d2147627a56af426ff064c0401eb0c8410c6325d07" dependencies = [ "arrayvec", - "bitflags 2.13.1", + "bitflags 2.13.2", "bytemuck", "cfg-if", "cfg_aliases", @@ -10103,7 +10147,7 @@ dependencies = [ "arrayvec", "bit-set 0.9.1", "bit-vec 0.9.1", - "bitflags 2.13.1", + "bitflags 2.13.2", "bytemuck", "cfg_aliases", "document-features", @@ -10174,7 +10218,7 @@ dependencies = [ "arrayvec", "ash", "bit-set 0.9.1", - "bitflags 2.13.1", + "bitflags 2.13.2", "block2 0.6.2", "bytemuck", "cfg-if", @@ -10234,7 +10278,7 @@ version = "29.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84bf84cd9ca8ca45e2b223a3868f1adf9bfc0c66aeac212e76ee7e40fdadf8f5" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "bytemuck", "js-sys", "log", @@ -10932,7 +10976,7 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6c2ad15e0e922856ee89afe862b8992334bbe7953adad56cd1199358cb30566" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "libc", "quick-xml", "x11", @@ -10957,7 +11001,7 @@ name = "xim-parser" version = "0.2.1" source = "git+https://github.com/zed-industries/xim-rs.git?rev=16f35a2c881b815a2b6cdfd6687988e84f8447d8#16f35a2c881b815a2b6cdfd6687988e84f8447d8" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", ] [[package]] @@ -11158,7 +11202,7 @@ name = "zed-font-kit" version = "0.14.1-zed" source = "git+https://github.com/zed-industries/font-kit?rev=94b0f28166665e8fd2f53ff6d268a14955c82269#94b0f28166665e8fd2f53ff6d268a14955c82269" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "byteorder", "core-foundation 0.10.1", "core-graphics 0.24.0", @@ -11182,7 +11226,7 @@ name = "zed-reqwest" version = "0.12.15-zed" source = "git+https://github.com/zed-industries/reqwest.git?rev=33bc764aa15ff7b200bf7c93bd96e24878d53e14#33bc764aa15ff7b200bf7c93bd96e24878d53e14" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "encoding_rs", "futures-core", @@ -11231,7 +11275,7 @@ name = "zed-reqwest" version = "0.12.15-zed" source = "git+https://github.com/zed-industries/reqwest.git?rev=c15662463bda39148ba154100dd44d3fba5873a4#c15662463bda39148ba154100dd44d3fba5873a4" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "encoding_rs", "futures-core", @@ -11329,18 +11373,18 @@ checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" [[package]] name = "zerocopy" -version = "0.8.56" +version = "0.8.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.56" +version = "0.8.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc" dependencies = [ "proc-macro2", "quote", @@ -11430,7 +11474,7 @@ checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" [[package]] name = "zlog" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870e269ad88802147f2baec3086abb67d17260a" +source = "git+https://github.com/zed-industries/zed#6ad3c7f278e513875e0bbde5ee935f6c9a342b9c" dependencies = [ "anyhow", "chrono", @@ -11447,7 +11491,7 @@ checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "ztracing" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870e269ad88802147f2baec3086abb67d17260a" +source = "git+https://github.com/zed-industries/zed#6ad3c7f278e513875e0bbde5ee935f6c9a342b9c" dependencies = [ "tracing", "tracing-subscriber", @@ -11458,7 +11502,7 @@ dependencies = [ [[package]] name = "ztracing_macro" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870e269ad88802147f2baec3086abb67d17260a" +source = "git+https://github.com/zed-industries/zed#6ad3c7f278e513875e0bbde5ee935f6c9a342b9c" [[package]] name = "zune-core" diff --git a/crates/signed_core/Cargo.toml b/crates/signed_core/Cargo.toml index 5b1e5ca..7328213 100644 --- a/crates/signed_core/Cargo.toml +++ b/crates/signed_core/Cargo.toml @@ -6,3 +6,7 @@ publish.workspace = true [dependencies] nostr.workspace = true +serde.workspace = true + +[dev-dependencies] +serde_json.workspace = true diff --git a/crates/signed_core/src/filters.rs b/crates/signed_core/src/filters.rs index e6485cf..5b040a3 100644 --- a/crates/signed_core/src/filters.rs +++ b/crates/signed_core/src/filters.rs @@ -2,7 +2,7 @@ use std::time::Duration; use nostr::prelude::*; -use crate::RepoAddr; +use crate::{COVER_NOTE_KIND, RepoAddr}; /// Kinds that make up the activity of a repository. pub const ACTIVITY_KINDS: [Kind; 9] = [ @@ -17,6 +17,41 @@ pub const ACTIVITY_KINDS: [Kind; 9] = [ Kind::GitStatusDraft, ]; +/// Kinds that notify a user when they tag them via their `p` tag. +pub const NOTIFICATION_KINDS: [Kind; 9] = [ + Kind::GitIssue, + Kind::GitPullRequest, + Kind::GitPatch, + Kind::GitPullRequestUpdate, + COVER_NOTE_KIND, + Kind::GitStatusOpen, + Kind::GitStatusApplied, + Kind::GitStatusClosed, + Kind::GitStatusDraft, +]; + +/// Git root kinds that make a comment or cover note count as git activity. +const GIT_ROOT_KINDS: [Kind; 4] = [ + Kind::GitIssue, + Kind::GitPatch, + Kind::GitPullRequest, + Kind::GitRepoAnnouncement, +]; + +/// Value of the first tag named `name` on `event`. +fn tag_value<'a>(event: &'a Event, name: &str) -> Option<&'a str> { + event + .tags + .iter() + .find(|tag| tag.kind() == name) + .and_then(|tag| tag.content()) +} + +/// Kind named by the first tag `name` on `event`. +fn tag_kind(event: &Event, name: &str) -> Option { + tag_value(event, name)?.parse::().ok() +} + /// Latest announcement event for a repository. pub fn announcement(addr: &RepoAddr) -> Filter { Filter::new() @@ -94,6 +129,68 @@ pub fn comments_for(roots: impl IntoIterator) -> Vec { ] } +/// NIP-22 comments on our issues, patches and pull requests. +/// They are matched via the uppercase `P` and `K` tags, not authorship. +pub fn notification_comments(me: PublicKey) -> Filter { + Filter::new() + .kind(Kind::Comment) + .custom_tags(SingleLetterTag::UPPERCASE_P, [me.to_hex()]) + .custom_tags(SingleLetterTag::UPPERCASE_K, ["1621", "1617", "1618"]) +} + +/// Activity directed at us: comments on our roots, and git events tagging us +/// via their lowercase `p` tag. `Filter::pubkey` sets that `p` tag. +pub fn notifications(me: PublicKey) -> Vec { + vec![ + notification_comments(me), + Filter::new().kinds(NOTIFICATION_KINDS).pubkey(me), + ] +} + +/// Git activity authored by `me`, for "Continue where you left off". +/// +/// A comment on an unrelated kind is matched too, so results must be filtered +/// through [`is_git_activity`] before display. +pub fn authored_activity(me: PublicKey) -> Filter { + Filter::new() + .kinds( + ACTIVITY_KINDS + .into_iter() + .chain(std::iter::once(COVER_NOTE_KIND)), + ) + .author(me) +} + +/// Whether a kind-1111 comment targets a git root, checked via its `K` tag. +fn is_git_comment(event: &Event) -> bool { + event.kind == Kind::Comment + && tag_kind(event, "K").is_some_and(|kind| GIT_ROOT_KINDS.contains(&kind)) +} + +/// Whether a kind-1624 cover note targets a git root, checked via its `k` tag. +fn is_git_cover_note(event: &Event) -> bool { + event.kind == COVER_NOTE_KIND + && tag_kind(event, "k").is_some_and(|kind| GIT_ROOT_KINDS.contains(&kind)) +} + +/// Whether a status event references a git root, checked via its `k` tag. +fn is_git_status(event: &Event) -> bool { + tag_kind(event, "k").is_some_and(|kind| GIT_ROOT_KINDS.contains(&kind)) +} + +/// Whether `event` is git activity worth showing in the activity list. +pub fn is_git_activity(event: &Event) -> bool { + match event.kind { + Kind::GitIssue | Kind::GitPatch | Kind::GitPullRequest => true, + Kind::Comment => is_git_comment(event), + Kind::GitStatusOpen + | Kind::GitStatusApplied + | Kind::GitStatusClosed + | Kind::GitStatusDraft => is_git_status(event), + kind => kind == COVER_NOTE_KIND && is_git_cover_note(event), + } +} + /// All repository announcements, for global discovery. pub fn all_announcements() -> Filter { Filter::new().kind(Kind::GitRepoAnnouncement) @@ -134,3 +231,94 @@ pub fn deletions_for_repo(addr: &RepoAddr) -> Vec { Filter::new().kind(Kind::EventDeletion).coordinate(addr), ] } + +#[cfg(test)] +mod tests { + use super::*; + + fn keys(seed: u8) -> Keys { + let mut hex = "00000000000000000000000000000000000000000000000000000000000000".to_string(); + hex.push_str(&format!("{seed:02x}")); + Keys::new(SecretKey::from_hex(&hex).expect("valid secret key")) + } + + fn signed(author: &Keys, kind: Kind, tags: Vec) -> Event { + EventBuilder::new(kind, "") + .tags(tags) + .finalize(author) + .expect("signed event") + } + + fn kind_tag(name: &str, kind: Kind) -> Tag { + Tag::parse([name, &kind.as_u16().to_string()]).expect("valid kind tag") + } + + #[test] + fn root_git_kinds_are_activity() { + for kind in [Kind::GitIssue, Kind::GitPatch, Kind::GitPullRequest] { + assert!(is_git_activity(&signed(&keys(1), kind, Vec::new()))); + } + } + + #[test] + fn comment_activity_depends_on_the_uppercase_k_tag() { + let on_git = signed(&keys(1), Kind::Comment, vec![kind_tag("K", Kind::GitIssue)]); + let on_repo = signed( + &keys(1), + Kind::Comment, + vec![kind_tag("K", Kind::GitRepoAnnouncement)], + ); + let on_note = signed(&keys(1), Kind::Comment, vec![kind_tag("K", Kind::TextNote)]); + + assert!(is_git_activity(&on_git)); + assert!(is_git_activity(&on_repo)); + assert!(!is_git_activity(&on_note)); + assert!(!is_git_activity(&signed( + &keys(1), + Kind::Comment, + Vec::new() + ))); + } + + #[test] + fn status_and_cover_note_activity_depend_on_the_lowercase_k_tag() { + let status = signed( + &keys(1), + Kind::GitStatusClosed, + vec![kind_tag("k", Kind::GitPullRequest)], + ); + let cover = signed( + &keys(1), + COVER_NOTE_KIND, + vec![kind_tag("k", Kind::GitPatch)], + ); + let unrelated = signed( + &keys(1), + Kind::GitStatusClosed, + vec![kind_tag("k", Kind::Metadata)], + ); + + assert!(is_git_activity(&status)); + assert!(is_git_activity(&cover)); + assert!(!is_git_activity(&unrelated)); + assert!(!is_git_activity(&signed( + &keys(1), + Kind::GitStatusClosed, + Vec::new() + ))); + } + + #[test] + fn non_git_kinds_are_not_activity() { + assert!(!is_git_activity(&signed( + &keys(1), + Kind::TextNote, + Vec::new() + ))); + assert!(!is_git_activity(&signed( + &keys(1), + Kind::GitPullRequestUpdate, + Vec::new(), + ))); + } +} diff --git a/crates/signed_core/src/inbox.rs b/crates/signed_core/src/inbox.rs new file mode 100644 index 0000000..4a07e24 --- /dev/null +++ b/crates/signed_core/src/inbox.rs @@ -0,0 +1,645 @@ +use std::collections::{HashMap, HashSet}; +use std::time::Duration; + +use nostr::prelude::*; +use serde::{Deserialize, Serialize}; + +use crate::{COVER_NOTE_KIND, RepoAddr}; + +/// Window before `now` that an advanced cutoff retreats to. +const ADVANCE_WINDOW: Duration = Duration::from_secs(3 * 24 * 60 * 60); + +/// Window before `now` that a mark-all cutoff retreats to. +const MARK_ALL_WINDOW: Duration = Duration::from_secs(10 * 24 * 60 * 60); + +/// A thread of notification events sharing one root +#[derive(Debug, Clone)] +pub struct InboxItem { + /// The root issue, patch or pull request the notifications belong to. + pub root: EventId, + /// Kind of the root event, when it is known locally. + pub root_kind: Option, + /// Repository the root belongs to, from the root's `a` tag. + pub address: Option, + /// Events in the group, newest first. + pub events: Vec, + /// Unread event ids, oldest first. + pub unread_ids: Vec, + /// Whether every event in the group is archived. + pub archived: bool, +} + +impl InboxItem { + /// Timestamp of the newest event in the group. + pub fn latest_activity(&self) -> Timestamp { + self.events + .first() + .map(|event| event.created_at) + .unwrap_or_default() + } + + /// Whether the group has an unread event still visible in the inbox. + pub fn is_unread(&self) -> bool { + !self.archived && !self.unread_ids.is_empty() + } +} + +/// Root issue, patch or pull request of a notification event. +/// +/// Returns `None` when the event is not git-related, or when its root is a +/// coordinate rather than an event. +/// +/// - issue (1621) / PR (1618): itself +/// - patch (1617): its `e` parent patch, else itself +/// - NIP-22 comment (1111): uppercase `E` root pointer +/// - PR update (1619): uppercase `E` +/// - statuses (1630-1633) / cover note (1624): NIP-10 root `e` +pub fn notification_root(event: &Event, lookup: &L) -> Option +where + L: Fn(EventId) -> Option, +{ + if event.kind == COVER_NOTE_KIND { + return nip10_root_id(event).map(|root| resolve_thread_root(root, lookup)); + } + match event.kind { + Kind::GitIssue | Kind::GitPullRequest => Some(event.id), + Kind::GitPatch => Some(match first_e_id(event) { + Some(parent) => resolve_thread_root(parent, lookup), + None => event.id, + }), + Kind::Comment => match nip22::extract_root(event) { + Some(CommentTarget::Event { id, .. }) => Some(resolve_thread_root(id, lookup)), + _ => None, + }, + Kind::GitPullRequestUpdate => { + first_uppercase_e_id(event).map(|root| resolve_thread_root(root, lookup)) + } + Kind::GitStatusOpen + | Kind::GitStatusApplied + | Kind::GitStatusClosed + | Kind::GitStatusDraft => { + nip10_root_id(event).map(|root| resolve_thread_root(root, lookup)) + } + _ => None, + } +} + +/// Group notification events by root, newest activity first. +pub fn group(events: E, me: PublicKey, state: &InboxReadState, lookup: &L) -> Vec +where + E: IntoIterator, + L: Fn(EventId) -> Option, +{ + let mut groups: HashMap> = HashMap::new(); + for event in events { + if event.pubkey == me { + continue; + } + let Some(root) = notification_root(&event, lookup) else { + continue; + }; + groups.entry(root).or_default().push(event); + } + + let mut items: Vec = groups + .into_iter() + .map(|(root, mut events)| { + events.sort_by(|a, b| { + b.created_at + .cmp(&a.created_at) + .then_with(|| b.id.to_hex().cmp(&a.id.to_hex())) + }); + + let root_event = lookup(root); + + let unread_ids = events + .iter() + .rev() + .filter(|event| !state.is_read(event)) + .map(|event| event.id) + .collect(); + + let archived = events.iter().all(|event| state.is_archived(event)); + + InboxItem { + root, + root_kind: root_event.as_ref().map(|event| event.kind), + address: root_event + .as_ref() + .and_then(|event| event.tags.coordinates().next()), + events, + unread_ids, + archived, + } + }) + .collect(); + + items.sort_by(|a, b| { + b.latest_activity() + .cmp(&a.latest_activity()) + .then_with(|| b.root.to_hex().cmp(&a.root.to_hex())) + }); + + items +} + +/// Read and archive state of the inbox, a high-water-mark model. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct InboxReadState { + #[serde(default)] + pub read_before: Timestamp, + #[serde(default)] + pub read_ids: HashSet, + #[serde(default)] + pub archived_before: Timestamp, + #[serde(default)] + pub archived_ids: HashSet, +} + +impl InboxReadState { + /// Whether `event` is at or before the read cutoff, or marked read. + pub fn is_read(&self, event: &Event) -> bool { + event.created_at <= self.read_before || self.read_ids.contains(&event.id) + } + + /// Whether `event` is at or before the archived cutoff, or marked archived. + pub fn is_archived(&self, event: &Event) -> bool { + event.created_at <= self.archived_before || self.archived_ids.contains(&event.id) + } + + /// Mark one event read. Events at or before the cutoff are already read. + pub fn mark_read(&mut self, event: &Event) { + if event.created_at > self.read_before { + self.read_ids.insert(event.id); + } + } + + /// Mark every non-self event read, anchoring the cutoff ten days back. + pub fn mark_all_read(&mut self, all: &[Event], me: PublicKey, now: Timestamp) { + let cutoff = now - MARK_ALL_WINDOW; + self.read_before = cutoff; + self.read_ids = all + .iter() + .filter(|event| event.pubkey != me && event.created_at > cutoff) + .map(|event| event.id) + .collect(); + } + + /// Advance the read cutoff to the newest point that keeps unread events + /// unread, then prune the id set. + pub fn advance_read(&mut self, all: &[Event], me: PublicKey, now: Timestamp) { + let cutoff = advance_cutoff(all, me, now, self.read_before, |event| self.is_read(event)); + self.read_before = cutoff; + prune_ids(&mut self.read_ids, all, cutoff); + } + + /// Advance the archived cutoff, mirroring [`Self::advance_read`]. + pub fn advance_archived(&mut self, all: &[Event], me: PublicKey, now: Timestamp) { + let cutoff = advance_cutoff(all, me, now, self.archived_before, |event| { + self.is_archived(event) + }); + self.archived_before = cutoff; + prune_ids(&mut self.archived_ids, all, cutoff); + } +} + +/// Newest cutoff that keeps unread events unread, never earlier than `current`. +fn advance_cutoff( + all: &[Event], + me: PublicKey, + now: Timestamp, + current: Timestamp, + is_marked: M, +) -> Timestamp +where + M: Fn(&Event) -> bool, +{ + let fallback = now - ADVANCE_WINDOW; + + let oldest = all + .iter() + .filter(|event| event.pubkey != me && !is_marked(event)) + .map(|event| event.created_at) + .min(); + + let candidate = match oldest { + Some(at) if at < fallback => at - 1, + _ => fallback, + }; + + candidate.max(current) +} + +/// Drop ids whose event is unknown or now covered by the cutoff. +fn prune_ids(ids: &mut HashSet, all: &[Event], cutoff: Timestamp) { + let created_at: HashMap = all + .iter() + .map(|event| (event.id, event.created_at)) + .collect(); + ids.retain(|id| created_at.get(id).is_some_and(|at| *at >= cutoff)); +} + +/// Follow NIP-10/NIP-22 parent pointers until a root item is reached. +fn resolve_thread_root(id: EventId, lookup: &impl Fn(EventId) -> Option) -> EventId { + let mut seen = HashSet::new(); + let mut root = id; + + loop { + if !seen.insert(root) { + return id; + } + + let Some(event) = lookup(root) else { + return root; + }; + + if matches!(event.kind, Kind::GitIssue | Kind::GitPullRequest) { + return root; + } + + match parent_id(&event) { + Some(parent) => root = parent, + None => return root, + } + } +} + +/// Parent of a thread event, mirroring gitworkshop's `getParentId`. +fn parent_id(event: &Event) -> Option { + for marker in ["reply", "root"] { + if let Some(id) = event + .tags + .iter() + .find_map(|tag| e_tag_with_marker(tag, marker)) + { + return Some(id); + } + } + + if let Some(id) = event.tags.iter().find_map(|tag| { + if tag.kind() != "e" { + return None; + } + + let slice = tag.as_slice(); + let is_mention = slice.len() == 4 && slice[3] == "mention"; + + if is_mention { + return None; + } + + tag.content() + .and_then(|content| EventId::from_hex(content).ok()) + }) { + return Some(id); + } + + first_uppercase_e_id(event) +} + +/// NIP-10 root of an event: the `e` tag marked `root`, else the first `e` tag. +fn nip10_root_id(event: &Event) -> Option { + event + .tags + .iter() + .find_map(|tag| e_tag_with_marker(tag, "root")) + .or_else(|| first_e_id(event)) +} + +/// First `e` tag id, in document order. +fn first_e_id(event: &Event) -> Option { + first_tag_id(event, "e") +} + +/// First uppercase `E` tag id, in document order. +fn first_uppercase_e_id(event: &Event) -> Option { + first_tag_id(event, "E") +} + +fn first_tag_id(event: &Event, name: &str) -> Option { + event.tags.iter().find_map(|tag| { + if tag.kind() != name { + return None; + } + tag.content() + .and_then(|content| EventId::from_hex(content).ok()) + }) +} + +/// Event id from a four-element `e` tag carrying `marker`. +fn e_tag_with_marker(tag: &Tag, marker: &str) -> Option { + let slice = tag.as_slice(); + if tag.kind() != "e" || slice.len() != 4 || slice[3] != marker { + return None; + } + tag.content() + .and_then(|content| EventId::from_hex(content).ok()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn keys(seed: u8) -> Keys { + let mut hex = "00000000000000000000000000000000000000000000000000000000000000".to_string(); + hex.push_str(&format!("{seed:02x}")); + Keys::new(SecretKey::from_hex(&hex).expect("valid secret key")) + } + + fn signed(author: &Keys, kind: Kind, tags: Vec, created_at: u64) -> Event { + EventBuilder::new(kind, "") + .tags(tags) + .custom_created_at(Timestamp::from_secs(created_at)) + .finalize(author) + .expect("signed event") + } + + fn e_tag(event: &Event) -> Tag { + Tag::parse(["e", &event.id.to_hex()]).expect("valid e tag") + } + + fn marked_e_tag(event: &Event, marker: &str) -> Tag { + Tag::parse(["e", &event.id.to_hex(), "wss://relay.example.com", marker]) + .expect("valid e tag") + } + + fn uppercase_e_tag(event: &Event) -> Tag { + Tag::parse(["E", &event.id.to_hex()]).expect("valid E tag") + } + + fn a_tag(owner: &PublicKey, id: &str) -> Tag { + Tag::parse(["a", &format!("30617:{}:{id}", owner.to_hex())]).expect("valid a tag") + } + + fn lookup(events: &[Event]) -> impl Fn(EventId) -> Option + '_ { + move |id| events.iter().find(|event| event.id == id).cloned() + } + + fn issue(author: &Keys, at: u64) -> Event { + signed(author, Kind::GitIssue, Vec::new(), at) + } + + #[test] + fn issue_and_pull_request_are_their_own_root() { + let events = [ + issue(&keys(1), 100), + signed(&keys(1), Kind::GitPullRequest, Vec::new(), 100), + ]; + let lookup = lookup(&events); + for event in &events { + assert_eq!(notification_root(event, &lookup), Some(event.id)); + } + } + + #[test] + fn comment_resolves_to_its_uppercase_root() { + let issue = issue(&keys(1), 100); + let comment = signed( + &keys(2), + Kind::Comment, + vec![ + uppercase_e_tag(&issue), + Tag::parse(["K", "1621"]).expect("valid K tag"), + ], + 200, + ); + let events = [issue.clone(), comment.clone()]; + assert_eq!( + notification_root(&comment, &lookup(&events)), + Some(issue.id) + ); + } + + #[test] + fn comment_without_root_pointer_has_no_root() { + let comment = signed( + &keys(2), + Kind::Comment, + vec![e_tag(&issue(&keys(1), 100))], + 200, + ); + assert_eq!(notification_root(&comment, &lookup(&[])), None); + } + + #[test] + fn child_patch_resolves_to_the_root_patch() { + let root_patch = signed(&keys(1), Kind::GitPatch, Vec::new(), 100); + let child_patch = signed(&keys(1), Kind::GitPatch, vec![e_tag(&root_patch)], 200); + let events = [root_patch.clone(), child_patch.clone()]; + assert_eq!( + notification_root(&child_patch, &lookup(&events)), + Some(root_patch.id) + ); + } + + #[test] + fn status_resolves_via_the_root_marker() { + let issue = issue(&keys(1), 100); + let status = signed( + &keys(2), + Kind::GitStatusClosed, + vec![marked_e_tag(&issue, "root")], + 200, + ); + let events = [issue.clone(), status.clone()]; + assert_eq!(notification_root(&status, &lookup(&events)), Some(issue.id)); + } + + #[test] + fn pull_request_update_resolves_via_uppercase_e() { + let pr = signed(&keys(1), Kind::GitPullRequest, Vec::new(), 100); + let update = signed( + &keys(2), + Kind::GitPullRequestUpdate, + vec![uppercase_e_tag(&pr)], + 200, + ); + let events = [pr.clone(), update.clone()]; + assert_eq!(notification_root(&update, &lookup(&events)), Some(pr.id)); + } + + #[test] + fn nested_comment_chain_follows_to_the_root() { + let issue = issue(&keys(1), 100); + let reply = signed(&keys(2), Kind::Comment, vec![uppercase_e_tag(&issue)], 200); + let nested = signed(&keys(3), Kind::Comment, vec![uppercase_e_tag(&reply)], 300); + let events = [issue.clone(), reply, nested.clone()]; + assert_eq!(notification_root(&nested, &lookup(&events)), Some(issue.id)); + } + + #[test] + fn group_excludes_self_and_sorts_groups_newest_first() { + let me = keys(1); + let issue = issue(&keys(2), 100); + let comment = signed(&keys(3), Kind::Comment, vec![uppercase_e_tag(&issue)], 300); + let other_issue = signed( + &keys(2), + Kind::GitIssue, + vec![Tag::parse(["p", &me.public_key().to_hex()]).expect("valid p tag")], + 200, + ); + let mine = signed(&keys(1), Kind::Comment, vec![uppercase_e_tag(&issue)], 400); + + let events = [issue.clone(), comment.clone(), other_issue.clone(), mine]; + let items = group( + events, + me.public_key(), + &InboxReadState::default(), + &lookup(&[]), + ); + + assert_eq!(items.len(), 2); + assert_eq!(items[0].root, issue.id); + // The issue itself plus the comment; the self-authored comment is out. + assert_eq!(items[0].events.len(), 2); + assert_eq!(items[1].root, other_issue.id); + } + + #[test] + fn group_reports_unread_oldest_first_and_archived() { + let me = keys(1); + let issue = issue(&keys(2), 100); + let older = signed(&keys(3), Kind::Comment, vec![uppercase_e_tag(&issue)], 200); + let newer = signed(&keys(4), Kind::Comment, vec![uppercase_e_tag(&issue)], 300); + + let events = [issue.clone(), older.clone(), newer.clone()]; + let items = group( + events, + me.public_key(), + &InboxReadState::default(), + &lookup(&[]), + ); + assert_eq!(items[0].unread_ids, vec![issue.id, older.id, newer.id]); + assert!(!items[0].archived); + assert!(items[0].is_unread()); + + let state = InboxReadState { + archived_before: Timestamp::from_secs(1000), + ..Default::default() + }; + let items = group( + [issue.clone(), older, newer], + me.public_key(), + &state, + &lookup(&[]), + ); + assert!(items[0].archived); + assert!(!items[0].unread_ids.is_empty()); + assert!(!items[0].is_unread()); + } + + #[test] + fn group_reads_root_kind_and_address_from_the_root_event() { + let me = keys(1); + let owner_keys = keys(2); + let owner = owner_keys.public_key(); + let issue = signed( + &owner_keys, + Kind::GitIssue, + vec![a_tag(&owner, "my-repo")], + 100, + ); + let comment = signed(&keys(3), Kind::Comment, vec![uppercase_e_tag(&issue)], 200); + + let events = [issue.clone(), comment]; + let items = group( + events.clone(), + me.public_key(), + &InboxReadState::default(), + &lookup(&events), + ); + + assert_eq!(items[0].root_kind, Some(Kind::GitIssue)); + assert_eq!(items[0].address, issue.tags.coordinates().next()); + } + + #[test] + fn mark_all_read_marks_known_recent_events() { + let me = keys(1); + let now = Timestamp::from_secs(1_000_000_000); + let recent = issue(&keys(2), now.as_secs() - 1000); + let old = issue(&keys(2), now.as_secs() - 5 * 24 * 60 * 60); + let ancient = issue(&keys(2), now.as_secs() - 20 * 24 * 60 * 60); + let mine = issue(&keys(1), now.as_secs() - 100); + + let mut state = InboxReadState::default(); + state.mark_all_read( + &[recent.clone(), old.clone(), ancient.clone(), mine.clone()], + me.public_key(), + now, + ); + + assert_eq!(state.read_before, now - MARK_ALL_WINDOW); + assert_eq!(state.read_ids, HashSet::from([recent.id, old.id])); + assert!(state.is_read(&recent)); + assert!(state.is_read(&ancient)); + assert!(!state.is_read(&mine)); + } + + #[test] + fn advance_read_never_moves_the_cutoff_backwards() { + let me = keys(1); + let unread = issue(&keys(2), 1_000); + let all = [unread]; + let now = Timestamp::from_secs(1_000_000_000); + + let mut state = InboxReadState { + read_before: Timestamp::from_secs(999_999_999), + ..Default::default() + }; + state.advance_read(&all, me.public_key(), now); + + assert_eq!(state.read_before, Timestamp::from_secs(999_999_999)); + } + + #[test] + fn advance_read_moves_before_the_oldest_unread_and_prunes_ids() { + let me = keys(1); + let now = Timestamp::from_secs(1_000_000_000); + let five_days = 5 * 24 * 60 * 60; + let old_unread = issue(&keys(2), now.as_secs() - five_days); + // Read ids that fall before and after the new cutoff. + let stale = signed( + &keys(2), + Kind::GitIssue, + Vec::new(), + now.as_secs() - five_days - 1000, + ); + let fresh = signed( + &keys(2), + Kind::GitIssue, + Vec::new(), + now.as_secs() - 100_000, + ); + + let mut state = InboxReadState { + read_ids: HashSet::from([stale.id, fresh.id]), + ..Default::default() + }; + state.advance_read( + &[old_unread.clone(), stale.clone(), fresh.clone()], + me.public_key(), + now, + ); + + assert_eq!(state.read_before, old_unread.created_at - 1); + assert_eq!(state.read_ids, HashSet::from([fresh.id])); + } + + #[test] + fn serde_round_trip_preserves_state() { + let first = issue(&keys(1), 100); + let second = issue(&keys(2), 200); + let state = InboxReadState { + read_before: Timestamp::from_secs(150), + read_ids: HashSet::from([second.id]), + archived_before: Timestamp::from_secs(50), + archived_ids: HashSet::from([first.id]), + }; + + let json = serde_json::to_string(&state).expect("serialized"); + let parsed: InboxReadState = serde_json::from_str(&json).expect("deserialized"); + + assert_eq!(parsed, state); + } +} diff --git a/crates/signed_core/src/lib.rs b/crates/signed_core/src/lib.rs index b909b86..e7ecb69 100644 --- a/crates/signed_core/src/lib.rs +++ b/crates/signed_core/src/lib.rs @@ -3,6 +3,7 @@ pub mod annotations; pub mod clone_url; pub mod deletions; pub mod filters; +pub mod inbox; pub mod model; pub mod state; pub mod status; @@ -11,6 +12,10 @@ pub use addr::{RepoAddr, identifier_from_name, repo_addr}; pub use annotations::{COVER_NOTE_KIND, cover_note, labels_and_subject, subject_override}; pub use clone_url::{CloneTarget, parse_clone_url}; pub use deletions::Deletions; +pub use filters::{ + NOTIFICATION_KINDS, authored_activity, is_git_activity, notification_comments, notifications, +}; +pub use inbox::{InboxItem, InboxReadState, group, notification_root}; pub use model::{ Announcement, activity_subject, branch_name_of, clone_urls_of, current_commit_of, fork_candidates, latest_update, merge_base_of, pull_request_patch, pull_request_patches, diff --git a/docs/inbox-plan.md b/docs/inbox-plan.md new file mode 100644 index 0000000..ceefae6 --- /dev/null +++ b/docs/inbox-plan.md @@ -0,0 +1,515 @@ +# Inbox (home screen) implementation plan + +Ported from GitWorkshop's home screen, the `Dashboard` rendered at route `/` for a logged-in user. + +> **Correction to the first draft.** The first draft assumed the inbox was the `/notifications` +> page. It is not. GitWorkshop's `Index` route (`src/pages/Index.tsx`) renders `` when +> an account is active, and that home screen is the inbox. + +## 1. What the GitWorkshop home screen is + +`Index.tsx`: + +```tsx +if (account) return ; +return ; +``` + +`Dashboard.tsx` layout: + +- Desktop: two columns. + - **Left column**: `GreetingHeader`, `NotificationsPanel`, `RecentActivitySection`. + - **Right column**: `MyRepositoriesPanel`, `AccessiblePrivateRepositoriesPanel`, + `FollowedReposPanel`. +- Mobile: a single column in a different order. + +The panel that gives the screen its inbox identity is `NotificationsPanel`: + +- heading **Notifications** with a bell icon and an unread count badge, +- a **Mark all read** action and a **View all** link to `/notifications`, +- a compact list of the first 5 **non-archived** notification items, +- the empty state reads **"Your inbox is empty"** (with an `Inbox` icon). + +So in GitWorkshop's vocabulary, "inbox" is the non-archived activity directed at you, surfaced +inline on the home screen. The home screen also shows your own recent activity and your repositories. + +Data hooks: + +| Section | Hook | What it loads | +|---|---|---| +| Notifications (inbox) | `useNotifications()` | Notification model: grouped thread activity directed at you, read/archived state | +| Continue where you left off | `useUserActivity(pubkey)` | Git activity authored by you: kinds 1621/1617/1618/1111 (git `K`)/1624/1630-1633, newest first, limit 50 | +| My repositories | `useUserRepositories(pubkey)` | Kind 30617 announcements authored by you | +| Followed repositories | `useUserFollowedRepos(pubkey)` | Repos you follow | +| Accessible private repositories | `useAccessiblePrivateRepositories()` | Private repos from CI/services | + +## 2. Scope for Signed + +| Priority | Section | Notes | +|---|---|---| +| **P0** | Inbox panel | Activity directed at you, grouped by thread root; unread badge; mark all read; top N + show all | +| **P0** | My repositories | Reuse `RepoListStore::announcements_of(me)`; search filter; existing New-repo dialog | +| **P0** | Continue where you left off | Your own recent git activity, newest first | +| **P1** | Unread / Archived sub-views | Open as **bottom-dock panels**, not tabs inside the inbox panel | +| **P1** | Click-through | Open the repo panel at the relevant PR/issue | +| **P2 (defer)** | Standalone notifications page, NIP-65 relay discovery, pagination | Web-app concerns | +| **Out of scope** | Greeting header, followed repositories, private repositories, pinned repositories | Not needed in Signed | + +Notes: + +- There is **no greeting header**. The screen starts with the inbox panel. +- Unread and Archived are separate panels opened in the bottom dock, not tabs in the inbox panel. + +## 3. The Signed screen + +`InboxView` is a center panel, opened by the sidebar's existing **Inbox** nav item (currently a +placeholder that opens Explore). It is one scrollable two-column flex row: + +``` ++--------------------------------------------------+-----------------------+ +| Inbox (3 unread) [Unread] [Archived] [Mark all read] | +| [avatar] issue opened on you/repo 2m | +| [avatar] commented on "Fix parser" 1h | +| [avatar] PR update on you/repo 3h | +| [Show all] | +| | +| Continue where you left off | +| [icon] "Fix parser bug" you/repo opened 3d | +| [icon] "Add retry" you/repo PR 5d | ++--------------------------------------------------+-----------------------+ +| bottom dock: Unread or Archived list (opened by the header buttons) | ++--------------------------------------------------+-----------------------+ +``` + +The right column is **My repositories**, mirroring the sidebar's signed-in repo list: + +``` +| My repositories | +| [search] [New] | +| repo row | +| repo row | +``` + +## 4. Data layer + +### 4.1 `signed_core`: pure logic + +**`filters.rs`** (extend, next to `activity`/`comments_for`): + +```rust +/// Kinds that notify a user when they tag them directly. +pub const NOTIFICATION_KINDS: [Kind; 9] = [ + Kind::GitIssue, + Kind::GitPullRequest, + Kind::GitPatch, + Kind::GitPullRequestUpdate, + COVER_NOTE_KIND, + Kind::GitStatusOpen, + Kind::GitStatusApplied, + Kind::GitStatusClosed, + Kind::GitStatusDraft, +]; + +/// Comments on our issues/PRs/patches. +pub fn notification_comments(me: PublicKey) -> Filter { + Filter::new() + .kind(Kind::Comment) + .custom_tags(SingleLetterTag::UPPERCASE_P, [me.to_hex()]) + .custom_tags(SingleLetterTag::UPPERCASE_K, ["1621", "1617", "1618"]) +} + +/// Activity directed at us: comments on our roots, and git events tagging us. +pub fn notifications(me: PublicKey) -> Vec { + vec![ + notification_comments(me), + Filter::new().kinds(NOTIFICATION_KINDS).pubkey(me), + ] +} + +/// Git activity authored by `me`, for "Continue where you left off". +pub fn authored_activity(me: PublicKey) -> Filter { + Filter::new() + .kinds([ACTIVITY_KINDS.as_slice(), &[COVER_NOTE_KIND]].concat()) + .author(me) +} +``` + +`ACTIVITY_KINDS` already exists in this file. All builders use existing SDK APIs +(`Filter::kind/kinds/pubkey/custom_tags`, `SingleLetterTag::{UPPERCASE_P, UPPERCASE_K}`). + +Comments authored by `me` are not all git comments, so the activity query needs a post-filter: +keep kind 1111 only when its uppercase `K` tag is a git root kind (1621/1617/1618/30617), matching +gitworkshop's `isGitComment`. + +**`inbox.rs`** (new file): + +```rust +pub struct InboxItem { + pub root: EventId, + pub root_kind: Option, + pub address: Option, + /// Events in the group, newest first. + pub events: Vec, + /// Unread event ids, oldest first. + pub unread_ids: Vec, + pub archived: bool, +} + +/// The thread root of a notification event, or `None` if it isn't git-related. +pub fn notification_root( + event: &Event, + lookup: &impl Fn(EventId) -> Option, +) -> Option; + +/// Group notification events by root, newest activity first, self excluded. +pub fn group( + events: impl IntoIterator, + me: PublicKey, + state: &InboxReadState, + lookup: &impl Fn(EventId) -> Option, +) -> Vec; +``` + +Root resolution, ported from `getNotificationRootId`: + +- issue (1621) / PR (1618): itself +- patch (1617): its `e` parent patch, else itself +- NIP-22 comment (1111): uppercase `E` root pointer (SDK `nip22::extract_root`) +- PR update (1619): uppercase `E` +- statuses (1630-1633) / cover note (1624): NIP-10 root `e` +- self-authored events are excluded + +Read/archive state, the compact high-water-mark model: + +```rust +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct InboxReadState { + #[serde(default)] pub read_before: Timestamp, + #[serde(default)] pub read_ids: HashSet, + #[serde(default)] pub archived_before: Timestamp, + #[serde(default)] pub archived_ids: HashSet, +} + +impl InboxReadState { + pub fn is_read(&self, event: &Event) -> bool; + pub fn is_archived(&self, event: &Event) -> bool; + pub fn mark_read(&mut self, event: &Event); + pub fn mark_all_read(&mut self, all: &[Event], me: PublicKey); + /// Move the cutoff to `min(oldest unread - 1, now - 3 days)` and prune ids. + pub fn advance_read(&mut self, all: &[Event], me: PublicKey); + pub fn advance_archived(&mut self, all: &[Event], me: PublicKey); +} +``` + +`activity_subject` in `model.rs` already gives an issue/PR title from the `subject` tag or first +line; reuse it for the home screen rows. + +### 4.2 Persistence: NIP-78 in the local database, never published + +Read state is a normal NIP-78 (kind `30078`, `Kind::ApplicationSpecificData`) addressable event +**written to LMDB only**. It is never broadcast to a relay, so the read state stays on this device. + +It is signed with a **random keypair**, never the user's signer. The event is local application +storage, so its author carries no identity; this avoids a signing round-trip and does not depend on +the signer type. The `d` tag identifies the owning user, so state does not leak across identities +when the signed-in key changes. + +```rust +/// d tag identifying the inbox read/archive state event of `me`. +fn inbox_state_d_tag(me: PublicKey) -> String { + format!("signed-inbox-state:{me}") +} + +/// Newest stored read state for `me`, with the id of the event it came from. +async fn load_state( + client: &Client, + me: PublicKey, +) -> Result> { + // No author filter: the signing key is random per session. + let filter = Filter::new() + .kind(Kind::ApplicationSpecificData) + .identifier(inbox_state_d_tag(me)); + let events = client.database().query(filter).await?; + let Some(event) = events.into_iter().max_by_key(|e| e.created_at) else { + return Ok(None); + }; + Ok(serde_json::from_str(&event.content) + .ok() + .map(|state| (state, event.id))) +} + +/// Sign with the random `keys` and store locally. +async fn save_state( + client: &Client, + keys: &Keys, + me: PublicKey, + state: &InboxReadState, +) -> Result { + let event = EventBuilder::new(Kind::ApplicationSpecificData, serde_json::to_string(state)?) + .tags([Tag::identifier(inbox_state_d_tag(me))]) + .finalize(keys)?; // synchronous: random keys, no user signer + // Local-only: no `send_event`, no broadcast. The event lives in LMDB. + client.database().save_event(&event).await?; + Ok(event.id) +} +``` + +Kind 30078 is addressable, so saves signed by the same `keys` replace the previous event. Because +`keys` is random per session, the first save of a session creates a new coordinate; the store then +deletes the event it loaded (its id is kept from `load_state`) so exactly one state event remains. +`NostrDatabase::{save_event, query}` and `Client::database()` are existing SDK APIs. + +### 4.3 `signed_state`: one `InboxStore` + +One store backs the whole screen, modelled on `RepoListStore` (`repos.rs`): + +```rust +pub struct InboxStore { + /// Activity directed at the user, grouped by thread root, newest first. + pub notifications: Arc>, + /// The user's own recent git activity, newest first. + pub activity: Arc>, + /// Unread notification count (non-archived). + pub unread_count: usize, + state: InboxReadState, + user: Option, + /// Random keypair signing the local NIP-78 storage event. + keys: Keys, + /// Event id of the loaded state event, pruned on the next save. + loaded_state: Option, + refresh: RefreshGate, + _subscription: Subscription, +} +``` + +**Lifespan: idle until a signer exists.** The store is created in `signed_state::init` like the +other stores, but it does nothing until the user has a signer. It is never wired from the `desktop` +crate, and `signed_state::init` gains no parameters. + +- `new` schedules `cx.defer`, like the other stores. The deferred bootstrap checks + `Backend::current_user()`: + - signer already present (session restored before the store was created): activate now; + - no signer: do nothing, wait for the event. +- `BackendEvent::SignerChanged`: activate. +- `BackendEvent::SignerRequired` (logout / no credential): clear `user`, `notifications`, + `activity`, `unread_count`. +- `BackendEvent::NostrUpdate(updates)`: refresh when any update kind is in `NOTIFICATION_KINDS`, + is `Kind::Comment`, or is a deletion. +- `BackendEvent::Synced`: refresh. +- `BackendEvent::Published`: refresh. + +**Activation** (only on signer): + +```rust +fn activate(&mut self, me: PublicKey, cx: &mut Context) { + self.user = Some(me); + // Load the NIP-78 state from LMDB, then subscribe and refresh. + // All run on background tasks; only plain data crosses back. + self.load_state(me, cx); + self.subscribe_remote(cx); + self.refresh(cx); +} +``` + +Loading uses the random `keys` only for signing on save; reading the state event needs no signer at +all. Activation itself is still gated on the signer because the fetch filters need the user's pubkey. + +**Fetch** (reuses `Backend::subscribe_bootstrap` / `connect_repo_relays`): + +```rust +fn subscribe_remote(&mut self, cx: &mut Context) { + let Some(me) = self.user else { return }; + let backend = Backend::global(cx); + backend.update(cx, |backend, cx| { + backend.subscribe_bootstrap(filters::notifications(me), cx); + backend.subscribe_bootstrap(vec![filters::authored_activity(me)], cx); + }); + // Relays of the user's own repos, so their activity there is found too. + let relays = own_repo_relays(me, cx); + backend.update(cx, |backend, cx| { + backend.connect_repo_relays(relays.clone(), filters::notifications(me), cx); + backend.connect_repo_relays(relays, vec![filters::authored_activity(me)], cx); + }); +} +``` + +`own_repo_relays` reads `RepoListStore::global(cx).read(cx).announcements_of(&me)` and unions their +`relays`. (NIP-65 outbox relay discovery is deferred; Signed does not fetch kind 10002 yet.) + +**Refresh** (mirrors `RepoListStore::run_refresh`): + +- `cx.background_spawn`: query the notification filters and the activity filter from + `client.database()`. +- Query `filters::deletions()`, build `Deletions`, skip deleted events. +- Build `HashMap` for root walking; group the notification events with + `inbox::group`. +- Filter the activity events: keep issues/PRs/patches/statuses/cover notes, and comments only when + their `K` tag is a git kind; sort newest first; take the top N. +- Cross back to the main thread: set `notifications`, `activity`, `unread_count`, `cx.notify()`, + `refresh.finish()`. + +**Actions**: `mark_read(root)`, `mark_all_read()`, plus `advance_read` after marking to bound the +`read_ids` set. After each change, sign with the random `keys` and save the NIP-78 event to LMDB, +then delete the previously loaded copy (see 4.2). + +**My repositories needs no new store**: `RepoListStore` already holds every announcement and exposes +`announcements_of(user)`. + +### 4.4 `Cargo.toml` + +- `signed_core`: add `serde.workspace` for the `InboxReadState` derives. +- `signed_state`: add `serde_json.workspace` for the NIP-78 content. + +## 5. UI + +### 5.1 `InboxView` center panel + +New `crates/workspace/src/views/inbox.rs`, a `BasePanel` + `Panel` + `Render`, like `RepoListView`. +One scrollable two-column flex row. + +- **Inbox column**: header with the unread count badge and actions **Unread**, **Archived**, + **Mark all read**; then the non-archived notification items (top 5, with a **Show all** toggle + expanding inline). Rows show the actor avatar, a kind badge, the subject, the repo name, a + relative time, and an unread dot. Empty state: "You're all caught up." with `IconName::Inbox`. +- **Continue where you left off**: `InboxStore::activity`, top 15, each row a kind icon, subject, + repo name, and relative time. +- **My repositories**: `RepoListStore::announcements_of(me)` with a small search `InputState` (same + pattern as `RepoListView`) and a **New** button opening the existing `create_repo_dialog`. Rows + open `open_repo_panel`. + +No greeting header. + +### 5.2 Unread / Archived as bottom-dock panels + +Add a bottom-panel helper next to `add_center_panel` in `crates/dock/src/lib.rs`: + +```rust +/// Add an already-wrapped panel handle to the bottom dock of `area`. +pub fn add_bottom_panel( + area: &mut DockArea, + panel: Arc, + window: &mut Window, + cx: &mut Context, +) { + area.add_panel_view(panel, DockPlacement::Bottom, None, window, cx); +} +``` + +The workspace already supports a bottom dock and prunes it when empty (`workspace.rs`). Then: + +- New `InboxFilterView` panel taking a mode `InboxFilter::Unread | InboxFilter::Archived` and the + `InboxStore`. It renders the matching subset of `InboxStore::notifications` as a list. +- The **Unread** and **Archived** header buttons in `InboxView` call `add_bottom_panel` with the + requested mode. `InboxView` keeps `filter_view: Option>`; when it + already exists, update its mode and focus instead of adding a duplicate. + +### 5.3 Sidebar + +In `views/sidebar/mod.rs`: + +- Add `inbox: Option>` (mirrors `explore`). +- Add `fn open_inbox(&mut self, window, cx)` that focuses the existing panel or adds a center panel. +- Point the existing nav item at it and add an unread suffix: + + ```rust + NavItem::new("inbox", "Inbox", Icon::new(IconName::Inbox).small()) + .when(unread > 0, |this| this.suffix(...badge...)) + .on_click(cx.listener(|this, _ev, window, cx| this.open_inbox(window, cx))), + ``` + +- `cx.observe` the `InboxStore` so the badge updates. + +### 5.4 Click-through (P1) + +Reuse the single `RepoStore` that `RepoDetailView` already creates instead of making a second one: + +1. In `repo_detail/mod.rs`, add: + + ```rust + pub(crate) enum RepoItem { + Issue(EventId), + PullRequest(EventId), + Patch(EventId), + } + + impl RepoDetailView { + pub(crate) fn open_item( + &mut self, + item: RepoItem, + window: &mut Window, + cx: &mut Context, + ) { /* open IssueDetailView / PullRequestDetailView in the dock */ } + } + ``` + +2. `open_repo_panel` already returns `Entity`; the caller invokes + `detail.update_in(window, cx, |detail, window, cx| detail.open_item(...))`. +3. `InboxView` resolves `item.address` to an `Announcement` from `RepoListStore`, opens the repo + panel, then calls `open_item` with the root id and kind. + +Patches have no detail view in Signed (they are only consumed inside `PullRequestDetailView`), so a +patch-root click opens the repo panel. Note as a known limitation. + +## 6. File-by-file change list + +| File | Change | +|---|---| +| `crates/signed_core/Cargo.toml` | add `serde` | +| `crates/signed_core/src/filters.rs` | `NOTIFICATION_KINDS`, `notification_comments`, `notifications`, `authored_activity` | +| `crates/signed_core/src/inbox.rs` | **new**: `InboxItem`, `notification_root`, `group`, `InboxReadState`, tests | +| `crates/signed_core/src/lib.rs` | `mod inbox;` and re-exports | +| `crates/signed_state/Cargo.toml` | add `serde_json` | +| `crates/signed_state/src/inbox.rs` | **new**: `InboxStore`, global, signer-gated activation, NIP-78 load/save, actions | +| `crates/signed_state/src/lib.rs` | `mod inbox;`, set global in `init` | +| `crates/dock/src/lib.rs` | `add_bottom_panel` helper | +| `crates/workspace/src/views/inbox.rs` | **new**: `InboxView` home panel and `InboxFilterView` | +| `crates/workspace/src/views/mod.rs` | `mod inbox; pub use inbox::InboxView;` | +| `crates/workspace/src/views/sidebar/mod.rs` | `inbox` field, `open_inbox`, nav wiring and badge | +| `crates/workspace/src/views/repo_detail/mod.rs` | `RepoItem`, `RepoDetailView::open_item` (P1) | + +No changes to `desktop` or `signed_nostr`. `signed_state::init` gains no parameters; the store +bootstraps itself via `cx.defer` once a signer is present. + +## 7. Phasing + +1. **Phase 0 - pure logic**: `signed_core` filters and `inbox.rs` plus tests. **DONE.** + Implemented as `filters::{NOTIFICATION_KINDS, notification_comments, notifications, authored_activity, is_git_activity}` + and `inbox::{InboxItem, notification_root, group, InboxReadState}`. Two deviations from the sketch: + the cutoff methods take an explicit `now: Timestamp` so the pure logic stays deterministic and testable, + and `authored_activity` results must pass through `is_git_activity` before display (comments on + non-git roots are matched by the filter). `cargo test -p signed_core` passes (62 tests). +2. **Phase 1 - store**: `InboxStore` with signer-gated activation, both queries, unread count, and + NIP-78 load/save to LMDB, global install. +3. **Phase 2 - screen**: `InboxView` (inbox + activity + my repositories), sidebar nav and badge. +4. **Phase 3 - sub-views**: `add_bottom_panel` and `InboxFilterView` for Unread / Archived. +5. **Phase 4 - click-through**: `open_item` and announcement lookup. +6. **Phase 5 (optional)**: standalone notifications page, NIP-65 relays, pagination, patch detail + view. + +Each phase compiles and is usable on its own. + +## 8. Validation + +- `cargo test -p signed_core`: root resolution, grouping, read-state cutoff, serde round-trip. +- `cargo test -p signed_state`: NIP-78 content round-trip (`serde_json`), if a non-GPUI path is + factored out. +- `cargo check --workspace` after each phase. +- Manual: log in with a repo-owning identity; confirm the inbox panel populates from another + identity's issue/comment, the activity list shows your own items, the repositories panel matches + the sidebar, and that no kind-30078 event is broadcast (watch the relays / `Published` events). + Restart to confirm the read state is read back from LMDB. + +## 9. SDK APIs used (verified in the pinned `5c669a4` checkout) + +- `Kind::{Comment, GitIssue, GitPullRequest, GitPatch, GitPullRequestUpdate,` + `GitStatusOpen/Applied/Closed/Draft, ApplicationSpecificData, EventDeletion, RequestToVanish}` +- `Filter::{kind, kinds, pubkey, pubkeys, custom_tags, limit, since, events, coordinate, identifier}` + - Non-obvious: `Filter::pubkey`/`pubkeys` set the lowercase **`p` tag**, not `authors`. Use + `Filter::author`/`authors` for authorship. The `notifications` filter relies on this. +- `SingleLetterTag::{LOWERCASE_P, LOWERCASE_E, UPPERCASE_P, UPPERCASE_K, UPPERCASE_E}` +- `nostr::nips::nip22::{extract_root, extract_parent, CommentTarget}`: NIP-22 root/parent pointers +- `Tags::{event_ids, public_keys, coordinates, identifier, hashtags}` iterators +- `Client::{database, subscribe, sync, notifications, send_event, add_relay}`; + `NostrDatabase::{save_event, query}`; `NostrLmdb`, `NostrGossipMemory` +- `EventBuilder::{new, tags, finalize}`, `Tag::identifier`, `Keys::generate` +- `Timestamp`, `EventId` (hex serde), `PublicKey`, `Coordinate`