feat: out-of-box experience #2
+1
-1
@@ -20,7 +20,7 @@ reqwest_client = { git = "https://github.com/zed-industries/zed" }
|
|||||||
|
|
||||||
gpui-component = { git = "https://github.com/longbridge/gpui-component" }
|
gpui-component = { git = "https://github.com/longbridge/gpui-component" }
|
||||||
|
|
||||||
nostr = { git = "https://github.com/rust-nostr/nostr", rev = "d0a1d67d3c9e5cf9710807a6a414c155a5f47215", features = ["nip59", "nip49", "nip44"] }
|
nostr = { git = "https://github.com/rust-nostr/nostr", rev = "d0a1d67d3c9e5cf9710807a6a414c155a5f47215", features = ["nip59", "nip49", "nip44", "os-rng"] }
|
||||||
nostr-lmdb = { git = "https://github.com/rust-nostr/nostr", rev = "d0a1d67d3c9e5cf9710807a6a414c155a5f47215" }
|
nostr-lmdb = { git = "https://github.com/rust-nostr/nostr", rev = "d0a1d67d3c9e5cf9710807a6a414c155a5f47215" }
|
||||||
nostr-memory = { git = "https://github.com/rust-nostr/nostr", rev = "d0a1d67d3c9e5cf9710807a6a414c155a5f47215" }
|
nostr-memory = { git = "https://github.com/rust-nostr/nostr", rev = "d0a1d67d3c9e5cf9710807a6a414c155a5f47215" }
|
||||||
nostr-blossom = { git = "https://github.com/rust-nostr/nostr", rev = "d0a1d67d3c9e5cf9710807a6a414c155a5f47215" }
|
nostr-blossom = { git = "https://github.com/rust-nostr/nostr", rev = "d0a1d67d3c9e5cf9710807a6a414c155a5f47215" }
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ pub mod addr;
|
|||||||
pub mod clone_url;
|
pub mod clone_url;
|
||||||
pub mod filters;
|
pub mod filters;
|
||||||
pub mod model;
|
pub mod model;
|
||||||
|
pub mod state;
|
||||||
pub mod status;
|
pub mod status;
|
||||||
|
|
||||||
pub use addr::{RepoAddr, repo_addr};
|
pub use addr::{RepoAddr, repo_addr};
|
||||||
pub use clone_url::{CloneTarget, parse_clone_url};
|
pub use clone_url::{CloneTarget, parse_clone_url};
|
||||||
pub use model::Announcement;
|
pub use model::Announcement;
|
||||||
|
pub use state::parse_state;
|
||||||
pub use status::{RepoStatus, references_root, resolve_status};
|
pub use status::{RepoStatus, references_root, resolve_status};
|
||||||
|
|||||||
@@ -81,3 +81,109 @@ impl Announcement {
|
|||||||
crate::repo_addr(self.owner, self.id.clone())
|
crate::repo_addr(self.owner, self.id.clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const MAINTAINER_HEX: &str = "68d81165918100b7da43fc28f7d1fc12554466e1115886b9e7bb326f65ec4272";
|
||||||
|
|
||||||
|
fn keys() -> Keys {
|
||||||
|
Keys::new(
|
||||||
|
SecretKey::from_hex("0000000000000000000000000000000000000000000000000000000000000001")
|
||||||
|
.expect("valid secret key"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a signed kind `30617` event from raw tag values.
|
||||||
|
fn announcement_event(tags: &[&[&str]]) -> Event {
|
||||||
|
let tags: Vec<Tag> = tags
|
||||||
|
.iter()
|
||||||
|
.map(|t| Tag::parse(t.to_vec()).expect("valid tag"))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
EventBuilder::new(Kind::GitRepoAnnouncement, "")
|
||||||
|
.tags(tags)
|
||||||
|
.finalize(&keys())
|
||||||
|
.expect("signed event")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_full_announcement() {
|
||||||
|
let event = announcement_event(&[
|
||||||
|
&["d", "my-repo"],
|
||||||
|
&["name", "My Repo"],
|
||||||
|
&["description", "A test repository"],
|
||||||
|
&["web", "https://example.com/repo"],
|
||||||
|
&["clone", "https://example.com/repo.git"],
|
||||||
|
&["relays", "wss://relay.example.com"],
|
||||||
|
&["r", "aa231c4c6a5777dc89b42207b499891a344add5c", "euc"],
|
||||||
|
&["maintainers", MAINTAINER_HEX],
|
||||||
|
]);
|
||||||
|
|
||||||
|
let announcement = Announcement::from_event(&event).expect("parses");
|
||||||
|
|
||||||
|
assert_eq!(announcement.owner, keys().public_key());
|
||||||
|
assert_eq!(announcement.id, "my-repo");
|
||||||
|
assert_eq!(announcement.name.as_deref(), Some("My Repo"));
|
||||||
|
assert_eq!(
|
||||||
|
announcement.description.as_deref(),
|
||||||
|
Some("A test repository")
|
||||||
|
);
|
||||||
|
assert_eq!(announcement.web, vec!["https://example.com/repo"]);
|
||||||
|
assert_eq!(announcement.clone, vec!["https://example.com/repo.git"]);
|
||||||
|
assert_eq!(announcement.relays, vec!["wss://relay.example.com"]);
|
||||||
|
assert_eq!(
|
||||||
|
announcement.euc.as_deref(),
|
||||||
|
Some("aa231c4c6a5777dc89b42207b499891a344add5c")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
announcement.maintainers,
|
||||||
|
vec![PublicKey::from_hex(MAINTAINER_HEX).expect("valid pubkey")]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn requires_d_tag() {
|
||||||
|
let event = announcement_event(&[&["name", "No id"]]);
|
||||||
|
|
||||||
|
assert!(Announcement::from_event(&event).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ignores_other_kinds() {
|
||||||
|
let event = EventBuilder::new(Kind::GitIssue, "")
|
||||||
|
.finalize(&keys())
|
||||||
|
.expect("signed event");
|
||||||
|
|
||||||
|
assert!(Announcement::from_event(&event).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn drops_malformed_values() {
|
||||||
|
let event = announcement_event(&[
|
||||||
|
&["d", "my-repo"],
|
||||||
|
&["clone", "not a url"],
|
||||||
|
&["relays", "wss://good.example.com"],
|
||||||
|
&["maintainers", "not-a-pubkey"],
|
||||||
|
]);
|
||||||
|
|
||||||
|
let announcement = Announcement::from_event(&event).expect("parses");
|
||||||
|
|
||||||
|
// An invalid URL keeps the whole clone tag from being parsed.
|
||||||
|
assert!(announcement.clone.is_empty());
|
||||||
|
assert_eq!(announcement.relays, vec!["wss://good.example.com"]);
|
||||||
|
assert!(announcement.maintainers.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ignores_unknown_tags() {
|
||||||
|
let event = announcement_event(&[&["d", "my-repo"], &["t", "label"], &["subject", "n/a"]]);
|
||||||
|
|
||||||
|
let announcement = Announcement::from_event(&event).expect("parses");
|
||||||
|
|
||||||
|
assert_eq!(announcement.id, "my-repo");
|
||||||
|
assert!(announcement.name.is_none());
|
||||||
|
assert!(announcement.web.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
use nostr::prelude::*;
|
||||||
|
|
||||||
|
/// Parse a kind `30618` repository state event into refs and HEAD.
|
||||||
|
///
|
||||||
|
/// `refs` are `(refname, commit-id)` pairs; `head` is the branch pointed to
|
||||||
|
/// by the `HEAD` tag, if any.
|
||||||
|
pub fn parse_state(event: &Event) -> (Vec<(String, String)>, Option<String>) {
|
||||||
|
let mut refs = Vec::new();
|
||||||
|
let mut head = None;
|
||||||
|
|
||||||
|
for tag in event.tags.iter() {
|
||||||
|
match Nip34Tag::parse(tag.as_slice()) {
|
||||||
|
Ok(Nip34Tag::Head(branch)) => head = Some(branch),
|
||||||
|
Ok(Nip34Tag::RefHead { branch, commit }) => {
|
||||||
|
refs.push((format!("refs/heads/{branch}"), commit.to_string()));
|
||||||
|
}
|
||||||
|
Ok(Nip34Tag::RefTag { name, commit }) => {
|
||||||
|
refs.push((format!("refs/tags/{name}"), commit.to_string()));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(refs, head)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const COMMIT_A: &str = "aa231c4c6a5777dc89b42207b499891a344add5c";
|
||||||
|
const COMMIT_B: &str = "59429cfc6cb35b0a1ddace73b5a5c5ed57b8f5ca";
|
||||||
|
|
||||||
|
fn keys() -> Keys {
|
||||||
|
Keys::new(
|
||||||
|
SecretKey::from_hex("0000000000000000000000000000000000000000000000000000000000000001")
|
||||||
|
.expect("valid secret key"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a signed kind `30618` event from raw tag values.
|
||||||
|
fn state_event(tags: &[&[&str]]) -> Event {
|
||||||
|
let tags: Vec<Tag> = tags
|
||||||
|
.iter()
|
||||||
|
.map(|t| Tag::parse(t.to_vec()).expect("valid tag"))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
EventBuilder::new(Kind::RepoState, "")
|
||||||
|
.tags(tags)
|
||||||
|
.finalize(&keys())
|
||||||
|
.expect("signed event")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_heads_and_tags() {
|
||||||
|
let event = state_event(&[
|
||||||
|
&["HEAD", "ref: refs/heads/main"],
|
||||||
|
&["refs/heads/main", COMMIT_A],
|
||||||
|
&["refs/heads/dev", COMMIT_B],
|
||||||
|
&["refs/tags/v1.0", COMMIT_A],
|
||||||
|
]);
|
||||||
|
|
||||||
|
let (refs, head) = parse_state(&event);
|
||||||
|
|
||||||
|
assert_eq!(head.as_deref(), Some("main"));
|
||||||
|
assert_eq!(
|
||||||
|
refs,
|
||||||
|
vec![
|
||||||
|
("refs/heads/main".to_owned(), COMMIT_A.to_owned()),
|
||||||
|
("refs/heads/dev".to_owned(), COMMIT_B.to_owned()),
|
||||||
|
("refs/tags/v1.0".to_owned(), COMMIT_A.to_owned()),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn head_without_prefix_is_ignored() {
|
||||||
|
let event = state_event(&[&["HEAD", "main"]]);
|
||||||
|
|
||||||
|
let (refs, head) = parse_state(&event);
|
||||||
|
|
||||||
|
assert!(refs.is_empty());
|
||||||
|
assert!(head.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ignores_non_state_tags() {
|
||||||
|
let event = state_event(&[&["d", "my-repo"], &["name", "ignored"]]);
|
||||||
|
|
||||||
|
let (refs, head) = parse_state(&event);
|
||||||
|
|
||||||
|
assert!(refs.is_empty());
|
||||||
|
assert!(head.is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,3 +54,137 @@ where
|
|||||||
.and_then(|e| RepoStatus::from_kind(e.kind))
|
.and_then(|e| RepoStatus::from_kind(e.kind))
|
||||||
.unwrap_or(RepoStatus::Open)
|
.unwrap_or(RepoStatus::Open)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const ROOT_ID_HEX: &str = "1111111111111111111111111111111111111111111111111111111111111111";
|
||||||
|
const OTHER_ID_HEX: &str = "2222222222222222222222222222222222222222222222222222222222222222";
|
||||||
|
|
||||||
|
fn keys_from_hex(hex: &str) -> Keys {
|
||||||
|
Keys::new(SecretKey::from_hex(hex).expect("valid secret key"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn root_event_id() -> EventId {
|
||||||
|
EventId::from_hex(ROOT_ID_HEX).expect("valid event id")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a signed status event with a controlled `created_at`.
|
||||||
|
fn status_event(author: &Keys, kind: Kind, root: EventId, created_at: u64) -> Event {
|
||||||
|
EventBuilder::new(kind, "")
|
||||||
|
.tags([Tag::event(root)])
|
||||||
|
.custom_created_at(Timestamp::from(created_at))
|
||||||
|
.finalize(author)
|
||||||
|
.expect("signed event")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn references_root_matches_e_tag() {
|
||||||
|
let root = root_event_id();
|
||||||
|
let event = EventBuilder::new(Kind::GitStatusOpen, "")
|
||||||
|
.tags([Tag::event(root)])
|
||||||
|
.finalize(&keys_from_hex(
|
||||||
|
"0000000000000000000000000000000000000000000000000000000000000001",
|
||||||
|
))
|
||||||
|
.expect("signed event");
|
||||||
|
|
||||||
|
assert!(references_root(&event, &root));
|
||||||
|
assert!(!references_root(
|
||||||
|
&event,
|
||||||
|
&EventId::from_hex(OTHER_ID_HEX).expect("valid id")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn references_root_false_without_e_tags() {
|
||||||
|
let event = EventBuilder::new(Kind::GitStatusOpen, "")
|
||||||
|
.finalize(&keys_from_hex(
|
||||||
|
"0000000000000000000000000000000000000000000000000000000000000001",
|
||||||
|
))
|
||||||
|
.expect("signed event");
|
||||||
|
|
||||||
|
assert!(!references_root(&event, &root_event_id()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn defaults_to_open_without_status_events() {
|
||||||
|
let owner =
|
||||||
|
keys_from_hex("0000000000000000000000000000000000000000000000000000000000000001");
|
||||||
|
let maintainer =
|
||||||
|
keys_from_hex("0000000000000000000000000000000000000000000000000000000000000002");
|
||||||
|
|
||||||
|
let statuses: Vec<Event> = Vec::new();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
resolve_status(
|
||||||
|
statuses.iter(),
|
||||||
|
&owner.public_key(),
|
||||||
|
&[maintainer.public_key()]
|
||||||
|
),
|
||||||
|
RepoStatus::Open
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn latest_status_wins() {
|
||||||
|
let owner =
|
||||||
|
keys_from_hex("0000000000000000000000000000000000000000000000000000000000000001");
|
||||||
|
let maintainer =
|
||||||
|
keys_from_hex("0000000000000000000000000000000000000000000000000000000000000002");
|
||||||
|
let root = root_event_id();
|
||||||
|
|
||||||
|
let statuses = [
|
||||||
|
status_event(&maintainer, Kind::GitStatusClosed, root, 100),
|
||||||
|
status_event(&owner, Kind::GitStatusOpen, root, 200),
|
||||||
|
];
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
resolve_status(
|
||||||
|
statuses.iter(),
|
||||||
|
&owner.public_key(),
|
||||||
|
&[maintainer.public_key()]
|
||||||
|
),
|
||||||
|
RepoStatus::Open
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ignores_statuses_from_others() {
|
||||||
|
let owner =
|
||||||
|
keys_from_hex("0000000000000000000000000000000000000000000000000000000000000001");
|
||||||
|
let maintainer =
|
||||||
|
keys_from_hex("0000000000000000000000000000000000000000000000000000000000000002");
|
||||||
|
let stranger =
|
||||||
|
keys_from_hex("0000000000000000000000000000000000000000000000000000000000000003");
|
||||||
|
let root = root_event_id();
|
||||||
|
|
||||||
|
let statuses = [
|
||||||
|
status_event(&stranger, Kind::GitStatusClosed, root, 300),
|
||||||
|
status_event(&maintainer, Kind::GitStatusDraft, root, 100),
|
||||||
|
];
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
resolve_status(
|
||||||
|
statuses.iter(),
|
||||||
|
&owner.public_key(),
|
||||||
|
&[maintainer.public_key()]
|
||||||
|
),
|
||||||
|
RepoStatus::Draft
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ignores_non_status_kinds() {
|
||||||
|
let owner =
|
||||||
|
keys_from_hex("0000000000000000000000000000000000000000000000000000000000000001");
|
||||||
|
let root = root_event_id();
|
||||||
|
|
||||||
|
let statuses = [status_event(&owner, Kind::GitIssue, root, 100)];
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
resolve_status(statuses.iter(), &owner.public_key(), &[]),
|
||||||
|
RepoStatus::Open
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use gpui::{AppContext, Context, Subscription, Task};
|
use gpui::{AppContext, Context, Subscription, Task};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use signed_core::{Announcement, RepoAddr, RepoStatus, filters};
|
use signed_core::{Announcement, RepoAddr, RepoStatus, filters, parse_state};
|
||||||
|
|
||||||
use crate::backend::{Backend, BackendEvent};
|
use crate::backend::{Backend, BackendEvent};
|
||||||
|
|
||||||
|
/// Delay between a refresh request and the actual re-query, so bursts of
|
||||||
|
/// events (e.g. per-event `NostrUpdate`s) collapse into one query.
|
||||||
|
const REFRESH_DEBOUNCE: Duration = Duration::from_millis(300);
|
||||||
|
|
||||||
/// Per-repository store: announcement, state, issues, patches, PRs and
|
/// Per-repository store: announcement, state, issues, patches, PRs and
|
||||||
/// their resolved statuses. Always derived from the local database.
|
/// their resolved statuses. Always derived from the local database.
|
||||||
pub struct RepoStore {
|
pub struct RepoStore {
|
||||||
@@ -22,6 +28,8 @@ pub struct RepoStore {
|
|||||||
pub last_error: Option<String>,
|
pub last_error: Option<String>,
|
||||||
refreshing: bool,
|
refreshing: bool,
|
||||||
refresh_dirty: bool,
|
refresh_dirty: bool,
|
||||||
|
/// A refresh is waiting out [`REFRESH_DEBOUNCE`].
|
||||||
|
debouncing: bool,
|
||||||
tasks: Vec<Task<Result<(), Error>>>,
|
tasks: Vec<Task<Result<(), Error>>>,
|
||||||
_subscription: Subscription,
|
_subscription: Subscription,
|
||||||
}
|
}
|
||||||
@@ -66,6 +74,7 @@ impl RepoStore {
|
|||||||
last_error: None,
|
last_error: None,
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
refresh_dirty: false,
|
refresh_dirty: false,
|
||||||
|
debouncing: false,
|
||||||
_subscription: subscription,
|
_subscription: subscription,
|
||||||
tasks: Vec::new(),
|
tasks: Vec::new(),
|
||||||
};
|
};
|
||||||
@@ -98,14 +107,34 @@ impl RepoStore {
|
|||||||
|
|
||||||
/// Re-query the local database and update all fields.
|
/// Re-query the local database and update all fields.
|
||||||
///
|
///
|
||||||
/// Debounced: concurrent requests are coalesced into a single re-query
|
/// Debounced: a short delay collapses bursts of requests (e.g. per-event
|
||||||
/// after the running one finishes. The query and processing run on a
|
/// `NostrUpdate`s), and requests that arrive while a query is running are
|
||||||
|
/// folded into one follow-up query. The query and processing run on a
|
||||||
/// background thread; only the results are applied on the main thread.
|
/// background thread; only the results are applied on the main thread.
|
||||||
pub fn refresh(&mut self, cx: &mut Context<Self>) {
|
pub fn refresh(&mut self, cx: &mut Context<Self>) {
|
||||||
if self.refreshing {
|
if self.refreshing {
|
||||||
self.refresh_dirty = true;
|
self.refresh_dirty = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if self.debouncing {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.debouncing = true;
|
||||||
|
|
||||||
|
let task = cx.spawn(async move |this, cx| {
|
||||||
|
cx.background_executor().timer(REFRESH_DEBOUNCE).await;
|
||||||
|
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.debouncing = false;
|
||||||
|
this.run_refresh(cx);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
self.tasks.push(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One query + apply cycle (debounced entry point).
|
||||||
|
fn run_refresh(&mut self, cx: &mut Context<Self>) {
|
||||||
self.refreshing = true;
|
self.refreshing = true;
|
||||||
|
|
||||||
let client = Backend::global(cx).read(cx).client();
|
let client = Backend::global(cx).read(cx).client();
|
||||||
@@ -293,24 +322,3 @@ fn latest(events: Events) -> Option<Event> {
|
|||||||
fn sort_newest_first(events: &mut [Event]) {
|
fn sort_newest_first(events: &mut [Event]) {
|
||||||
events.sort_by_key(|e| std::cmp::Reverse(e.created_at));
|
events.sort_by_key(|e| std::cmp::Reverse(e.created_at));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a kind `30618` state event into refs and HEAD.
|
|
||||||
fn parse_state(event: &Event) -> (Vec<(String, String)>, Option<String>) {
|
|
||||||
let mut refs = Vec::new();
|
|
||||||
let mut head = None;
|
|
||||||
|
|
||||||
for tag in event.tags.iter() {
|
|
||||||
match Nip34Tag::parse(tag.as_slice()) {
|
|
||||||
Ok(Nip34Tag::Head(branch)) => head = Some(branch),
|
|
||||||
Ok(Nip34Tag::RefHead { branch, commit }) => {
|
|
||||||
refs.push((format!("refs/heads/{branch}"), commit.to_string()));
|
|
||||||
}
|
|
||||||
Ok(Nip34Tag::RefTag { name, commit }) => {
|
|
||||||
refs.push((format!("refs/tags/{name}"), commit.to_string()));
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(refs, head)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use gpui::{AppContext, Context, Subscription, Task};
|
use gpui::{AppContext, Context, Subscription, Task};
|
||||||
@@ -7,12 +9,19 @@ use signed_core::{Announcement, RepoAddr, filters};
|
|||||||
|
|
||||||
use crate::backend::{Backend, BackendEvent};
|
use crate::backend::{Backend, BackendEvent};
|
||||||
|
|
||||||
|
/// Delay between a refresh request and the actual re-query, so bursts of
|
||||||
|
/// events (e.g. sync progress ticks) collapse into one query.
|
||||||
|
const REFRESH_DEBOUNCE: Duration = Duration::from_millis(300);
|
||||||
|
|
||||||
/// Store listing repository announcements (global discovery or per-author).
|
/// Store listing repository announcements (global discovery or per-author).
|
||||||
pub struct RepoListStore {
|
pub struct RepoListStore {
|
||||||
pub announcements: Vec<Announcement>,
|
/// Shared so views can clone the list per frame without a deep copy.
|
||||||
|
pub announcements: Arc<Vec<Announcement>>,
|
||||||
author: Option<PublicKey>,
|
author: Option<PublicKey>,
|
||||||
refreshing: bool,
|
refreshing: bool,
|
||||||
refresh_dirty: bool,
|
refresh_dirty: bool,
|
||||||
|
/// A refresh is waiting out [`REFRESH_DEBOUNCE`].
|
||||||
|
debouncing: bool,
|
||||||
tasks: Vec<Task<Result<(), Error>>>,
|
tasks: Vec<Task<Result<(), Error>>>,
|
||||||
_subscription: Subscription,
|
_subscription: Subscription,
|
||||||
}
|
}
|
||||||
@@ -42,10 +51,11 @@ impl RepoListStore {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let mut store = Self {
|
let mut store = Self {
|
||||||
announcements: Vec::new(),
|
announcements: Arc::new(Vec::new()),
|
||||||
author,
|
author,
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
refresh_dirty: false,
|
refresh_dirty: false,
|
||||||
|
debouncing: false,
|
||||||
_subscription: subscription,
|
_subscription: subscription,
|
||||||
tasks: Vec::new(),
|
tasks: Vec::new(),
|
||||||
};
|
};
|
||||||
@@ -78,14 +88,34 @@ impl RepoListStore {
|
|||||||
|
|
||||||
/// Re-query the local database. Latest announcement per repository wins.
|
/// Re-query the local database. Latest announcement per repository wins.
|
||||||
///
|
///
|
||||||
/// Debounced: concurrent requests are coalesced into a single re-query
|
/// Debounced: a short delay collapses bursts of requests (e.g. sync
|
||||||
/// after the running one finishes. The query and processing run on a
|
/// progress ticks), and requests that arrive while a query is running
|
||||||
/// background thread; only the results are applied on the main thread.
|
/// are folded into one follow-up query. The query and processing run on
|
||||||
|
/// a background thread; only the results are applied on the main thread.
|
||||||
pub fn refresh(&mut self, cx: &mut Context<Self>) {
|
pub fn refresh(&mut self, cx: &mut Context<Self>) {
|
||||||
if self.refreshing {
|
if self.refreshing {
|
||||||
self.refresh_dirty = true;
|
self.refresh_dirty = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if self.debouncing {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.debouncing = true;
|
||||||
|
|
||||||
|
let task = cx.spawn(async move |this, cx| {
|
||||||
|
cx.background_executor().timer(REFRESH_DEBOUNCE).await;
|
||||||
|
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.debouncing = false;
|
||||||
|
this.run_refresh(cx);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
self.tasks.push(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One query + apply cycle (debounced entry point).
|
||||||
|
fn run_refresh(&mut self, cx: &mut Context<Self>) {
|
||||||
self.refreshing = true;
|
self.refreshing = true;
|
||||||
|
|
||||||
let client = Backend::global(cx).read(cx).client();
|
let client = Backend::global(cx).read(cx).client();
|
||||||
@@ -136,7 +166,7 @@ impl RepoListStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let again = this.update(cx, |this, cx| {
|
let again = this.update(cx, |this, cx| {
|
||||||
this.announcements = announcements;
|
this.announcements = Arc::new(announcements);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
||||||
this.refreshing = false;
|
this.refreshing = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user