update repo list
This commit is contained in:
@@ -7,6 +7,7 @@ publish.workspace = true
|
||||
[dependencies]
|
||||
signed_core = { path = "../signed_core" }
|
||||
signed_nostr = { path = "../signed_nostr" }
|
||||
utils = { path = "../utils" }
|
||||
|
||||
nostr.workspace = true
|
||||
nostr-sdk.workspace = true
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user