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/lib | |
| 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/lib')
| -rw-r--r-- | src/lib/client.rs | 67 | ||||
| -rw-r--r-- | src/lib/git_events.rs | 2 |
2 files changed, 53 insertions, 16 deletions
diff --git a/src/lib/client.rs b/src/lib/client.rs index 1f46e3c..8501a1f 100644 --- a/src/lib/client.rs +++ b/src/lib/client.rs | |||
| @@ -56,9 +56,9 @@ use crate::{ | |||
| 56 | get_dirs, | 56 | get_dirs, |
| 57 | git::{Repo, RepoActions, get_git_config_item}, | 57 | git::{Repo, RepoActions, get_git_config_item}, |
| 58 | git_events::{ | 58 | git_events::{ |
| 59 | KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, KIND_USER_GRASP_LIST, event_is_cover_letter, | 59 | KIND_COMMENT, KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, KIND_USER_GRASP_LIST, |
| 60 | event_is_patch_set_root, event_is_revision_root, event_is_valid_pr_or_pr_update, | 60 | event_is_cover_letter, event_is_patch_set_root, event_is_revision_root, |
| 61 | status_kinds, | 61 | event_is_valid_pr_or_pr_update, status_kinds, |
| 62 | }, | 62 | }, |
| 63 | login::{get_likely_logged_in_user, user::get_user_ref_from_cache}, | 63 | login::{get_likely_logged_in_user, user::get_user_ref_from_cache}, |
| 64 | repo_ref::{RepoRef, normalize_grasp_server_url}, | 64 | repo_ref::{RepoRef, normalize_grasp_server_url}, |
| @@ -1877,7 +1877,7 @@ async fn create_relays_request( | |||
| 1877 | }) | 1877 | }) |
| 1878 | } | 1878 | } |
| 1879 | 1879 | ||
| 1880 | #[allow(clippy::too_many_lines)] | 1880 | #[allow(clippy::too_many_lines, clippy::too_many_arguments)] |
| 1881 | async fn process_fetched_events( | 1881 | async fn process_fetched_events( |
| 1882 | events: Vec<nostr::Event>, | 1882 | events: Vec<nostr::Event>, |
| 1883 | request: &FetchRequest, | 1883 | request: &FetchRequest, |
| @@ -1996,6 +1996,8 @@ async fn process_fetched_events( | |||
| 1996 | { | 1996 | { |
| 1997 | fresh_profiles.insert(event.pubkey); | 1997 | fresh_profiles.insert(event.pubkey); |
| 1998 | } | 1998 | } |
| 1999 | } else if event.kind.eq(&KIND_COMMENT) { | ||
| 2000 | report.comments.insert(event.id); | ||
| 1999 | } else if [Kind::RelayList, Kind::Metadata, KIND_USER_GRASP_LIST].contains(&event.kind) | 2001 | } else if [Kind::RelayList, Kind::Metadata, KIND_USER_GRASP_LIST].contains(&event.kind) |
| 2000 | { | 2002 | { |
| 2001 | if request.missing_contributor_profiles.contains(&event.pubkey) { | 2003 | if request.missing_contributor_profiles.contains(&event.pubkey) { |
| @@ -2023,9 +2025,7 @@ async fn process_fetched_events( | |||
| 2023 | for event in &events { | 2025 | for event in &events { |
| 2024 | if !request.existing_events.contains(&event.id) { | 2026 | if !request.existing_events.contains(&event.id) { |
| 2025 | let tagged_root_id = event.tags.iter().find_map(|t| { | 2027 | let tagged_root_id = event.tags.iter().find_map(|t| { |
| 2026 | if t.as_slice().len() > 1 | 2028 | if t.as_slice().len() > 1 && (t.as_slice()[0].eq("E") || t.as_slice()[0].eq("e")) { |
| 2027 | && (t.as_slice()[0].eq("E") || t.as_slice()[0].eq("e")) | ||
| 2028 | { | ||
| 2029 | EventId::parse(&t.as_slice()[1]).ok() | 2029 | EventId::parse(&t.as_slice()[1]).ok() |
| 2030 | } else { | 2030 | } else { |
| 2031 | None | 2031 | None |
| @@ -2038,9 +2038,11 @@ async fn process_fetched_events( | |||
| 2038 | // as their parent (new issues/proposals already inflate the count). | 2038 | // as their parent (new issues/proposals already inflate the count). |
| 2039 | if let Some(root_id) = &tagged_root_id { | 2039 | if let Some(root_id) = &tagged_root_id { |
| 2040 | if report.issues.contains(root_id) { | 2040 | if report.issues.contains(root_id) { |
| 2041 | // status for a new issue in this batch — skip (counted via issues) | 2041 | // status for a new issue in this batch — skip (counted |
| 2042 | // via issues) | ||
| 2042 | } else if report.proposals.contains(root_id) { | 2043 | } else if report.proposals.contains(root_id) { |
| 2043 | // status for a new proposal in this batch — skip (counted via proposals) | 2044 | // status for a new proposal in this batch — skip |
| 2045 | // (counted via proposals) | ||
| 2044 | } else if request.issue_ids.contains(root_id) { | 2046 | } else if request.issue_ids.contains(root_id) { |
| 2045 | report.issue_statuses.insert(event.id); | 2047 | report.issue_statuses.insert(event.id); |
| 2046 | } else { | 2048 | } else { |
| @@ -2052,12 +2054,11 @@ async fn process_fetched_events( | |||
| 2052 | let not_tagged_with_new_proposal = tagged_root_id | 2054 | let not_tagged_with_new_proposal = tagged_root_id |
| 2053 | .as_ref() | 2055 | .as_ref() |
| 2054 | .is_none_or(|id| !report.proposals.contains(id)); | 2056 | .is_none_or(|id| !report.proposals.contains(id)); |
| 2055 | if not_tagged_with_new_proposal { | 2057 | if not_tagged_with_new_proposal |
| 2056 | if (event.kind.eq(&Kind::GitPatch) && !event_is_patch_set_root(event)) | 2058 | && ((event.kind.eq(&Kind::GitPatch) && !event_is_patch_set_root(event)) |
| 2057 | || event.kind.eq(&KIND_PULL_REQUEST_UPDATE) | 2059 | || event.kind.eq(&KIND_PULL_REQUEST_UPDATE)) |
| 2058 | { | 2060 | { |
| 2059 | report.commits.insert(event.id); | 2061 | report.commits.insert(event.id); |
| 2060 | } | ||
| 2061 | } | 2062 | } |
| 2062 | } | 2063 | } |
| 2063 | } | 2064 | } |
| @@ -2117,6 +2118,9 @@ pub fn consolidate_fetch_reports(reports: Vec<Result<FetchReport>>) -> FetchRepo | |||
| 2117 | for c in relay_report.issue_statuses { | 2118 | for c in relay_report.issue_statuses { |
| 2118 | report.issue_statuses.insert(c); | 2119 | report.issue_statuses.insert(c); |
| 2119 | } | 2120 | } |
| 2121 | for c in relay_report.comments { | ||
| 2122 | report.comments.insert(c); | ||
| 2123 | } | ||
| 2120 | report.deletions += relay_report.deletions; | 2124 | report.deletions += relay_report.deletions; |
| 2121 | for c in relay_report.contributor_profiles { | 2125 | for c in relay_report.contributor_profiles { |
| 2122 | report.contributor_profiles.insert(c); | 2126 | report.contributor_profiles.insert(c); |
| @@ -2223,6 +2227,24 @@ pub fn get_fetch_filters( | |||
| 2223 | .kinds(status_kinds()), | 2227 | .kinds(status_kinds()), |
| 2224 | ] | 2228 | ] |
| 2225 | }, | 2229 | }, |
| 2230 | // Fetch NIP-22 kind-1111 comments for issues and proposals (patches/PRs). | ||
| 2231 | // Comments use an uppercase `E` tag pointing to the root event ID. | ||
| 2232 | { | ||
| 2233 | let all_root_ids: HashSet<EventId> = issue_ids | ||
| 2234 | .iter() | ||
| 2235 | .chain(proposal_ids.iter()) | ||
| 2236 | .copied() | ||
| 2237 | .collect(); | ||
| 2238 | if all_root_ids.is_empty() { | ||
| 2239 | vec![] | ||
| 2240 | } else { | ||
| 2241 | vec![ | ||
| 2242 | nostr::Filter::default() | ||
| 2243 | .custom_tags(SingleLetterTag::uppercase(Alphabet::E), all_root_ids) | ||
| 2244 | .kind(KIND_COMMENT), | ||
| 2245 | ] | ||
| 2246 | } | ||
| 2247 | }, | ||
| 2226 | // Request kind-5 deletions for state events and repo announcements by | 2248 | // Request kind-5 deletions for state events and repo announcements by |
| 2227 | // their event ID (#e tag), as per NIP-09. The #a-tagged filter above | 2249 | // their event ID (#e tag), as per NIP-09. The #a-tagged filter above |
| 2228 | // covers addressable-event deletions; this covers the specific event IDs | 2250 | // covers addressable-event deletions; this covers the specific event IDs |
| @@ -2309,6 +2331,8 @@ pub struct FetchReport { | |||
| 2309 | statuses: HashSet<EventId>, | 2331 | statuses: HashSet<EventId>, |
| 2310 | issues: HashSet<EventId>, | 2332 | issues: HashSet<EventId>, |
| 2311 | issue_statuses: HashSet<EventId>, | 2333 | issue_statuses: HashSet<EventId>, |
| 2334 | /// NIP-22 kind-1111 comments against issues, patches, and PRs. | ||
| 2335 | comments: HashSet<EventId>, | ||
| 2312 | /// Count of kind-5 deletion events received (for display purposes). | 2336 | /// Count of kind-5 deletion events received (for display purposes). |
| 2313 | deletions: u32, | 2337 | deletions: u32, |
| 2314 | contributor_profiles: HashSet<PublicKey>, | 2338 | contributor_profiles: HashSet<PublicKey>, |
| @@ -2383,7 +2407,18 @@ impl Display for FetchReport { | |||
| 2383 | display_items.push(format!( | 2407 | display_items.push(format!( |
| 2384 | "{} issue status{}", | 2408 | "{} issue status{}", |
| 2385 | self.issue_statuses.len(), | 2409 | self.issue_statuses.len(), |
| 2386 | if self.issue_statuses.len() > 1 { "es" } else { "" }, | 2410 | if self.issue_statuses.len() > 1 { |
| 2411 | "es" | ||
| 2412 | } else { | ||
| 2413 | "" | ||
| 2414 | }, | ||
| 2415 | )); | ||
| 2416 | } | ||
| 2417 | if !self.comments.is_empty() { | ||
| 2418 | display_items.push(format!( | ||
| 2419 | "{} comment{}", | ||
| 2420 | self.comments.len(), | ||
| 2421 | if self.comments.len() > 1 { "s" } else { "" }, | ||
| 2387 | )); | 2422 | )); |
| 2388 | } | 2423 | } |
| 2389 | if self.deletions > 0 { | 2424 | if self.deletions > 0 { |
diff --git a/src/lib/git_events.rs b/src/lib/git_events.rs index 32c23ac..dde0e1a 100644 --- a/src/lib/git_events.rs +++ b/src/lib/git_events.rs | |||
| @@ -88,6 +88,8 @@ pub fn status_kinds() -> Vec<Kind> { | |||
| 88 | pub const KIND_PULL_REQUEST: Kind = Kind::Custom(1618); | 88 | pub const KIND_PULL_REQUEST: Kind = Kind::Custom(1618); |
| 89 | pub const KIND_PULL_REQUEST_UPDATE: Kind = Kind::Custom(1619); | 89 | pub const KIND_PULL_REQUEST_UPDATE: Kind = Kind::Custom(1619); |
| 90 | pub const KIND_USER_GRASP_LIST: Kind = Kind::Custom(10317); | 90 | pub const KIND_USER_GRASP_LIST: Kind = Kind::Custom(10317); |
| 91 | /// NIP-22 comment (kind 1111) — threaded comments on any event. | ||
| 92 | pub const KIND_COMMENT: Kind = Kind::Custom(1111); | ||
| 91 | 93 | ||
| 92 | pub fn event_is_patch_set_root(event: &Event) -> bool { | 94 | pub fn event_is_patch_set_root(event: &Event) -> bool { |
| 93 | event.kind.eq(&Kind::GitPatch) | 95 | event.kind.eq(&Kind::GitPatch) |