diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-03-04 13:54:32 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-03-04 14:50:01 +0000 |
| commit | 6d9b0cc8fff65447849d0d55db177dcdff315c48 (patch) | |
| tree | 604587b7e06149d89a36383eb2e4227043e6955d /src/bin/ngit/sub_commands/list.rs | |
| parent | 3908abbbfc5e748dd168d22bf5e3ea6aae17de61 (diff) | |
feat: fetch and display NIP-22 comment counts on issues and proposals
Download kind-1111 NIP-22 comments from relays and show a comment count
in the detail view of `ngit list <id>` and `ngit issue list <id>`.
- git_events: add KIND_COMMENT constant (kind 1111)
- client: import KIND_COMMENT; add `comments` field to FetchReport;
route kind-1111 events into report.comments in process_fetched_events;
consolidate comments across relay reports; display "N comment(s)" in
FetchReport; add a #E-tagged kind-1111 filter in get_fetch_filters
covering all known issue and proposal root IDs
- issue_list: add get_comment_counts() to query the local cache for
kind-1111 events by #E tag and count per issue; extend the filtered
tuple with comment_count; show a CMTS column in the table, a
"Comments: N" line in the detail view, and a "comments" field in JSON
- list: add KIND_COMMENT import; add resolve_event_id() helper and
get_comment_count_for_proposal() to look up the count for a single
proposal from the local cache; pass comment_count into
show_proposal_details(); display "Comments: N" in plain text and
"comments" in JSON; align detail-view labels
Diffstat (limited to 'src/bin/ngit/sub_commands/list.rs')
| -rw-r--r-- | src/bin/ngit/sub_commands/list.rs | 81 |
1 files changed, 65 insertions, 16 deletions
diff --git a/src/bin/ngit/sub_commands/list.rs b/src/bin/ngit/sub_commands/list.rs index 3d5e876..d1b6de8 100644 --- a/src/bin/ngit/sub_commands/list.rs +++ b/src/bin/ngit/sub_commands/list.rs | |||
| @@ -15,7 +15,7 @@ use ngit::{ | |||
| 15 | }, | 15 | }, |
| 16 | fetch::fetch_from_git_server, | 16 | fetch::fetch_from_git_server, |
| 17 | git_events::{ | 17 | git_events::{ |
| 18 | KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, get_commit_id_from_patch, | 18 | KIND_COMMENT, KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, get_commit_id_from_patch, |
| 19 | get_pr_tip_event_or_most_recent_patch_with_ancestors, get_status, status_kinds, tag_value, | 19 | get_pr_tip_event_or_most_recent_patch_with_ancestors, get_status, status_kinds, tag_value, |
| 20 | }, | 20 | }, |
| 21 | repo_ref::{RepoRef, is_grasp_server_in_list}, | 21 | repo_ref::{RepoRef, is_grasp_server_in_list}, |
| @@ -201,7 +201,16 @@ pub async fn launch(status: String, json: bool, id: Option<String>, offline: boo | |||
| 201 | .collect(); | 201 | .collect(); |
| 202 | 202 | ||
| 203 | if let Some(ref event_id_or_nevent) = id { | 203 | if let Some(ref event_id_or_nevent) = id { |
| 204 | return show_proposal_details(&filtered_proposals, &repo_ref, event_id_or_nevent, json); | 204 | // Resolve the target proposal ID so we can fetch its comment count. |
| 205 | let target_id = resolve_event_id(event_id_or_nevent)?; | ||
| 206 | let comment_count = get_comment_count_for_proposal(git_repo_path, &target_id).await?; | ||
| 207 | return show_proposal_details( | ||
| 208 | &filtered_proposals, | ||
| 209 | &repo_ref, | ||
| 210 | event_id_or_nevent, | ||
| 211 | json, | ||
| 212 | comment_count, | ||
| 213 | ); | ||
| 205 | } | 214 | } |
| 206 | 215 | ||
| 207 | if json { | 216 | if json { |
| @@ -213,6 +222,52 @@ pub async fn launch(status: String, json: bool, id: Option<String>, offline: boo | |||
| 213 | Ok(()) | 222 | Ok(()) |
| 214 | } | 223 | } |
| 215 | 224 | ||
| 225 | fn resolve_event_id(event_id_or_nevent: &str) -> Result<nostr::EventId> { | ||
| 226 | if event_id_or_nevent.starts_with("nevent") { | ||
| 227 | let nip19 = Nip19::from_bech32(event_id_or_nevent).context("failed to parse nevent")?; | ||
| 228 | match nip19 { | ||
| 229 | Nip19::EventId(id) => Ok(id), | ||
| 230 | Nip19::Event(event) => Ok(event.event_id), | ||
| 231 | _ => bail!("invalid nevent format"), | ||
| 232 | } | ||
| 233 | } else { | ||
| 234 | nostr::EventId::from_hex(event_id_or_nevent).context("failed to parse event id") | ||
| 235 | } | ||
| 236 | } | ||
| 237 | |||
| 238 | /// Count NIP-22 kind-1111 comments whose root `#E` tag matches `proposal_id`. | ||
| 239 | async fn get_comment_count_for_proposal( | ||
| 240 | git_repo_path: &std::path::Path, | ||
| 241 | proposal_id: &nostr::EventId, | ||
| 242 | ) -> Result<usize> { | ||
| 243 | let comments = get_events_from_local_cache( | ||
| 244 | git_repo_path, | ||
| 245 | vec![nostr::Filter::default() | ||
| 246 | .custom_tags( | ||
| 247 | SingleLetterTag::uppercase(Alphabet::E), | ||
| 248 | std::iter::once(*proposal_id), | ||
| 249 | ) | ||
| 250 | .kind(KIND_COMMENT)], | ||
| 251 | ) | ||
| 252 | .await?; | ||
| 253 | // Only count comments whose uppercase E tag actually points to this proposal | ||
| 254 | // (the filter is best-effort; verify explicitly). | ||
| 255 | let count = comments | ||
| 256 | .iter() | ||
| 257 | .filter(|c| { | ||
| 258 | c.tags.iter().any(|t| { | ||
| 259 | let s = t.as_slice(); | ||
| 260 | s.len() >= 2 | ||
| 261 | && s[0].eq("E") | ||
| 262 | && nostr::EventId::parse(&s[1]) | ||
| 263 | .map(|id| id == *proposal_id) | ||
| 264 | .unwrap_or(false) | ||
| 265 | }) | ||
| 266 | }) | ||
| 267 | .count(); | ||
| 268 | Ok(count) | ||
| 269 | } | ||
| 270 | |||
| 216 | fn status_kind_to_str(kind: Kind) -> &'static str { | 271 | fn status_kind_to_str(kind: Kind) -> &'static str { |
| 217 | match kind { | 272 | match kind { |
| 218 | Kind::GitStatusOpen => "open", | 273 | Kind::GitStatusOpen => "open", |
| @@ -299,17 +354,9 @@ fn show_proposal_details( | |||
| 299 | _repo_ref: &RepoRef, | 354 | _repo_ref: &RepoRef, |
| 300 | event_id_or_nevent: &str, | 355 | event_id_or_nevent: &str, |
| 301 | json: bool, | 356 | json: bool, |
| 357 | comment_count: usize, | ||
| 302 | ) -> Result<()> { | 358 | ) -> Result<()> { |
| 303 | let target_id = if event_id_or_nevent.starts_with("nevent") { | 359 | let target_id = resolve_event_id(event_id_or_nevent)?; |
| 304 | let nip19 = Nip19::from_bech32(event_id_or_nevent).context("failed to parse nevent")?; | ||
| 305 | match nip19 { | ||
| 306 | Nip19::EventId(id) => id, | ||
| 307 | Nip19::Event(event) => event.event_id, | ||
| 308 | _ => bail!("invalid nevent format"), | ||
| 309 | } | ||
| 310 | } else { | ||
| 311 | nostr::EventId::from_hex(event_id_or_nevent).context("failed to parse event id")? | ||
| 312 | }; | ||
| 313 | 360 | ||
| 314 | let (proposal, status_kind) = proposals | 361 | let (proposal, status_kind) = proposals |
| 315 | .iter() | 362 | .iter() |
| @@ -326,22 +373,24 @@ fn show_proposal_details( | |||
| 326 | "title": cover_letter.title, | 373 | "title": cover_letter.title, |
| 327 | "author": proposal.pubkey.to_bech32().unwrap_or_default(), | 374 | "author": proposal.pubkey.to_bech32().unwrap_or_default(), |
| 328 | "branch": cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?, | 375 | "branch": cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?, |
| 376 | "comments": comment_count, | ||
| 329 | "description": cover_letter.description, | 377 | "description": cover_letter.description, |
| 330 | }); | 378 | }); |
| 331 | println!("{}", serde_json::to_string_pretty(&json_output)?); | 379 | println!("{}", serde_json::to_string_pretty(&json_output)?); |
| 332 | return Ok(()); | 380 | return Ok(()); |
| 333 | } | 381 | } |
| 334 | 382 | ||
| 335 | println!("Title: {}", cover_letter.title); | 383 | println!("Title: {}", cover_letter.title); |
| 336 | println!( | 384 | println!( |
| 337 | "Author: {}", | 385 | "Author: {}", |
| 338 | proposal.pubkey.to_bech32().unwrap_or_default() | 386 | proposal.pubkey.to_bech32().unwrap_or_default() |
| 339 | ); | 387 | ); |
| 340 | println!("Status: {}", status_kind_to_str(*status_kind)); | 388 | println!("Status: {}", status_kind_to_str(*status_kind)); |
| 341 | println!( | 389 | println!( |
| 342 | "Branch: {}", | 390 | "Branch: {}", |
| 343 | cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()? | 391 | cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()? |
| 344 | ); | 392 | ); |
| 393 | println!("Comments: {comment_count}"); | ||
| 345 | 394 | ||
| 346 | if !cover_letter.description.is_empty() { | 395 | if !cover_letter.description.is_empty() { |
| 347 | println!(); | 396 | println!(); |