From 6eccd65b938b20b9037ec61f55aa7aa10c6994ff Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Sun, 23 Aug 2026 19:54:50 +0700 Subject: [PATCH] update issue panel --- Cargo.lock | 136 ++++++------ crates/signed_core/src/filters.rs | 26 +++ crates/signed_core/src/status.rs | 27 ++- crates/signed_state/src/backend.rs | 68 ++++++ crates/signed_state/src/repo.rs | 121 ++++++++++- .../src/views/repo_detail/helpers.rs | 2 +- .../src/views/repo_detail/issue_detail.rs | 205 ++++++++++++++---- .../workspace/src/views/repo_detail/issues.rs | 1 + crates/workspace/src/views/repo_detail/mod.rs | 7 +- .../views/repo_detail/pull_request_detail.rs | 2 + .../src/views/repo_detail/pull_requests.rs | 1 + crates/workspace/src/views/repo_list.rs | 1 + crates/workspace/src/views/sidebar/mod.rs | 4 +- 13 files changed, 470 insertions(+), 131 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ec41b5e..e0bf9c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1268,7 +1268,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637" +source = "git+https://github.com/zed-industries/zed#d9ad6aff67e47de43abb270d22de75dd950f1b48" dependencies = [ "gpui_util", "indexmap", @@ -1551,9 +1551,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" dependencies = [ "cfg-if", ] @@ -1734,7 +1734,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637" +source = "git+https://github.com/zed-industries/zed#d9ad6aff67e47de43abb270d22de75dd950f1b48" dependencies = [ "proc-macro2", "quote", @@ -2256,9 +2256,9 @@ dependencies = [ [[package]] name = "font-types" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75382bc7392ef10aad10935f92fc3db36d2d4dad0e5d96d8d65e04f89a07ec39" +checksum = "e64eb721ca85a34323425f4041adc5d82704d3782d5f8f03793bc012419dce23" dependencies = [ "bytemuck", ] @@ -2798,9 +2798,9 @@ dependencies = [ [[package]] name = "gix-features" -version = "0.49.0" +version = "0.49.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20aa09e83a48dc02c5f5f08578aa79d3ab1bab4618b8c362f88684645a02bdcc" +checksum = "39c0e59d9d253dcccc38c3a46b91bfb9b46bd63eed54fe1a719e12194884d52a" dependencies = [ "bytes", "crc32fast", @@ -2838,9 +2838,9 @@ dependencies = [ [[package]] name = "gix-fs" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "865cf13fcaf5455220546cb9607c416bd1be9a6caafd143655a362fdeab64e80" +checksum = "ebcfa9fd253f25350a3b21b3dd74034a446098e373c6123d4cee3519894f12ef" dependencies = [ "bstr", "gix-features", @@ -2851,9 +2851,9 @@ dependencies = [ [[package]] name = "gix-glob" -version = "0.27.0" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "421e92a711554fa5827d1b0599d3389acdd0f6729e97a8c5a57d79af1e50bf36" +checksum = "b417cf515fd8c91468b578071f76d6cba716f8a1eccd853906bff4908b2c1413" dependencies = [ "bitflags 2.13.1", "bstr", @@ -2863,9 +2863,9 @@ dependencies = [ [[package]] name = "gix-hash" -version = "0.26.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13adaa73415fd6c902310923f68d0b98e8cecf14b33ea58c02cc387cee56f54e" +checksum = "caf125eae66b7d6e4395511a06c0d43a3c34eac96c8641fb98b22078faee65b8" dependencies = [ "faster-hex", "gix-features", @@ -2886,9 +2886,9 @@ dependencies = [ [[package]] name = "gix-ignore" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12cff8e8aa125e39377456073e63df3334d9e5741372ddcc226198015076dda2" +checksum = "65859a2f7de5e159d4344a5486ebf53b6bac22d5ec6afa836e47c10ae88a7f0f" dependencies = [ "bstr", "gix-glob", @@ -2899,9 +2899,9 @@ dependencies = [ [[package]] name = "gix-imara-diff" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a791e6620676a875f362f3156ed213e73ca099a09bf992c18812abe65cc37b1" +checksum = "1c91d8cffac8849493a82233811bd02b2b183b8cf39bf704de0fa0841b737595" dependencies = [ "bstr", "hashbrown 0.17.1", @@ -3037,9 +3037,9 @@ dependencies = [ [[package]] name = "gix-path" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "751d6bd162106f8c1e7e9aaccb5bbdd605267e91a930a17a4560c46e33a9100c" +checksum = "2b075e730586bba7341304d6fc1b4efc1d10cf64532622521c0e07f30e661046" dependencies = [ "bstr", "gix-trace", @@ -3295,9 +3295,9 @@ dependencies = [ [[package]] name = "gix-utils" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1795bd2a970ca8b2185318c2abb97d955c71992f1cf28de73ad3b593a9f3ce8" +checksum = "0da1c46491b49458a446cc76f0085860f8164c2290742e0aa8c653ce67240a97" dependencies = [ "bstr", "fastrand", @@ -3307,9 +3307,9 @@ dependencies = [ [[package]] name = "gix-validate" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a034e84d1e04e1b1f20f51f12491da230b6ac8b925d0c8e1b89bcd87a7c5ccc" +checksum = "4dae8780f63ed8a803b8bdabbd7aa5f5c5d74592c8b50eed875c1bb4f6545a6a" dependencies = [ "bstr", ] @@ -3489,7 +3489,7 @@ dependencies = [ [[package]] name = "gpui" version = "0.2.2" -source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637" +source = "git+https://github.com/zed-industries/zed#d9ad6aff67e47de43abb270d22de75dd950f1b48" dependencies = [ "accesskit", "anyhow", @@ -3562,7 +3562,7 @@ dependencies = [ [[package]] name = "gpui-base" version = "0.5.2" -source = "git+https://github.com/longbridge/gpui-component#14359a0cdd00657eb68fa946bfd99cd83c26e002" +source = "git+https://github.com/longbridge/gpui-component#9e6d5000b4020e7849e167faa1f3ee8932480ff6" dependencies = [ "aho-corasick", "anyhow", @@ -3594,7 +3594,7 @@ dependencies = [ [[package]] name = "gpui-component" version = "0.5.2" -source = "git+https://github.com/longbridge/gpui-component#14359a0cdd00657eb68fa946bfd99cd83c26e002" +source = "git+https://github.com/longbridge/gpui-component#9e6d5000b4020e7849e167faa1f3ee8932480ff6" dependencies = [ "anyhow", "chrono", @@ -3674,7 +3674,7 @@ dependencies = [ [[package]] name = "gpui-component-assets" version = "0.5.1" -source = "git+https://github.com/longbridge/gpui-component#14359a0cdd00657eb68fa946bfd99cd83c26e002" +source = "git+https://github.com/longbridge/gpui-component#9e6d5000b4020e7849e167faa1f3ee8932480ff6" dependencies = [ "anyhow", "gpui", @@ -3688,7 +3688,7 @@ dependencies = [ [[package]] name = "gpui-component-macros" version = "0.5.1" -source = "git+https://github.com/longbridge/gpui-component#14359a0cdd00657eb68fa946bfd99cd83c26e002" +source = "git+https://github.com/longbridge/gpui-component#9e6d5000b4020e7849e167faa1f3ee8932480ff6" dependencies = [ "proc-macro2", "quote", @@ -3698,7 +3698,7 @@ dependencies = [ [[package]] name = "gpui_apple" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637" +source = "git+https://github.com/zed-industries/zed#d9ad6aff67e47de43abb270d22de75dd950f1b48" dependencies = [ "anyhow", "block", @@ -3721,7 +3721,7 @@ dependencies = [ [[package]] name = "gpui_linux" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637" +source = "git+https://github.com/zed-industries/zed#d9ad6aff67e47de43abb270d22de75dd950f1b48" dependencies = [ "accesskit", "accesskit_unix", @@ -3767,7 +3767,7 @@ dependencies = [ [[package]] name = "gpui_macos" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637" +source = "git+https://github.com/zed-industries/zed#d9ad6aff67e47de43abb270d22de75dd950f1b48" dependencies = [ "accesskit", "accesskit_macos", @@ -3813,7 +3813,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637" +source = "git+https://github.com/zed-industries/zed#d9ad6aff67e47de43abb270d22de75dd950f1b48" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -3824,7 +3824,7 @@ dependencies = [ [[package]] name = "gpui_platform" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637" +source = "git+https://github.com/zed-industries/zed#d9ad6aff67e47de43abb270d22de75dd950f1b48" dependencies = [ "console_error_panic_hook", "gpui", @@ -3837,7 +3837,7 @@ dependencies = [ [[package]] name = "gpui_shared_string" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637" +source = "git+https://github.com/zed-industries/zed#d9ad6aff67e47de43abb270d22de75dd950f1b48" dependencies = [ "schemars", "serde", @@ -3847,7 +3847,7 @@ dependencies = [ [[package]] name = "gpui_util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637" +source = "git+https://github.com/zed-industries/zed#d9ad6aff67e47de43abb270d22de75dd950f1b48" dependencies = [ "anyhow", "log", @@ -3857,7 +3857,7 @@ dependencies = [ [[package]] name = "gpui_web" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637" +source = "git+https://github.com/zed-industries/zed#d9ad6aff67e47de43abb270d22de75dd950f1b48" dependencies = [ "anyhow", "console_error_panic_hook", @@ -3881,7 +3881,7 @@ dependencies = [ [[package]] name = "gpui_wgpu" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637" +source = "git+https://github.com/zed-industries/zed#d9ad6aff67e47de43abb270d22de75dd950f1b48" dependencies = [ "anyhow", "bytemuck", @@ -3907,7 +3907,7 @@ dependencies = [ [[package]] name = "gpui_windows" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637" +source = "git+https://github.com/zed-industries/zed#d9ad6aff67e47de43abb270d22de75dd950f1b48" dependencies = [ "accesskit", "accesskit_windows", @@ -4234,7 +4234,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637" +source = "git+https://github.com/zed-industries/zed#d9ad6aff67e47de43abb270d22de75dd950f1b48" dependencies = [ "anyhow", "async-compression", @@ -4254,7 +4254,7 @@ dependencies = [ [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637" +source = "git+https://github.com/zed-industries/zed#d9ad6aff67e47de43abb270d22de75dd950f1b48" dependencies = [ "rustls", "rustls-platform-verifier 0.5.3", @@ -5039,9 +5039,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" dependencies = [ "serde_core", "value-bag", @@ -5230,7 +5230,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637" +source = "git+https://github.com/zed-industries/zed#d9ad6aff67e47de43abb270d22de75dd950f1b48" dependencies = [ "anyhow", "bindgen", @@ -5445,7 +5445,7 @@ dependencies = [ [[package]] name = "nostr" version = "0.45.3" -source = "git+https://github.com/rust-nostr/nostr#4dcf307d6102e598f3a4172b8ba5a7a779fa1630" +source = "git+https://github.com/rust-nostr/nostr#ea38c0e3b2fc2717a4a55d019560a5574baf8cd0" dependencies = [ "aes", "base64", @@ -5471,7 +5471,7 @@ dependencies = [ [[package]] name = "nostr-connect" version = "0.45.1" -source = "git+https://github.com/rust-nostr/nostr#4dcf307d6102e598f3a4172b8ba5a7a779fa1630" +source = "git+https://github.com/rust-nostr/nostr#ea38c0e3b2fc2717a4a55d019560a5574baf8cd0" dependencies = [ "async-utility", "futures-core", @@ -5485,7 +5485,7 @@ dependencies = [ [[package]] name = "nostr-database" version = "0.45.1" -source = "git+https://github.com/rust-nostr/nostr#4dcf307d6102e598f3a4172b8ba5a7a779fa1630" +source = "git+https://github.com/rust-nostr/nostr#ea38c0e3b2fc2717a4a55d019560a5574baf8cd0" dependencies = [ "nostr", "opaquerr", @@ -5494,7 +5494,7 @@ dependencies = [ [[package]] name = "nostr-gossip" version = "0.45.0" -source = "git+https://github.com/rust-nostr/nostr#4dcf307d6102e598f3a4172b8ba5a7a779fa1630" +source = "git+https://github.com/rust-nostr/nostr#ea38c0e3b2fc2717a4a55d019560a5574baf8cd0" dependencies = [ "nostr", "opaquerr", @@ -5503,7 +5503,7 @@ dependencies = [ [[package]] name = "nostr-gossip-memory" version = "0.45.0" -source = "git+https://github.com/rust-nostr/nostr#4dcf307d6102e598f3a4172b8ba5a7a779fa1630" +source = "git+https://github.com/rust-nostr/nostr#ea38c0e3b2fc2717a4a55d019560a5574baf8cd0" dependencies = [ "indexmap", "lru", @@ -5515,7 +5515,7 @@ dependencies = [ [[package]] name = "nostr-lmdb" version = "0.45.2" -source = "git+https://github.com/rust-nostr/nostr#4dcf307d6102e598f3a4172b8ba5a7a779fa1630" +source = "git+https://github.com/rust-nostr/nostr#ea38c0e3b2fc2717a4a55d019560a5574baf8cd0" dependencies = [ "async-utility", "flatbuffers", @@ -5530,7 +5530,7 @@ dependencies = [ [[package]] name = "nostr-memory" version = "0.45.1" -source = "git+https://github.com/rust-nostr/nostr#4dcf307d6102e598f3a4172b8ba5a7a779fa1630" +source = "git+https://github.com/rust-nostr/nostr#ea38c0e3b2fc2717a4a55d019560a5574baf8cd0" dependencies = [ "btreecap", "nostr", @@ -5541,7 +5541,7 @@ dependencies = [ [[package]] name = "nostr-sdk" version = "0.45.2" -source = "git+https://github.com/rust-nostr/nostr#4dcf307d6102e598f3a4172b8ba5a7a779fa1630" +source = "git+https://github.com/rust-nostr/nostr#ea38c0e3b2fc2717a4a55d019560a5574baf8cd0" dependencies = [ "async-utility", "async-wsocket", @@ -6154,9 +6154,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ordered-float" -version = "5.3.0" +version = "5.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" +checksum = "8c7c9e0d9b23589f26070720bac724174bfec1083e82f7854cdd0267518343c0" dependencies = [ "num-traits", ] @@ -6277,7 +6277,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perf" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637" +source = "git+https://github.com/zed-industries/zed#d9ad6aff67e47de43abb270d22de75dd950f1b48" dependencies = [ "collections", "serde", @@ -7021,7 +7021,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709" dependencies = [ "bytemuck", - "font-types 0.12.3", + "font-types 0.12.4", "once_cell", ] @@ -7074,7 +7074,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637" +source = "git+https://github.com/zed-industries/zed#d9ad6aff67e47de43abb270d22de75dd950f1b48" dependencies = [ "derive_refineable", ] @@ -7157,7 +7157,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637" +source = "git+https://github.com/zed-industries/zed#d9ad6aff67e47de43abb270d22de75dd950f1b48" dependencies = [ "anyhow", "bytes", @@ -7486,9 +7486,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.14" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "aws-lc-rs", "ring", @@ -7569,7 +7569,7 @@ dependencies = [ [[package]] name = "scheduler" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637" +source = "git+https://github.com/zed-industries/zed#d9ad6aff67e47de43abb270d22de75dd950f1b48" dependencies = [ "async-task", "backtrace", @@ -8296,7 +8296,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637" +source = "git+https://github.com/zed-industries/zed#d9ad6aff67e47de43abb270d22de75dd950f1b48" dependencies = [ "heapless 0.9.3", "log", @@ -9669,7 +9669,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "util_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637" +source = "git+https://github.com/zed-industries/zed#d9ad6aff67e47de43abb270d22de75dd950f1b48" dependencies = [ "perf", "quote", @@ -9685,9 +9685,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.24.1" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cefc03fd367c0c6d4305de1b312cf00248c4114f4a0418ce6a6af769e3b0bd9" +checksum = "f053576934f05a761a402421fbbe3d425d9366f75f978806a037b3ca481abecc" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -11467,7 +11467,7 @@ checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" [[package]] name = "zlog" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637" +source = "git+https://github.com/zed-industries/zed#d9ad6aff67e47de43abb270d22de75dd950f1b48" dependencies = [ "anyhow", "chrono", @@ -11484,7 +11484,7 @@ checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "ztracing" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637" +source = "git+https://github.com/zed-industries/zed#d9ad6aff67e47de43abb270d22de75dd950f1b48" dependencies = [ "tracing", "tracing-subscriber", @@ -11495,7 +11495,7 @@ dependencies = [ [[package]] name = "ztracing_macro" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ec18126b1dbd32b089e51d7edee1e20b3bd53637" +source = "git+https://github.com/zed-industries/zed#d9ad6aff67e47de43abb270d22de75dd950f1b48" [[package]] name = "zune-core" diff --git a/crates/signed_core/src/filters.rs b/crates/signed_core/src/filters.rs index d5ab78d..46e70db 100644 --- a/crates/signed_core/src/filters.rs +++ b/crates/signed_core/src/filters.rs @@ -59,6 +59,32 @@ pub fn grasp_list(public_key: PublicKey) -> Filter { .author(public_key) } +/// NIP-22 comments (kind `1111`) referencing any of the given root events +/// (issues, patches, PRs). +/// +/// Comments are not addressed to the repository — they carry no `a` tag with +/// the repo coordinate — so they must be fetched by their root reference +/// instead. NIP-22 defines the uppercase `E` tag as the root of the thread +/// (used by ngit) while some clients (including Signed itself) reference the +/// root with a lowercase `e` tag, so both are matched. +/// +/// Returns two filters because `#E` and `#e` conditions would be ANDed if +/// combined into one. +pub fn comments_for(roots: impl IntoIterator) -> Vec { + let roots: Vec = roots.into_iter().map(|id| id.to_hex()).collect(); + if roots.is_empty() { + return Vec::new(); + } + vec![ + Filter::new() + .kind(Kind::Comment) + .custom_tags(SingleLetterTag::UPPERCASE_E, roots.clone()), + Filter::new() + .kind(Kind::Comment) + .custom_tags(SingleLetterTag::LOWERCASE_E, roots), + ] +} + /// All repositories announced by an author. pub fn announcements_by(public_key: PublicKey) -> Filter { Filter::new() diff --git a/crates/signed_core/src/status.rs b/crates/signed_core/src/status.rs index 41b87ed..01b3d3f 100644 --- a/crates/signed_core/src/status.rs +++ b/crates/signed_core/src/status.rs @@ -30,9 +30,15 @@ impl RepoStatus { } } -/// Check whether a status event references the given root event via an `e` tag. +/// Check whether an event references the given root event via an `e` or `E` +/// tag. NIP-10 / NIP-34 use the lowercase `e` tag; NIP-22 comments (kind +/// `1111`) use the uppercase `E` tag for the root of the thread. pub fn references_root(event: &Event, root: &EventId) -> bool { - event.tags.event_ids().any(|id| id == *root) + let root = root.to_hex(); + event + .tags + .iter() + .any(|tag| matches!(tag.kind(), "e" | "E") && tag.content() == Some(root.as_str())) } /// Resolve the status of a root event per NIP-34: @@ -96,6 +102,23 @@ mod tests { )); } + #[test] + fn references_root_matches_uppercase_e_tag() { + let root = root_event_id(); + let event = EventBuilder::new(Kind::Comment, "") + .tags([Tag::parse(["E", ROOT_ID_HEX]).expect("valid E tag")]) + .finalize(&keys_from_hex( + "0000000000000000000000000000000000000000000000000000000000000001", + )) + .expect("signed event"); + + assert!(references_root(&event, &root)); + assert!(!references_root( + &event, + &EventId::from_hex(OTHER_ID_HEX).expect("valid id") + )); + } + #[test] fn references_root_false_without_e_tags() { let event = EventBuilder::new(Kind::GitStatusOpen, "") diff --git a/crates/signed_state/src/backend.rs b/crates/signed_state/src/backend.rs index 82128b0..5ac5869 100644 --- a/crates/signed_state/src/backend.rs +++ b/crates/signed_state/src/backend.rs @@ -670,6 +670,30 @@ impl Backend { })); } + /// Connect to relays announced by a repository (NIP-34 `relays` tag) and + /// fetch its events from them: a one-shot auto-closing subscription for + /// `filters`, plus a negentropy sync so issues, patches and PRs stored + /// only on those relays are not missed. + /// + /// Best-effort: failures are logged, not surfaced, because the bootstrap + /// relays already cover the repository. The relays stay in the pool, so + /// events the user publishes for this repository also reach them. + pub fn connect_repo_relays( + &mut self, + relays: Vec, + filters: Vec, + cx: &mut Context, + ) { + let client = self.client.clone(); + + self.tasks.push(cx.spawn(async move |_this, _cx| { + if let Err(e) = connect_repo_relays_only(&client, relays, filters).await { + log::warn!("repo relay fetch failed: {e}"); + } + Ok(()) + })); + } + /// Start a one-shot subscription targeted only at the bootstrap relays, /// auto-closing after EOSE or a short timeout. Matching events are stored /// in the database and surface as [`BackendEvent::NostrUpdate`] while the @@ -832,6 +856,50 @@ impl Backend { } } +/// Add the given relays, connect to them, and fetch the filters: a one-shot +/// subscription (auto-closing after EOSE) plus a negentropy sync per filter +/// as a second pass, so events that race with the subscription or relays +/// with flaky EOSE behavior can't be missed. Relays without NEG-XX support +/// just fail the sync step; the subscription already covered them. +async fn connect_repo_relays_only( + client: &Client, + relays: Vec, + filters: Vec, +) -> Result<(), Error> { + if relays.is_empty() { + return Ok(()); + } + + for url in &relays { + client.add_relay(url).await?; + } + client.connect().await; + + let opts = SubscribeAutoCloseOptions::default() + .exit_policy(ReqExitPolicy::ExitOnEOSE) + .timeout(Some(Duration::from_secs(10))); + + let target: HashMap<&str, Vec> = relays + .iter() + .map(|url| (url.as_str(), filters.clone())) + .collect(); + client.subscribe(target).close_on(opts).await?; + + for filter in filters { + let sync_opts = SyncOptions::default().initial_timeout(Duration::from_secs(5)); + if let Err(e) = client + .sync(filter) + .with(relays.iter()) + .opts(sync_opts) + .await + { + log::warn!("repo relay negentropy sync failed: {e}"); + } + } + + Ok(()) +} + /// Subscribe only on the bootstrap relays, auto-closing after EOSE or a /// short timeout. Use for one-shot data fetches (repo events, profiles) /// instead of persistent gossip-routed subscriptions. diff --git a/crates/signed_state/src/repo.rs b/crates/signed_state/src/repo.rs index 8d7c8ff..0154f55 100644 --- a/crates/signed_state/src/repo.rs +++ b/crates/signed_state/src/repo.rs @@ -1,3 +1,4 @@ +use std::collections::HashSet; use std::time::Duration; use anyhow::Error; @@ -32,6 +33,13 @@ pub struct RepoStore { statuses: Vec, /// Error of the last action initiated from this store, if any. pub last_error: Option, + /// Relays announced by this repository (NIP-34 `relays` tag) that we + /// have already been asked to connect to and fetch from, to avoid + /// re-subscribing on every refresh. + repo_relays: HashSet, + /// Root events (issues, patches, PRs) for which a NIP-22 comment fetch + /// has already been requested, to avoid re-fetching on every refresh. + comment_roots: HashSet, refreshing: bool, refresh_dirty: bool, /// A refresh is waiting out [`REFRESH_DEBOUNCE`]. @@ -41,7 +49,7 @@ pub struct RepoStore { } impl RepoStore { - pub fn new(addr: RepoAddr, cx: &mut Context) -> Self { + pub fn new(addr: RepoAddr, announced_relays: Vec, cx: &mut Context) -> Self { let backend = Backend::global(cx); let subscription = cx.subscribe(&backend, |this, _backend, event, cx| { @@ -53,8 +61,12 @@ impl RepoStore { let coordinate = update.coordinate.as_ref() == Some(&this.addr); let author = update.author == this.addr.public_key; let kind = update.kind == Kind::GitRepoAnnouncement; + // NIP-22 comments carry no `a` tag, so they can't be + // matched by coordinate; any comment may reference this + // repository's roots. + let comment = update.kind == Kind::Comment; - deletion || coordinate || (author && kind) + deletion || coordinate || (author && kind) || comment } BackendEvent::Published(event) => { let kind = event.kind == Kind::GitRepoAnnouncement; @@ -82,6 +94,8 @@ impl RepoStore { comments: Vec::new(), statuses: Vec::new(), last_error: None, + repo_relays: HashSet::new(), + comment_roots: HashSet::new(), refreshing: false, refresh_dirty: false, debouncing: false, @@ -90,6 +104,10 @@ impl RepoStore { }; store.subscribe_remote(cx); + // The announcement we opened the repo from may already list its + // relays; connect to them right away instead of waiting for the + // bootstrap fetch to return the same event. + store.connect_announced_relays(&announced_relays, cx); store.refresh(cx); store } @@ -98,6 +116,42 @@ impl RepoStore { &self.addr } + /// Filters that make up a repository: announcement, state, activity and + /// deletions targeting it. + fn repo_filters(addr: &RepoAddr) -> Vec { + let mut filters = vec![ + filters::announcement(addr), + filters::state(addr), + filters::activity(addr), + ]; + // Deletion requests (NIP-09/62) must be known before any event of + // this repository can be shown. + filters.extend(filters::deletions_for_repo(addr)); + filters + } + + /// Fetch this repository's events from the relays announced in its + /// NIP-34 `relays` tag. Deduplicated: each relay is only contacted once + /// per store, so refreshes after the first are no-ops unless the + /// announcement lists new relays. + fn connect_announced_relays(&mut self, relays: &[RelayUrl], cx: &mut Context) { + let new: Vec = relays + .iter() + .filter(|url| !self.repo_relays.contains(*url)) + .cloned() + .collect(); + if new.is_empty() { + return; + } + self.repo_relays.extend(new.iter().cloned()); + + let backend = Backend::global(cx); + let addr = self.addr.clone(); + backend.update(cx, |backend, cx| { + backend.connect_repo_relays(new, Self::repo_filters(&addr), cx); + }); + } + /// Fetch this repository's events from the bootstrap relays (one-shot, /// auto-closing subscription). fn subscribe_remote(&mut self, cx: &mut Context) { @@ -105,15 +159,7 @@ impl RepoStore { let addr = self.addr.clone(); backend.update(cx, |backend, cx| { - let mut repo_filters = vec![ - filters::announcement(&addr), - filters::state(&addr), - filters::activity(&addr), - ]; - // Deletion requests (NIP-09/62) must be known before any - // event of this repository can be shown. - repo_filters.extend(filters::deletions_for_repo(&addr)); - backend.subscribe_bootstrap(repo_filters, cx); + backend.subscribe_bootstrap(Self::repo_filters(&addr), cx); }); } @@ -196,6 +242,24 @@ impl RepoStore { } } + // NIP-22 comments reference their root via an `E`/`e` tag rather + // than the repository's `a` tag, so query them by the root events + // of this repository. + let db = client.database(); + let mut seen_comments: HashSet = comments.iter().map(|e| e.id).collect(); + let roots = issues + .iter() + .chain(&patches) + .chain(&pull_requests) + .map(|e| e.id); + for filter in filters::comments_for(roots) { + for event in db.query(filter).await? { + if seen_comments.insert(event.id) { + comments.push(event); + } + } + } + sort_newest_first(&mut issues); sort_newest_first(&mut patches); sort_newest_first(&mut pull_requests); @@ -230,6 +294,15 @@ impl RepoStore { let again = this.update(cx, |this, cx| { this.announcement = announcement; + // The announcement may list relays for this repository's + // activity; connect to any we haven't fetched from yet. + let relays = this + .announcement + .as_ref() + .map(|a| a.relays.clone()) + .unwrap_or_default(); + this.connect_announced_relays(&relays, cx); + if let Some((refs, head)) = state { this.refs = refs; this.head = head; @@ -241,6 +314,32 @@ impl RepoStore { this.comments = comments; this.statuses = statuses; + // Comments are not addressed to the repository, so fetch + // them by the root events they reference, on the bootstrap + // relays and on the relays this repository announced. + let roots = this + .issues + .iter() + .chain(&this.patches) + .chain(&this.pull_requests) + .map(|e| e.id) + .collect::>(); + let new_roots: Vec = roots + .iter() + .filter(|id| !this.comment_roots.contains(id)) + .copied() + .collect(); + if !new_roots.is_empty() { + this.comment_roots.extend(new_roots.iter().copied()); + let comment_filters = filters::comments_for(new_roots); + let announced: Vec = this.repo_relays.iter().cloned().collect(); + let backend = Backend::global(cx); + backend.update(cx, |backend, cx| { + backend.subscribe_bootstrap(comment_filters.clone(), cx); + backend.connect_repo_relays(announced, comment_filters, cx); + }); + } + cx.notify(); this.refreshing = false; diff --git a/crates/workspace/src/views/repo_detail/helpers.rs b/crates/workspace/src/views/repo_detail/helpers.rs index b536807..408982d 100644 --- a/crates/workspace/src/views/repo_detail/helpers.rs +++ b/crates/workspace/src/views/repo_detail/helpers.rs @@ -225,7 +225,7 @@ pub(super) fn placeholder(message: &str, cx: &App) -> AnyElement { pub(super) fn status_badge(status: RepoStatus, cx: &App) -> AnyElement { let (icon, label, tooltip, bg, fg) = match status { RepoStatus::Open => ( - CustomIconName::GitIssueOpen, + CustomIconName::GitIssueDone, "open", "Issue is open", cx.theme().primary, diff --git a/crates/workspace/src/views/repo_detail/issue_detail.rs b/crates/workspace/src/views/repo_detail/issue_detail.rs index abca4db..91f4505 100644 --- a/crates/workspace/src/views/repo_detail/issue_detail.rs +++ b/crates/workspace/src/views/repo_detail/issue_detail.rs @@ -1,14 +1,16 @@ use dock::{BasePanel, Panel, PanelEvent}; use gpui::prelude::*; use gpui::{ - App, Context, Entity, EventEmitter, FocusHandle, Focusable, Render, SharedString, Window, div, - px, + AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Render, SharedString, + Window, div, px, relative, }; use gpui_component::avatar::Avatar; use gpui_component::button::{Button, ButtonVariants}; use gpui_component::input::{Textarea, TextareaState}; +use gpui_component::scroll::ScrollableElement; +use gpui_component::tag::Tag; use gpui_component::{ActiveTheme, Sizable, StyledExt, h_flex, v_flex}; -use nostr::prelude::{Event, EventId}; +use nostr::prelude::{Event, EventId, PublicKey}; use signed_core::activity_subject; use signed_state::{ProfileStore, RepoStore}; use utils::relative_time; @@ -46,6 +48,88 @@ impl IssueDetailView { } } + fn render_sidebar(&self, cx: &mut Context) -> impl IntoElement { + let profile_store = ProfileStore::global(cx); + let store = self.store.read(cx); + + let Some(issue) = store.issues.iter().find(|issue| issue.id == self.issue_id) else { + // `render` already bails out when the issue is missing. + return div().into_any_element(); + }; + + // Participants: the issue author plus everyone who commented. + let mut participants: Vec = vec![issue.pubkey]; + participants.extend(store.comments_of(&issue.id).map(|comment| comment.pubkey)); + participants.sort_by_key(PublicKey::to_hex); + participants.dedup(); + + // Issue labels are NIP-34 `t` hashtag tags on the event. + let labels: Vec = issue.tags.hashtags().map(|tag| tag.to_string()).collect(); + + v_flex() + .w(px(240.)) + .h_full() + .flex_none() + .px_4() + .gap_4() + .border_l(px(1.)) + .border_color(cx.theme().sidebar_border) + .child( + v_flex() + .gap_2() + .child(sidebar_title("Participants", cx)) + .children(participants.iter().map(|pubkey| { + let profile = profile_store.read(cx).get(pubkey); + let name = profile.name(); + let picture = profile.picture(); + + h_flex() + .gap_2() + .items_center() + .child( + Avatar::new() + .name(name.clone()) + .when_some(picture, |this, url| this.src(url)) + .rounded(cx.theme().radius) + .small(), + ) + .child(div().text_sm().truncate().text_ellipsis().child(name)) + .into_any_element() + })), + ) + .child( + v_flex() + .gap_2() + .child(sidebar_title("Labels", cx)) + .map(|this| { + if labels.is_empty() { + this.child( + div() + .text_sm() + .text_color(cx.theme().muted_foreground) + .child("No labels"), + ) + } else { + this.child(h_flex().gap_1().children({ + let mut items = vec![]; + + for label in labels.iter() { + items.push( + Tag::secondary() + .outline() + .xsmall() + .child(SharedString::from(label)), + ); + } + + items + })) + } + }), + ) + .into_any_element() + } + fn render_comments(&mut self, id: &EventId, cx: &mut Context) -> impl IntoElement { let store = self.store.read(cx); let comments: Vec<&Event> = store.comments_of(id).collect(); @@ -71,6 +155,7 @@ impl IssueDetailView { Avatar::new() .name(author.clone()) .when_some(picture, |this, url| this.src(url)) + .rounded(cx.theme().radius) .xsmall(), ) .child(author), @@ -165,14 +250,16 @@ impl Focusable for IssueDetailView { impl Render for IssueDetailView { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - // Extract everything owned first: the store borrow must end before - // the markdown state is (re)built below. + let store = self.store.read(cx); + + let Some(issue) = store.issues.iter().find(|issue| issue.id == self.issue_id) else { + return placeholder("Issue not found", cx); + }; + let (title, author, picture, status, age, issue_id, content) = { - let store = self.store.read(cx); - let Some(issue) = store.issues.iter().find(|issue| issue.id == self.issue_id) else { - return placeholder("Issue not found", cx); - }; - let profile = ProfileStore::global(cx).read(cx).get(&issue.pubkey); + let profile_store = ProfileStore::global(cx); + let profile = profile_store.read(cx).get(&issue.pubkey); + ( activity_subject(issue), profile.name(), @@ -184,57 +271,83 @@ impl Render for IssueDetailView { ) }; - v_flex() + h_flex() .id("issue-detail") .size_full() - .overflow_y_scroll() - .gap_6() - .px_4() - .child( - h_flex() - .gap_2() - .items_center() - .child(status_badge(status, cx)) - .child(div().font_semibold().child(title)), - ) .child( v_flex() .px_4() - .gap_8() + .pb_4() + .gap_6() + .size_full() + .min_w_0() + .overflow_y_scrollbar() + .child( + h_flex() + .min_h_16() + .gap_2() + .child(status_badge(status, cx)) + .child( + div() + .flex_1() + .min_w_0() + .font_semibold() + .line_height(relative(1.2)) + .child(title), + ), + ) .child( v_flex() - .gap_2() + .px_4() + .gap_8() .child( - h_flex() - .gap_2() - .text_sm() + v_flex() + .gap_4() .child( h_flex() - .gap_1() + .gap_2() + .text_sm() .child( - Avatar::new() - .name(author.clone()) - .when_some(picture, |this, url| this.src(url)) - .small(), + h_flex() + .gap_1() + .child( + Avatar::new() + .when_some(picture, |this, url| { + this.src(url) + }) + .name(author.clone()) + .rounded(cx.theme().radius) + .small(), + ) + .child(author), ) - .child(author), + .child( + div() + .text_color(cx.theme().muted_foreground) + .child(SharedString::from("opened")), + ) + .child( + div() + .text_color(cx.theme().muted_foreground) + .child(SharedString::from(age)), + ), ) - .child( - div() - .text_color(cx.theme().muted_foreground) - .child(SharedString::from("opened")), - ) - .child( - div() - .text_color(cx.theme().muted_foreground) - .child(SharedString::from(age)), - ), + .child(div().text_sm().child(SharedString::from(&content))), ) - .child(div().text_sm().child(SharedString::from(&content))), - ) - .child(self.render_comments(&issue_id, cx)) - .child(self.render_form(&issue_id, cx)), + .child(self.render_comments(&issue_id, cx)) + .child(self.render_form(&issue_id, cx)), + ), ) + .child(self.render_sidebar(cx)) .into_any_element() } } + +fn sidebar_title(text: &str, cx: &App) -> AnyElement { + div() + .text_xs() + .font_semibold() + .text_color(cx.theme().muted_foreground) + .child(text.to_string()) + .into_any_element() +} diff --git a/crates/workspace/src/views/repo_detail/issues.rs b/crates/workspace/src/views/repo_detail/issues.rs index 0ace4ca..79408b0 100644 --- a/crates/workspace/src/views/repo_detail/issues.rs +++ b/crates/workspace/src/views/repo_detail/issues.rs @@ -177,6 +177,7 @@ impl IssuesView { Avatar::new() .name(author.clone()) .when_some(picture, |this, url| this.src(url)) + .rounded(cx.theme().radius) .small(), ) .child(div().child(author)), diff --git a/crates/workspace/src/views/repo_detail/mod.rs b/crates/workspace/src/views/repo_detail/mod.rs index 1ca7ecb..ae1fbf1 100644 --- a/crates/workspace/src/views/repo_detail/mod.rs +++ b/crates/workspace/src/views/repo_detail/mod.rs @@ -149,7 +149,10 @@ impl RepoDetailView { // cache until the panel closes; free them then. crate::image_cache::clear_on_release(&cx.entity(), window, cx); - let store = cx.new(|cx| RepoStore::new(initial.addr(), cx)); + // The announcement we opened from already carries the repository's + // NIP-34 `relays` tag, so the store can connect to those relays + // immediately instead of waiting for the bootstrap fetch. + let store = cx.new(|cx| RepoStore::new(initial.addr(), initial.relays.clone(), cx)); let tree_state = cx.new(|cx| TreeState::new(cx)); // Empty until the clone completes; populated with the local refs. @@ -1184,6 +1187,7 @@ impl RepoDetailView { Avatar::new() .name(owner_name.clone()) .when_some(owner_picture, |this, url| this.src(url)) + .rounded(cx.theme().radius) .small(), ) .child(div().text_xs().whitespace_nowrap().child(owner_name)), @@ -1195,6 +1199,7 @@ impl RepoDetailView { Avatar::new() .name(profile.name()) .when_some(profile.picture(), |this, url| this.src(url)) + .rounded(cx.theme().radius) }), )) }) diff --git a/crates/workspace/src/views/repo_detail/pull_request_detail.rs b/crates/workspace/src/views/repo_detail/pull_request_detail.rs index 02764a6..9e7651d 100644 --- a/crates/workspace/src/views/repo_detail/pull_request_detail.rs +++ b/crates/workspace/src/views/repo_detail/pull_request_detail.rs @@ -555,6 +555,7 @@ impl PullRequestDetailView { Avatar::new() .name(author.clone()) .when_some(picture, |this, url| this.src(url)) + .rounded(cx.theme().radius) .small(), ) .child(author), @@ -684,6 +685,7 @@ impl PullRequestDetailView { Avatar::new() .name(author.clone()) .when_some(picture, |this, url| this.src(url)) + .rounded(cx.theme().radius) .xsmall(), ) .child(author), diff --git a/crates/workspace/src/views/repo_detail/pull_requests.rs b/crates/workspace/src/views/repo_detail/pull_requests.rs index e1bd047..2abbe69 100644 --- a/crates/workspace/src/views/repo_detail/pull_requests.rs +++ b/crates/workspace/src/views/repo_detail/pull_requests.rs @@ -180,6 +180,7 @@ impl PullRequestsView { Avatar::new() .name(author.clone()) .when_some(picture, |this, url| this.src(url)) + .rounded(cx.theme().radius) .small(), ) .child(div().child(author)), diff --git a/crates/workspace/src/views/repo_list.rs b/crates/workspace/src/views/repo_list.rs index bf91b3f..3282197 100644 --- a/crates/workspace/src/views/repo_list.rs +++ b/crates/workspace/src/views/repo_list.rs @@ -141,6 +141,7 @@ impl RepoListView { Avatar::new() .name(owner.name()) .when_some(owner.picture(), |this, url| this.src(url)) + .rounded(cx.theme().radius) .small(), ) .child( diff --git a/crates/workspace/src/views/sidebar/mod.rs b/crates/workspace/src/views/sidebar/mod.rs index c2fb862..94c0b3e 100644 --- a/crates/workspace/src/views/sidebar/mod.rs +++ b/crates/workspace/src/views/sidebar/mod.rs @@ -126,8 +126,8 @@ impl SidebarPanel { Avatar::new() .name(name.clone()) .when_some(picture, |this, url| this.src(url)) - .small() - .border_0(), + .rounded(cx.theme().radius) + .small(), ) .child(div().text_xs().font_semibold().child(name)), ),