update repo list

This commit is contained in:
2026-08-08 08:44:02 +07:00
parent de9673e8fb
commit 322f6f60bc
12 changed files with 226 additions and 20 deletions
+3 -1
View File
@@ -7,7 +7,9 @@ use std::path::Path;
pub use backend::{Backend, BackendEvent};
use gpui::{App, AppContext, Entity};
pub use profile::{Profile, ProfileStore, shorten_pubkey};
pub use nostr_sdk::prelude::Timestamp;
pub use profile::{Profile, ProfileStore};
pub use utils::shorten_pubkey;
pub use repo::RepoStore;
pub use repo_list::RepoListStore;
use signed_nostr::new_backend;
+1 -6
View File
@@ -6,6 +6,7 @@ use anyhow::Error;
use flume::{Receiver, RecvTimeoutError, Sender};
use gpui::{App, AppContext, Context, Entity, Global, SharedString, Subscription, Task};
use nostr_sdk::prelude::*;
use utils::shorten_pubkey;
use crate::backend::{Backend, BackendEvent, sync_bootstrap_only};
@@ -59,12 +60,6 @@ impl Profile {
}
}
/// Shorten a [`PublicKey`] to `npub1abc...wxyz` form.
pub fn shorten_pubkey(public_key: PublicKey, len: usize) -> String {
let npub = public_key.to_bech32().unwrap();
format!("{}...{}", &npub[..(len + 5)], &npub[npub.len() - len..])
}
/// Message from the fetch task to the main thread.
enum Dispatch {
/// A batched sync finished; re-read seen profiles from the database.
+71 -8
View File
@@ -5,7 +5,7 @@ use std::time::Duration;
use anyhow::Error;
use gpui::{AppContext, Context, Subscription, Task};
use nostr_sdk::prelude::*;
use signed_core::{Announcement, RepoAddr, filters};
use signed_core::{Announcement, RepoAddr, filters, repo_addr};
use crate::backend::{Backend, BackendEvent};
@@ -13,10 +13,16 @@ use crate::backend::{Backend, BackendEvent};
/// events (e.g. sync progress ticks) collapse into one query.
const REFRESH_DEBOUNCE: Duration = Duration::from_millis(300);
/// How far back activity events count toward a repository's last activity.
const ACTIVITY_WINDOW: Duration = Duration::from_secs(90 * 86_400);
/// Store listing repository announcements (global discovery or per-author).
pub struct RepoListStore {
/// Shared so views can clone the list per frame without a deep copy.
pub announcements: Arc<Vec<Announcement>>,
/// Latest known activity timestamp per repository
/// (announcements, state updates, patches, PRs, issues, statuses).
pub last_activity: Arc<HashMap<RepoAddr, Timestamp>>,
author: Option<PublicKey>,
refreshing: bool,
refresh_dirty: bool,
@@ -32,14 +38,22 @@ impl RepoListStore {
let backend = Backend::global(cx);
let subscription = cx.subscribe(&backend, |this, _backend, event, cx| {
let git_kind = Kind::GitRepoAnnouncement;
let relevant = match event {
BackendEvent::NostrUpdate(update) => {
update.kind == git_kind && this.author.is_none_or(|a| a == update.author)
// Activity (patches, issues, ...) is addressed to repos via
// `a` tags, so its author isn't the repo owner; always refresh.
if filters::ACTIVITY_KINDS.contains(&update.kind) {
true
} else {
let is_announcement = update.kind == Kind::GitRepoAnnouncement;
let is_repo_state = update.kind == Kind::RepoState;
let tracked = is_announcement || is_repo_state;
tracked && this.author.is_none_or(|a| a == update.author)
}
}
BackendEvent::Published(event) => {
event.kind == git_kind && this.author.is_none_or(|a| a == event.pubkey)
event.kind == Kind::GitRepoAnnouncement
&& this.author.is_none_or(|a| a == event.pubkey)
}
BackendEvent::Synced | BackendEvent::SyncProgress { .. } => true,
_ => false,
@@ -52,6 +66,7 @@ impl RepoListStore {
let mut store = Self {
announcements: Arc::new(Vec::new()),
last_activity: Arc::new(HashMap::new()),
author,
refreshing: false,
refresh_dirty: false,
@@ -151,12 +166,59 @@ impl RepoListStore {
let mut announcements: Vec<Announcement> = by_repo.into_values().collect();
announcements.sort_by_key(|a| std::cmp::Reverse(a.created_at));
Ok::<_, Error>(announcements)
// Last activity per repository: state updates plus all NIP-34
// activity events (patches, PRs, issues, statuses).
let mut last_activity: HashMap<RepoAddr, Timestamp> = announcements
.iter()
.map(|a| (a.addr(), a.created_at))
.collect();
let state_filter = Filter::new().kind(Kind::RepoState);
for event in client.database().query(state_filter).await? {
let Some(id) = event.tags.identifier() else {
continue;
};
let addr = repo_addr(event.pubkey, id);
let Some(entry) = last_activity.get_mut(&addr) else {
continue;
};
*entry = (*entry).max(event.created_at);
}
// Bound the activity query to a recent window; older repos fall
// back to their announcement / state timestamps.
let activity_filter = Filter::new()
.kinds(filters::ACTIVITY_KINDS)
.since(Timestamp::now() - ACTIVITY_WINDOW);
for event in client.database().query(activity_filter).await? {
for tag in event.tags.iter() {
if tag.kind() != "a" {
continue;
}
let Some(content) = tag.content() else {
continue;
};
let Ok(addr) = Coordinate::parse(content) else {
continue;
};
if addr.kind != Kind::GitRepoAnnouncement {
continue;
}
// Skip events for repos we don't list, so the map can't
// grow beyond the number of announcements.
let Some(entry) = last_activity.get_mut(&addr) else {
continue;
};
*entry = (*entry).max(event.created_at);
}
}
Ok::<_, Error>((announcements, last_activity))
});
self.tasks.push(cx.spawn(async move |this, cx| {
let announcements = match work.await {
Ok(announcements) => announcements,
let (announcements, last_activity) = match work.await {
Ok(results) => results,
// Database errors are transient; keep the last list.
Err(_) => {
return this.update(cx, |this, _cx| {
@@ -167,6 +229,7 @@ impl RepoListStore {
let again = this.update(cx, |this, cx| {
this.announcements = Arc::new(announcements);
this.last_activity = Arc::new(last_activity);
cx.notify();
this.refreshing = false;