From 6d9b0cc8fff65447849d0d55db177dcdff315c48 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Wed, 4 Mar 2026 13:54:32 +0000 Subject: 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 ` and `ngit issue list `. - 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 --- src/bin/ngit/sub_commands/list.rs | 81 +++++++++++++++++++++++++++++++-------- 1 file changed, 65 insertions(+), 16 deletions(-) (limited to 'src/bin/ngit/sub_commands/list.rs') 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::{ }, fetch::fetch_from_git_server, git_events::{ - KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, get_commit_id_from_patch, + KIND_COMMENT, KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, get_commit_id_from_patch, get_pr_tip_event_or_most_recent_patch_with_ancestors, get_status, status_kinds, tag_value, }, repo_ref::{RepoRef, is_grasp_server_in_list}, @@ -201,7 +201,16 @@ pub async fn launch(status: String, json: bool, id: Option, offline: boo .collect(); if let Some(ref event_id_or_nevent) = id { - return show_proposal_details(&filtered_proposals, &repo_ref, event_id_or_nevent, json); + // Resolve the target proposal ID so we can fetch its comment count. + let target_id = resolve_event_id(event_id_or_nevent)?; + let comment_count = get_comment_count_for_proposal(git_repo_path, &target_id).await?; + return show_proposal_details( + &filtered_proposals, + &repo_ref, + event_id_or_nevent, + json, + comment_count, + ); } if json { @@ -213,6 +222,52 @@ pub async fn launch(status: String, json: bool, id: Option, offline: boo Ok(()) } +fn resolve_event_id(event_id_or_nevent: &str) -> Result { + if event_id_or_nevent.starts_with("nevent") { + let nip19 = Nip19::from_bech32(event_id_or_nevent).context("failed to parse nevent")?; + match nip19 { + Nip19::EventId(id) => Ok(id), + Nip19::Event(event) => Ok(event.event_id), + _ => bail!("invalid nevent format"), + } + } else { + nostr::EventId::from_hex(event_id_or_nevent).context("failed to parse event id") + } +} + +/// Count NIP-22 kind-1111 comments whose root `#E` tag matches `proposal_id`. +async fn get_comment_count_for_proposal( + git_repo_path: &std::path::Path, + proposal_id: &nostr::EventId, +) -> Result { + let comments = get_events_from_local_cache( + git_repo_path, + vec![nostr::Filter::default() + .custom_tags( + SingleLetterTag::uppercase(Alphabet::E), + std::iter::once(*proposal_id), + ) + .kind(KIND_COMMENT)], + ) + .await?; + // Only count comments whose uppercase E tag actually points to this proposal + // (the filter is best-effort; verify explicitly). + let count = comments + .iter() + .filter(|c| { + c.tags.iter().any(|t| { + let s = t.as_slice(); + s.len() >= 2 + && s[0].eq("E") + && nostr::EventId::parse(&s[1]) + .map(|id| id == *proposal_id) + .unwrap_or(false) + }) + }) + .count(); + Ok(count) +} + fn status_kind_to_str(kind: Kind) -> &'static str { match kind { Kind::GitStatusOpen => "open", @@ -299,17 +354,9 @@ fn show_proposal_details( _repo_ref: &RepoRef, event_id_or_nevent: &str, json: bool, + comment_count: usize, ) -> Result<()> { - let target_id = if event_id_or_nevent.starts_with("nevent") { - let nip19 = Nip19::from_bech32(event_id_or_nevent).context("failed to parse nevent")?; - match nip19 { - Nip19::EventId(id) => id, - Nip19::Event(event) => event.event_id, - _ => bail!("invalid nevent format"), - } - } else { - nostr::EventId::from_hex(event_id_or_nevent).context("failed to parse event id")? - }; + let target_id = resolve_event_id(event_id_or_nevent)?; let (proposal, status_kind) = proposals .iter() @@ -326,22 +373,24 @@ fn show_proposal_details( "title": cover_letter.title, "author": proposal.pubkey.to_bech32().unwrap_or_default(), "branch": cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?, + "comments": comment_count, "description": cover_letter.description, }); println!("{}", serde_json::to_string_pretty(&json_output)?); return Ok(()); } - println!("Title: {}", cover_letter.title); + println!("Title: {}", cover_letter.title); println!( - "Author: {}", + "Author: {}", proposal.pubkey.to_bech32().unwrap_or_default() ); - println!("Status: {}", status_kind_to_str(*status_kind)); + println!("Status: {}", status_kind_to_str(*status_kind)); println!( - "Branch: {}", + "Branch: {}", cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()? ); + println!("Comments: {comment_count}"); if !cover_letter.description.is_empty() { println!(); -- cgit v1.2.3