feat: hide PRs you can neither act on nor learn from

Overview and the PRs tab listed open PRs from every repo, including ones
cloned for reference (e.g. pterodactyl) whose PRs you can't merge and
didn't author — pure noise. A PR is now surfaced only when you authored
it but can't merge it (your contribution elsewhere) or someone else
authored it and you can (awaiting your review); your own PR in a repo
you control, and strangers' PRs you can't touch, are filtered out.

Providers report viewer merge permission (RepoMeta.viewer_can_merge,
from GitHub/Gitea repo permissions and GitLab access level), stamped
onto each PR at refresh. The cache keeps every PR; the filter is applied
where the hub builds state.prs, so the per-repo detail view is unchanged.
This commit is contained in:
dualfroz committed 2026-07-11 22:19:59 +02:00
1 parent 4a3fa58745
commit abb1e92064
8 files changed
+68 -5

No files matched your search

+28
View File
@@ -11,6 +11,34 @@ use std::collections::HashSet;
use std::path::PathBuf;
use super::state::HubState;
// ── PR relevance ──────────────────────────────────────────────────────────────
/// Account logins to treat as "you" when deciding PR authorship.
pub fn account_logins(accounts: &[crate::auth::Account]) -> Vec<String> {
accounts.iter().map(|a| a.username.clone()).filter(|u| !u.is_empty()).collect()
}
/// Whether a PR is worth surfacing in the hub. Shown only when you authored
/// it but can't merge it yourself (your contribution to a repo you don't
/// control) or someone else authored it and you can (it's awaiting your
/// review). Your own PR in a repo you can merge in, and strangers' PRs you
/// can't act on, are both noise and filtered out. `logins` are your account
/// usernames; an empty `logins` shows everything (identity unknown).
pub fn pr_is_relevant(pr: &crate::providers::PrDetail, logins: &[String]) -> bool {
if logins.is_empty() { return true; }
let mine = logins.iter().any(|u| u.eq_ignore_ascii_case(&pr.author));
mine != pr.viewer_can_merge
}
/// Filters a cached PR list down to the relevant ones (see `pr_is_relevant`).
pub fn relevant_prs(
prs: impl IntoIterator<Item = crate::providers::PrDetail>,
accounts: &[crate::auth::Account],
) -> Vec<crate::providers::PrDetail> {
let logins = account_logins(accounts);
prs.into_iter().filter(|p| pr_is_relevant(p, &logins)).collect()
}
// ── sync flow ─────────────────────────────────────────────────────────────────
/// Runs `action` ("pull", "commit_push", or anything else meaning "push")
+1 -1
View File
@@ -157,7 +157,7 @@ pub fn poll_refresh(state: &mut HubState) {
for (name, i) in issues { fetched_iss += i.len(); cache.set_issues(&name, i); }
let _ = crate::cache::save(&cache);
state.prs = cache.all_prs().into_iter().cloned().collect();
state.prs = super::actions::relevant_prs(cache.all_prs().into_iter().cloned(), &state.accounts);
state.issues = cache.all_issues().into_iter().cloned().collect();
if closed_counts.is_some() { state.closed_counts = closed_counts; }
+7 -3
View File
@@ -91,14 +91,18 @@ pub async fn run_overview_tui(
if r.visibility.is_none() { r.visibility = m.visibility.clone(); }
}
}
let cached_prs: Vec<PrDetail> = meta_cache.all_prs().into_iter().cloned().collect();
let cached_issues: Vec<IssueDetail> = meta_cache.all_issues().into_iter().cloned().collect();
let debug_mode = crate::debug::is_enabled();
let debug_log = crate::debug::take();
let accounts = crate::auth::load_accounts_path(&cfg.data_dir).unwrap_or_default();
let templates = load_templates();
// The cache keeps every PR; the hub only surfaces the ones you can act on
// (yours-elsewhere or awaiting-your-review — see actions::pr_is_relevant),
// so PRs from repos you merely cloned don't clutter Overview and the tab.
let cached_prs: Vec<PrDetail> =
actions::relevant_prs(meta_cache.all_prs().into_iter().cloned(), &accounts);
let cached_issues: Vec<IssueDetail> = meta_cache.all_issues().into_iter().cloned().collect();
let mut state = HubState {
repos, projects, project_map, todos,
prs: cached_prs,
+5 -1
View File
@@ -816,6 +816,7 @@ pub fn spawn_api_refresh(state: &mut HubState) {
.await;
for (name, slug, meta, p, i, ci) in results {
let can_merge = meta.as_ref().map(|m| m.viewer_can_merge).unwrap_or(false);
if let Some(meta) = meta {
let slug_lc = slug.to_lowercase();
let dep = dependabot
@@ -837,7 +838,10 @@ pub fn spawn_api_refresh(state: &mut HubState) {
fetched_at: chrono::Utc::now().timestamp(),
}));
}
if let Some(p) = p { prs.push((name.clone(), p)); }
if let Some(mut p) = p {
for pr in &mut p { pr.viewer_can_merge = can_merge; }
prs.push((name.clone(), p));
}
if let Some(i) = i { issues.push((name, i)); }
}