This commit is contained in:
2026-08-06 16:00:00 +07:00
parent 0c6d700395
commit 640549a2c5
16 changed files with 426 additions and 438 deletions
+63 -40
View File
@@ -6,8 +6,8 @@ use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Task};
use nostr_connect::prelude::*;
use nostr_sdk::client::SyncSummary;
use nostr_sdk::prelude::*;
use signed_core::{builders, filters};
use signed_nostr::{NostrBackend, SignedAuthUrlHandler, UniversalSigner, Update};
use signed_core::filters;
use signed_nostr::{SignedAuthUrlHandler, UniversalSigner, Update};
/// Keyring entry holding the user credential (`nsec1...` or `bunker://...`
/// with an embedded `?master=<nsec>` NIP-46 session key).
@@ -71,7 +71,8 @@ impl BackendEvent {
/// notification pump. Stores subscribe to [`BackendEvent`] and re-query the
/// local database when relevant updates arrive.
pub struct Backend {
inner: NostrBackend,
client: Client,
signer: UniversalSigner,
current_user: Option<PublicKey>,
connected: bool,
sync_progress: Option<(u64, u64)>,
@@ -94,11 +95,11 @@ impl Backend {
cx.set_global(GlobalBackend(entity));
}
pub(crate) fn new(inner: NostrBackend, cx: &mut Context<Self>) -> Self {
let client = inner.client();
pub(crate) fn new(client: Client, signer: UniversalSigner, cx: &mut Context<Self>) -> Self {
let pump_client = client.clone();
let pump = cx.spawn(async move |this, cx| {
let mut notifications = client.notifications();
let mut notifications = pump_client.notifications();
while let Some(notification) = notifications.next().await {
let ClientNotification::Event { event, .. } = notification else {
@@ -119,7 +120,8 @@ impl Backend {
});
let mut this = Self {
inner,
client,
signer,
current_user: None,
connected: false,
sync_progress: None,
@@ -133,16 +135,19 @@ impl Backend {
/// Bootstrap the client: connect to the default relays (indexers as
/// discovery-only) and restore the saved session, if any.
fn bootstrap(&mut self, cx: &mut Context<Self>) {
let backend = self.inner.clone();
let client = self.client.clone();
let task = cx.background_spawn(async move {
for url in BOOTSTRAP_RELAYS {
backend.add_relay(url).await?;
client.add_relay(url).await?;
}
for url in INDEXER_RELAYS {
backend.add_discovery_relay(url).await?;
client
.add_relay(url)
.capabilities(RelayCapabilities::DISCOVERY)
.await?;
}
backend.connect().await;
client.connect().await;
Ok::<(), Error>(())
});
@@ -276,7 +281,7 @@ impl Backend {
this.update(cx, |this, cx| {
// Become the new identity, so the publishes below are
// signed with the new keys.
this.inner.signer().swap_inner(keys);
this.signer.swap_inner(keys);
this.current_user = Some(public_key);
this.bootstrap_user(public_key, cx);
cx.emit(BackendEvent::SignerChanged);
@@ -317,7 +322,7 @@ impl Backend {
.map(|url| RelayUrl::parse(url).expect("valid relay URL"))
.collect();
this.send(builders::grasp_list(grasp_servers), cx);
this.send(GitUserGraspList { grasp_servers }.into_event_builder(), cx);
})?;
Ok(public_key)
@@ -441,7 +446,7 @@ impl Backend {
delete.await.ok();
this.update(cx, |this, cx| {
this.inner.signer().swap_inner(Keys::generate());
this.signer.swap_inner(Keys::generate());
this.current_user = None;
cx.emit(BackendEvent::SignerChanged);
cx.emit(BackendEvent::SignerRequired);
@@ -455,14 +460,11 @@ impl Backend {
/// Fetch the user's grasp list (kind `10317`) and add the listed grasp
/// servers as relays.
fn bootstrap_user(&mut self, public_key: PublicKey, cx: &mut Context<Self>) {
let backend = self.inner.clone();
let client = self.client.clone();
self.tasks.push(cx.spawn(async move |this, cx| {
let result = async {
let events = backend
.client()
.fetch_events(filters::grasp_list(public_key))
.await?;
let events = client.fetch_events(filters::grasp_list(public_key)).await?;
let urls: Vec<String> = events
.into_iter()
@@ -477,9 +479,9 @@ impl Backend {
.unwrap_or_default();
for url in urls {
backend.add_relay(&url).await.ok();
client.add_relay(&url).await.ok();
}
backend.connect().await;
client.connect().await;
Ok::<_, Error>(())
}
@@ -495,12 +497,12 @@ impl Backend {
/// Get the nostr client.
pub fn client(&self) -> Client {
self.inner.client()
self.client.clone()
}
/// Get the current signer.
pub fn signer(&self) -> UniversalSigner {
self.inner.signer()
self.signer.clone()
}
/// Get the current user's public key.
@@ -536,7 +538,7 @@ impl Backend {
match new_signer.get_public_key_async().await {
Ok(public_key) => {
this.update(cx, |this, cx| {
this.inner.signer().swap_inner(new_signer);
this.signer.swap_inner(new_signer);
this.current_user = Some(public_key);
this.bootstrap_user(public_key, cx);
cx.emit(BackendEvent::SignerChanged);
@@ -557,13 +559,13 @@ impl Backend {
/// Add relays and connect to them.
pub fn add_relays(&mut self, urls: Vec<String>, cx: &mut Context<Self>) {
let backend = self.inner.clone();
let client = self.client.clone();
let task = cx.background_spawn(async move {
for url in urls {
backend.add_relay(&url).await?;
client.add_relay(&url).await?;
}
backend.connect().await;
client.connect().await;
Ok::<(), Error>(())
});
@@ -587,13 +589,16 @@ impl Backend {
/// Add relays used only for discovery (e.g. NIP-65 indexers) and
/// connect to them. No subscriptions or writes are routed through them.
pub fn add_discovery_relays(&mut self, urls: Vec<String>, cx: &mut Context<Self>) {
let backend = self.inner.clone();
let client = self.client.clone();
let task = cx.background_spawn(async move {
for url in urls {
backend.add_discovery_relay(&url).await?;
client
.add_relay(&url)
.capabilities(RelayCapabilities::DISCOVERY)
.await?;
}
backend.connect().await;
client.connect().await;
Ok::<(), Error>(())
});
@@ -608,9 +613,9 @@ impl Backend {
/// Start a persistent subscription. Matching events are stored in the
/// database automatically and surface as [`BackendEvent::NostrUpdate`].
pub fn subscribe(&mut self, filter: Filter, cx: &mut Context<Self>) {
let backend = self.inner.clone();
let client = self.client.clone();
let task = cx.background_spawn(async move { backend.subscribe(filter).await.map(|_| ()) });
let task = cx.background_spawn(async move { client.subscribe(filter).await.map(|_| ()) });
self.tasks.push(cx.spawn(async move |this, cx| {
if let Err(e) = task.await {
@@ -625,11 +630,10 @@ impl Backend {
/// in the database and surface as [`BackendEvent::NostrUpdate`] while the
/// subscription is open.
pub fn subscribe_bootstrap(&mut self, filters: Vec<Filter>, cx: &mut Context<Self>) {
let backend = self.inner.clone();
let client = self.client.clone();
let task = cx.background_spawn(async move {
subscribe_bootstrap_only(&backend.client(), filters).await
});
let task =
cx.background_spawn(async move { subscribe_bootstrap_only(&client, filters).await });
self.tasks.push(cx.spawn(async move |this, cx| {
if let Err(e) = task.await {
@@ -644,7 +648,7 @@ impl Backend {
/// Emits [`BackendEvent::SyncProgress`] while running (throttled to
/// whole-percent changes) and [`BackendEvent::Synced`] on completion.
pub fn sync_bootstrap(&mut self, filter: Filter, cx: &mut Context<Self>) {
let backend = self.inner.clone();
let client = self.client.clone();
self.sync_progress = Some((0, 0));
cx.notify();
@@ -681,7 +685,7 @@ impl Backend {
let task = cx.background_spawn(async move {
let opts = SyncOptions::default().progress(tx);
sync_bootstrap_only(&backend.client(), filter, opts).await
sync_bootstrap_only(&client, filter, opts).await
});
self.tasks.push(cx.spawn(async move |this, cx| {
@@ -721,8 +725,27 @@ impl Backend {
cx: &mut Context<Self>,
) -> flume::Receiver<Result<Event, Error>> {
let (tx, rx) = flume::bounded(1);
let backend = self.inner.clone();
let task = cx.background_spawn(async move { backend.send(builder).await });
let client = self.client.clone();
let signer = self.signer.clone();
let task = cx.background_spawn(async move {
// Sign with the current signer, broadcast, and save locally so
// the event is immediately visible to database queries.
let event = builder.finalize_async(&signer).await?;
let output = client.send_event(&event).await?;
if output.success.is_empty() && !output.failed.is_empty() {
let reasons = output
.failed
.values()
.cloned()
.collect::<Vec<String>>()
.join(", ");
return Err(anyhow!("event not accepted by any relay: {reasons}"));
}
Ok(event)
});
self.tasks.push(cx.spawn(async move |this, cx| {
let result = task.await;
+6 -6
View File
@@ -10,7 +10,7 @@ use gpui::{App, AppContext, Entity};
pub use profile::{Profile, ProfileStore, shorten_pubkey};
pub use repo::RepoStore;
pub use repo_list::RepoListStore;
use signed_nostr::NostrBackend;
use signed_nostr::new_backend;
/// Initialize the backend and stores, and install them as globals. Call once
/// at startup, before opening any window that uses the stores.
@@ -22,13 +22,13 @@ pub fn init(db_path: impl AsRef<Path>, cx: &mut App) -> Entity<Backend> {
.ok();
let path = db_path.as_ref().to_path_buf();
let inner = cx.foreground_executor().block_on(async move {
NostrBackend::new(path)
let (client, signer) = cx.foreground_executor().block_on(async move {
new_backend(path)
.await
.expect("failed to initialize nostr backend")
});
let entity = cx.new(|cx| Backend::new(inner, cx));
let entity = cx.new(|cx| Backend::new(client, signer, cx));
Backend::set_global(entity.clone(), cx);
ProfileStore::set_global(cx.new(ProfileStore::new), cx);
@@ -39,9 +39,9 @@ pub fn init(db_path: impl AsRef<Path>, cx: &mut App) -> Entity<Backend> {
/// Initialize the backend with an in-memory database on wasm.
#[cfg(target_arch = "wasm32")]
pub fn init(cx: &mut App) -> Entity<Backend> {
let inner = NostrBackend::new().expect("failed to initialize nostr backend");
let (client, signer) = new_backend().expect("failed to initialize nostr backend");
let entity = cx.new(|cx| Backend::new(inner, cx));
let entity = cx.new(|cx| Backend::new(client, signer, cx));
Backend::set_global(entity.clone(), cx);
ProfileStore::set_global(cx.new(ProfileStore::new), cx);
+58 -28
View File
@@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet};
use std::time::Duration;
use anyhow::Error;
use gpui::{App, Context, Entity, Global, SharedString, Subscription, Task};
use gpui::{App, AppContext, Context, Entity, Global, SharedString, Subscription, Task};
use nostr_sdk::prelude::*;
use crate::backend::{Backend, BackendEvent, sync_bootstrap_only};
@@ -138,47 +138,68 @@ impl ProfileStore {
fn load(&mut self, cx: &mut Context<Self>) {
let client = Backend::global(cx).read(cx).client();
let task = cx.spawn(async move |this, cx| {
let work = cx.background_spawn(async move {
let filter = Filter::new().kind(Kind::Metadata).limit(200);
let events = client.database().query(filter).await?;
this.update(cx, |this, cx| {
for event in events {
// Parse off the main thread; only plain profiles cross back.
let profiles: Vec<Profile> = events
.into_iter()
.map(|event| {
let metadata = Metadata::from_json(&event.content).unwrap_or_default();
this.profiles
.insert(event.pubkey, Profile::new(event.pubkey, metadata));
Profile::new(event.pubkey, metadata)
})
.collect();
Ok::<_, Error>(profiles)
});
self.tasks.push(cx.spawn(async move |this, cx| {
let profiles = work.await?;
this.update(cx, |this, cx| {
for profile in profiles {
this.profiles.insert(profile.public_key(), profile);
}
cx.notify();
})?;
Ok(())
});
self.tasks.push(task);
}));
}
/// Re-read the latest metadata of an author from the local database.
fn apply_author(&mut self, public_key: PublicKey, cx: &mut Context<Self>) {
let client = Backend::global(cx).read(cx).client();
let task = cx.spawn(async move |this, cx| {
let work = cx.background_spawn(async move {
let filter = Filter::new().kind(Kind::Metadata).author(public_key);
let events = client.database().query(filter).await?;
if let Some(event) = events.into_iter().max_by_key(|e| e.created_at) {
let metadata = Metadata::from_json(event.content).unwrap_or_default();
// Parse off the main thread; only the profile crosses back.
let profile = events
.into_iter()
.max_by_key(|e| e.created_at)
.map(|event| {
let metadata = Metadata::from_json(event.content).unwrap_or_default();
Profile::new(event.pubkey, metadata)
});
this.update(cx, |this, cx| {
this.profiles
.insert(public_key, Profile::new(public_key, metadata));
cx.notify();
})?;
}
Ok(())
Ok::<_, Error>(profile)
});
self.tasks.push(task);
self.tasks.push(cx.spawn(async move |this, cx| {
let profile = work.await?;
this.update(cx, |this, cx| {
if let Some(profile) = profile {
this.profiles.insert(profile.public_key(), profile);
cx.notify();
}
})?;
Ok(())
}));
}
/// Re-read the latest metadata of every requested author from the local
@@ -191,10 +212,11 @@ impl ProfileStore {
let client = Backend::global(cx).read(cx).client();
let authors: Vec<PublicKey> = self.seen.iter().copied().collect();
let task = cx.spawn(async move |this, cx| {
let work = cx.background_spawn(async move {
let filter = Filter::new().kind(Kind::Metadata).authors(authors);
let events = client.database().query(filter).await?;
// Pick the latest metadata per author off the main thread.
let mut latest: HashMap<PublicKey, (Timestamp, Metadata)> = HashMap::new();
for event in events {
match latest.get(&event.pubkey) {
@@ -211,18 +233,26 @@ impl ProfileStore {
}
}
let profiles: Vec<Profile> = latest
.into_iter()
.map(|(public_key, (_, metadata))| Profile::new(public_key, metadata))
.collect();
Ok::<_, Error>(profiles)
});
self.tasks.push(cx.spawn(async move |this, cx| {
let profiles = work.await?;
this.update(cx, |this, cx| {
for (public_key, (_, metadata)) in latest {
this.profiles
.insert(public_key, Profile::new(public_key, metadata));
for profile in profiles {
this.profiles.insert(profile.public_key(), profile);
}
cx.notify();
})?;
Ok(())
});
self.tasks.push(task);
}));
}
/// Drain the queue in a batched fetch, debounced to collect requests.
+102 -91
View File
@@ -1,5 +1,5 @@
use anyhow::Error;
use gpui::{Context, Subscription, Task};
use gpui::{AppContext, Context, Subscription, Task};
use nostr_sdk::prelude::*;
use signed_core::{Announcement, RepoAddr, RepoStatus, filters};
@@ -33,20 +33,16 @@ impl RepoStore {
let subscription = cx.subscribe(&backend, |this, _backend, event, cx| {
let relevant = match event {
BackendEvent::NostrUpdate(update) => {
let coordinate = update.coordinate.as_ref() == Some(&this.addr.coordinate());
let author = update.author == this.addr.owner;
let coordinate = update.coordinate.as_ref() == Some(&this.addr);
let author = update.author == this.addr.public_key;
let kind = update.kind == Kind::GitRepoAnnouncement;
coordinate || (author && kind)
}
BackendEvent::Published(event) => {
let kind = event.kind == Kind::GitRepoAnnouncement;
let author = event.pubkey == this.addr.owner;
let coordinate = event
.tags
.coordinates()
.into_iter()
.any(|c| c == this.addr.coordinate());
let author = event.pubkey == this.addr.public_key;
let coordinate = event.tags.coordinates().into_iter().any(|c| c == this.addr);
coordinate || (kind && author)
}
@@ -103,7 +99,8 @@ impl RepoStore {
/// Re-query the local database and update all fields.
///
/// Debounced: concurrent requests are coalesced into a single re-query
/// after the running one finishes.
/// after the running one finishes. 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>) {
if self.refreshing {
self.refresh_dirty = true;
@@ -114,84 +111,99 @@ impl RepoStore {
let client = Backend::global(cx).read(cx).client();
let addr = self.addr.clone();
let task = cx.spawn(async move |this, cx| {
loop {
let queries = async {
let db = client.database();
let work = cx.background_spawn(async move {
let queries = async {
let db = client.database();
let announcements = db.query(filters::announcement(&addr)).await?;
let states = db.query(filters::state(&addr)).await?;
let activity = db.query(filters::activity(&addr)).await?;
let announcements = db.query(filters::announcement(&addr)).await?;
let states = db.query(filters::state(&addr)).await?;
let activity = db.query(filters::activity(&addr)).await?;
Ok::<_, Error>((announcements, states, activity))
}
.await;
Ok::<_, Error>((announcements, states, activity))
}
.await?;
let (announcements, states, activity) = match queries {
Ok(results) => results,
Err(e) => {
return this.update(cx, |this, cx| {
this.refreshing = false;
this.last_error = Some(e.to_string());
cx.notify();
});
}
};
let (announcements, states, activity) = queries;
let again = this.update(cx, |this, cx| {
this.announcement = latest(announcements)
.as_ref()
.and_then(Announcement::from_event);
// Parse and sort off the main thread; only plain data
// crosses back into the entity.
let announcement = latest(announcements)
.as_ref()
.and_then(Announcement::from_event);
if let Some(state) = latest(states) {
let (refs, head) = parse_state(&state);
this.refs = refs;
this.head = head;
}
let state = latest(states).map(|state| parse_state(&state));
this.issues.clear();
this.patches.clear();
this.pull_requests.clear();
this.statuses.clear();
let (mut issues, mut patches, mut pull_requests, mut statuses) =
(Vec::new(), Vec::new(), Vec::new(), Vec::new());
for event in activity {
match event.kind {
Kind::GitIssue => this.issues.push(event),
Kind::GitPatch => this.patches.push(event),
Kind::GitPullRequest | Kind::GitPullRequestUpdate => {
this.pull_requests.push(event)
}
kind if RepoStatus::from_kind(kind).is_some() => {
this.statuses.push(event)
}
_ => {}
}
}
sort_newest_first(&mut this.issues);
sort_newest_first(&mut this.patches);
sort_newest_first(&mut this.pull_requests);
cx.notify();
if this.refresh_dirty {
this.refresh_dirty = false;
true
} else {
this.refreshing = false;
false
}
})?;
if !again {
break;
for event in activity {
match event.kind {
Kind::GitIssue => issues.push(event),
Kind::GitPatch => patches.push(event),
Kind::GitPullRequest | Kind::GitPullRequestUpdate => pull_requests.push(event),
kind if RepoStatus::from_kind(kind).is_some() => statuses.push(event),
_ => {}
}
}
Ok(())
sort_newest_first(&mut issues);
sort_newest_first(&mut patches);
sort_newest_first(&mut pull_requests);
Ok::<_, Error>((
announcement,
state,
issues,
patches,
pull_requests,
statuses,
))
});
self.tasks.push(task);
self.tasks.push(cx.spawn(async move |this, cx| {
let (announcement, state, issues, patches, pull_requests, statuses) = match work.await {
Ok(data) => data,
Err(e) => {
return this.update(cx, |this, cx| {
this.refreshing = false;
this.last_error = Some(e.to_string());
cx.notify();
});
}
};
let again = this.update(cx, |this, cx| {
this.announcement = announcement;
if let Some((refs, head)) = state {
this.refs = refs;
this.head = head;
}
this.issues = issues;
this.patches = patches;
this.pull_requests = pull_requests;
this.statuses = statuses;
cx.notify();
this.refreshing = false;
if this.refresh_dirty {
this.refresh_dirty = false;
true
} else {
false
}
})?;
// Requests that arrived while the refresh was running are
// coalesced into one follow-up refresh.
if again {
this.update(cx, |this, cx| this.refresh(cx))?;
}
Ok(())
}));
}
/// Resolve the status of a root event (issue / patch / PR) per NIP-34.
@@ -213,7 +225,7 @@ impl RepoStore {
/// Open an issue on this repository.
pub fn open_issue(&mut self, subject: Option<String>, content: String, cx: &mut Context<Self>) {
let builder = GitIssue {
repository: self.addr.coordinate(),
repository: self.addr.clone(),
content,
subject,
labels: Vec::new(),
@@ -230,8 +242,8 @@ impl RepoStore {
};
let builder = EventBuilder::new(Kind::GitPatch, patch).tags([
Tag::coordinate(self.addr.coordinate(), None),
Tag::public_key(self.addr.owner),
Tag::coordinate(self.addr.clone(), None),
Tag::public_key(self.addr.public_key),
root_marker,
]);
@@ -246,9 +258,9 @@ impl RepoStore {
let builder = EventBuilder::new(status.kind(), "").tags([
root_ref,
Tag::public_key(self.addr.owner),
Tag::public_key(self.addr.public_key),
Tag::public_key(root.pubkey),
Tag::coordinate(self.addr.coordinate(), None),
Tag::coordinate(self.addr.clone(), None),
]);
self.send(builder, cx);
@@ -288,16 +300,15 @@ fn parse_state(event: &Event) -> (Vec<(String, String)>, Option<String>) {
let mut head = None;
for tag in event.tags.iter() {
let kind = tag.kind();
if kind == "HEAD" {
head = tag
.content()
.and_then(|v| v.strip_prefix("ref: refs/heads/"))
.map(str::to_owned);
} else if kind.starts_with("refs/")
&& let Some(commit) = tag.content()
{
refs.push((kind.to_owned(), commit.to_owned()));
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()));
}
_ => {}
}
}
+61 -53
View File
@@ -1,9 +1,9 @@
use std::collections::HashMap;
use anyhow::Error;
use gpui::{Context, Subscription, Task};
use gpui::{AppContext, Context, Subscription, Task};
use nostr_sdk::prelude::*;
use signed_core::{Announcement, filters};
use signed_core::{Announcement, RepoAddr, filters};
use crate::backend::{Backend, BackendEvent};
@@ -79,7 +79,8 @@ impl RepoListStore {
/// Re-query the local database. Latest announcement per repository wins.
///
/// Debounced: concurrent requests are coalesced into a single re-query
/// after the running one finishes.
/// after the running one finishes. 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>) {
if self.refreshing {
self.refresh_dirty = true;
@@ -90,63 +91,70 @@ impl RepoListStore {
let client = Backend::global(cx).read(cx).client();
let author = self.author;
let task = cx.spawn(async move |this, cx| {
loop {
let filter = match author {
Some(a) => filters::announcements_by(a),
None => filters::all_announcements(),
let work = cx.background_spawn(async move {
let filter = match author {
Some(a) => filters::announcements_by(a),
None => filters::all_announcements(),
};
let events = client.database().query(filter).await?;
// Dedup and sort off the main thread; only the final list
// crosses back into the entity.
let mut by_repo: HashMap<RepoAddr, Announcement> = HashMap::new();
for event in events {
let Some(announcement) = Announcement::from_event(&event) else {
continue;
};
let events = match client.database().query(filter).await {
Ok(events) => events,
Err(_) => {
return this.update(cx, |this, _cx| {
this.refreshing = false;
});
let addr = announcement.addr();
match by_repo.get(&addr) {
Some(existing) if existing.created_at >= announcement.created_at => {}
_ => {
by_repo.insert(addr, announcement);
}
};
let again = this.update(cx, |this, cx| {
let mut by_repo: HashMap<(String, String), Announcement> = HashMap::new();
for event in events {
let Some(announcement) = Announcement::from_event(&event) else {
continue;
};
let key = (announcement.owner.to_hex(), announcement.id.clone());
match by_repo.get(&key) {
Some(existing) if existing.created_at >= announcement.created_at => {}
_ => {
by_repo.insert(key, announcement);
}
}
}
let mut announcements: Vec<Announcement> = by_repo.into_values().collect();
announcements.sort_by_key(|a| std::cmp::Reverse(a.created_at));
this.announcements = announcements;
cx.notify();
if this.refresh_dirty {
this.refresh_dirty = false;
true
} else {
this.refreshing = false;
false
}
})?;
if !again {
break;
}
}
Ok(())
let mut announcements: Vec<Announcement> = by_repo.into_values().collect();
announcements.sort_by_key(|a| std::cmp::Reverse(a.created_at));
Ok::<_, Error>(announcements)
});
self.tasks.push(task);
self.tasks.push(cx.spawn(async move |this, cx| {
let announcements = match work.await {
Ok(announcements) => announcements,
// Database errors are transient; keep the last list.
Err(_) => {
return this.update(cx, |this, _cx| {
this.refreshing = false;
});
}
};
let again = this.update(cx, |this, cx| {
this.announcements = announcements;
cx.notify();
this.refreshing = false;
if this.refresh_dirty {
this.refresh_dirty = false;
true
} else {
false
}
})?;
// Requests that arrived while the refresh was running are
// coalesced into one follow-up refresh.
if again {
this.update(cx, |this, cx| this.refresh(cx))?;
}
Ok(())
}));
}
}