update
This commit is contained in:
@@ -1,20 +1,11 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Error;
|
||||
use gpui::{AppContext, Context, Task};
|
||||
use nostr_sdk::prelude::*;
|
||||
use signed_core::{Deletions, InboxItem, InboxReadState, filters, inbox};
|
||||
|
||||
use crate::backend::{Backend, BackendEvent};
|
||||
use crate::refresh::{RefreshGate, RefreshRequest};
|
||||
|
||||
/// Delay between a refresh request and the actual re-query.
|
||||
const REFRESH_DEBOUNCE: Duration = Duration::from_millis(300);
|
||||
|
||||
/// Maximum number of "continue where you left off" activity events kept.
|
||||
const ACTIVITY_LIMIT: usize = 50;
|
||||
use crate::backend::Backend;
|
||||
|
||||
/// The user's persisted inbox read state.
|
||||
#[derive(Default)]
|
||||
@@ -22,7 +13,7 @@ pub struct Inbox {
|
||||
state: InboxReadState,
|
||||
/// Set once the stored state has been read for the current user.
|
||||
state_loaded: bool,
|
||||
/// Unread notification groups, published by [`InboxStore`] for the sidebar badge.
|
||||
/// Unread notification groups, published by the inbox panel for the sidebar badge.
|
||||
pub unread_count: usize,
|
||||
}
|
||||
|
||||
@@ -37,7 +28,7 @@ impl Inbox {
|
||||
self.state_loaded
|
||||
}
|
||||
|
||||
/// Publish the unread count derived by [`InboxStore`].
|
||||
/// Publish the unread count derived by the inbox panel.
|
||||
pub fn set_unread_count(&mut self, count: usize, cx: &mut Context<Self>) {
|
||||
if self.unread_count == count {
|
||||
return;
|
||||
@@ -150,288 +141,43 @@ impl Inbox {
|
||||
}
|
||||
}
|
||||
|
||||
/// Derives the inbox home screen's notification and activity lists.
|
||||
/// Derive the inbox home screen's lists for `me` from the local database.
|
||||
///
|
||||
/// Created by the inbox panel, so the database work only happens while the
|
||||
/// panel is open. The persisted read state stays in the global [`Inbox`].
|
||||
#[derive(Default)]
|
||||
pub struct InboxStore {
|
||||
/// Notifications grouped by thread root, newest activity first.
|
||||
pub notifications: Arc<Vec<InboxItem>>,
|
||||
/// The user's own recent git activity, newest first.
|
||||
pub activity: Arc<Vec<Event>>,
|
||||
/// Number of non-archived groups with an unread event.
|
||||
pub unread_count: usize,
|
||||
/// Copy of the global read state the current lists were derived with.
|
||||
state: InboxReadState,
|
||||
/// Set once the global state has been read for the current user.
|
||||
state_loaded: bool,
|
||||
refresh: RefreshGate,
|
||||
}
|
||||
/// Returns the notification groups, the user's own git activity and the number
|
||||
/// of non-archived groups with an unread event.
|
||||
pub async fn query_inbox(
|
||||
client: &Client,
|
||||
me: PublicKey,
|
||||
state: &InboxReadState,
|
||||
) -> Result<(Vec<InboxItem>, Vec<Event>, usize), Error> {
|
||||
let deletion_events = client.database().query(filters::deletions()).await?;
|
||||
let deletions = Deletions::from_events(deletion_events);
|
||||
|
||||
impl InboxStore {
|
||||
/// Create the store and derive the lists from the current global state.
|
||||
pub fn new(cx: &mut Context<Self>) -> Self {
|
||||
let weak = cx.entity().downgrade();
|
||||
cx.defer(move |cx| {
|
||||
if let Err(error) = weak.update(cx, |this, cx| this.sync_state(cx)) {
|
||||
log::warn!("inbox store dropped before bootstrap could run: {error}");
|
||||
}
|
||||
});
|
||||
let (notification_events, by_id) = fetch_notifications(client, me, &deletions).await?;
|
||||
let notifications = inbox::group(notification_events, me, state, &|id| {
|
||||
by_id.get(&id).cloned()
|
||||
});
|
||||
let unread_count = notifications.iter().filter(|item| item.is_unread()).count();
|
||||
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Mark every event in the group rooted at `root` read.
|
||||
pub fn mark_read(&mut self, root: EventId, cx: &mut Context<Self>) {
|
||||
let Some(me) = Backend::global(cx).read(cx).current_user() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(group) = self.group_events(root) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let all = self.all_notification_events();
|
||||
let inbox = Backend::global(cx).read(cx).inbox();
|
||||
inbox.update(cx, |inbox, cx| inbox.mark_read(&group, &all, me, cx));
|
||||
}
|
||||
|
||||
/// Archive the group rooted at `root`.
|
||||
pub fn mark_archived(&mut self, root: EventId, cx: &mut Context<Self>) {
|
||||
let Some(me) = Backend::global(cx).read(cx).current_user() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(group) = self.group_events(root) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let all = self.all_notification_events();
|
||||
let inbox = Backend::global(cx).read(cx).inbox();
|
||||
inbox.update(cx, |inbox, cx| inbox.mark_archived(&group, &all, me, cx));
|
||||
}
|
||||
|
||||
/// Mark every known notification read.
|
||||
pub fn mark_all_read(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(me) = Backend::global(cx).read(cx).current_user() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let all = self.all_notification_events();
|
||||
let inbox = Backend::global(cx).read(cx).inbox();
|
||||
inbox.update(cx, |inbox, cx| inbox.mark_all_read(&all, me, cx));
|
||||
}
|
||||
|
||||
/// Re-derive from the global state when it is loaded or changes.
|
||||
pub fn sync_state(&mut self, cx: &mut Context<Self>) {
|
||||
let inbox = Backend::global(cx).read(cx).inbox();
|
||||
let (loaded, state) = {
|
||||
let inbox = inbox.read(cx);
|
||||
(inbox.is_loaded(), inbox.state().clone())
|
||||
};
|
||||
|
||||
if !loaded {
|
||||
let was_present =
|
||||
self.state_loaded || !self.notifications.is_empty() || !self.activity.is_empty();
|
||||
self.clear();
|
||||
if was_present {
|
||||
cx.notify();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if !self.state_loaded {
|
||||
self.state_loaded = true;
|
||||
self.state = state;
|
||||
self.refresh_initial(cx);
|
||||
return;
|
||||
}
|
||||
|
||||
if self.state != state {
|
||||
self.state = state;
|
||||
self.regroup();
|
||||
self.publish_unread_count(cx);
|
||||
cx.notify();
|
||||
let mut activity = Vec::new();
|
||||
for event in client
|
||||
.database()
|
||||
.query(filters::authored_activity(me))
|
||||
.await?
|
||||
{
|
||||
if deletions.is_deleted(&event) || !filters::is_git_activity(&event) {
|
||||
continue;
|
||||
}
|
||||
activity.push(event);
|
||||
}
|
||||
|
||||
/// Handle a backend event that can change the derived lists.
|
||||
pub fn handle_backend_event(&mut self, event: &BackendEvent, cx: &mut Context<Self>) {
|
||||
match event {
|
||||
BackendEvent::Synced | BackendEvent::Published(_) => self.refresh(cx),
|
||||
BackendEvent::NostrUpdate(updates) => {
|
||||
let relevant = updates.iter().any(|update| {
|
||||
let is_notification = filters::NOTIFICATION_KINDS.contains(&update.kind);
|
||||
let is_comment = update.kind == Kind::Comment;
|
||||
let is_event_deletion = update.kind == Kind::EventDeletion;
|
||||
let is_request_to_vanish = update.kind == Kind::RequestToVanish;
|
||||
activity.sort_by(|a, b| {
|
||||
b.created_at
|
||||
.cmp(&a.created_at)
|
||||
.then_with(|| b.id.to_hex().cmp(&a.id.to_hex()))
|
||||
});
|
||||
|
||||
is_notification || is_comment || is_event_deletion || is_request_to_vanish
|
||||
});
|
||||
if relevant {
|
||||
self.refresh(cx);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// One-shot initial load, no debounce.
|
||||
fn refresh_initial(&mut self, cx: &mut Context<Self>) {
|
||||
debug_assert!(!self.refresh.debouncing());
|
||||
if self.refresh.running() {
|
||||
self.refresh.request();
|
||||
return;
|
||||
}
|
||||
|
||||
self.run_refresh(cx);
|
||||
}
|
||||
|
||||
/// Re-query the local database.
|
||||
pub fn refresh(&mut self, cx: &mut Context<Self>) {
|
||||
if !self.state_loaded {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.refresh.request() != RefreshRequest::Schedule {
|
||||
return;
|
||||
}
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
cx.background_executor().timer(REFRESH_DEBOUNCE).await;
|
||||
this.update(cx, |this, cx| this.run_refresh(cx))
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// One query and apply cycle, the debounced entry point.
|
||||
fn run_refresh(&mut self, cx: &mut Context<Self>) {
|
||||
self.refresh.begin();
|
||||
|
||||
let backend = Backend::global(cx);
|
||||
let Some(me) = backend.read(cx).current_user() else {
|
||||
self.refresh.abort();
|
||||
return;
|
||||
};
|
||||
|
||||
let client = backend.read(cx).client();
|
||||
let state = self.state.clone();
|
||||
|
||||
let work = cx.background_spawn(async move {
|
||||
let deletion_events = client.database().query(filters::deletions()).await?;
|
||||
let deletions = Deletions::from_events(deletion_events);
|
||||
|
||||
let (notification_events, by_id) = fetch_notifications(&client, me, &deletions).await?;
|
||||
let notifications = inbox::group(notification_events, me, &state, &|id| {
|
||||
by_id.get(&id).cloned()
|
||||
});
|
||||
let unread_count = notifications.iter().filter(|item| item.is_unread()).count();
|
||||
|
||||
let mut activity = Vec::new();
|
||||
for event in client
|
||||
.database()
|
||||
.query(filters::authored_activity(me))
|
||||
.await?
|
||||
{
|
||||
if deletions.is_deleted(&event) || !filters::is_git_activity(&event) {
|
||||
continue;
|
||||
}
|
||||
activity.push(event);
|
||||
}
|
||||
|
||||
activity.sort_by(|a, b| {
|
||||
b.created_at
|
||||
.cmp(&a.created_at)
|
||||
.then_with(|| b.id.to_hex().cmp(&a.id.to_hex()))
|
||||
});
|
||||
activity.truncate(ACTIVITY_LIMIT);
|
||||
|
||||
Ok::<_, Error>((notifications, activity, unread_count))
|
||||
});
|
||||
|
||||
let task: Task<Result<(), Error>> = cx.spawn(async move |this, cx| {
|
||||
let (notifications, activity, unread_count) = match work.await {
|
||||
Ok(results) => results,
|
||||
// Database errors are transient, keep the last lists.
|
||||
Err(error) => {
|
||||
log::warn!("inbox refresh failed: {error}");
|
||||
return this.update(cx, |this, _cx| this.refresh.abort());
|
||||
}
|
||||
};
|
||||
|
||||
let again = this.update(cx, |this, cx| {
|
||||
// The signer may have changed while the query ran, making
|
||||
// these results belong to the previous user.
|
||||
if Backend::global(cx).read(cx).current_user() != Some(me) {
|
||||
this.refresh.abort();
|
||||
return false;
|
||||
}
|
||||
|
||||
this.notifications = Arc::new(notifications);
|
||||
this.activity = Arc::new(activity);
|
||||
this.unread_count = unread_count;
|
||||
this.publish_unread_count(cx);
|
||||
cx.notify();
|
||||
|
||||
this.refresh.finish()
|
||||
})?;
|
||||
|
||||
if again {
|
||||
this.update(cx, |this, cx| this.refresh(cx))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
task.detach();
|
||||
}
|
||||
|
||||
/// Recompute the unread and archived flags from the current state.
|
||||
fn regroup(&mut self) {
|
||||
let mut items = (*self.notifications).clone();
|
||||
|
||||
for item in items.iter_mut() {
|
||||
item.apply_state(&self.state);
|
||||
}
|
||||
|
||||
self.unread_count = items.iter().filter(|item| item.is_unread()).count();
|
||||
self.notifications = Arc::new(items);
|
||||
}
|
||||
|
||||
/// Publish the derived unread count for the sidebar badge.
|
||||
fn publish_unread_count(&self, cx: &mut Context<Self>) {
|
||||
let count = self.unread_count;
|
||||
let inbox = Backend::global(cx).read(cx).inbox();
|
||||
inbox.update(cx, |inbox, cx| inbox.set_unread_count(count, cx));
|
||||
}
|
||||
|
||||
/// Forget everything derived for the current user.
|
||||
fn clear(&mut self) {
|
||||
self.notifications = Arc::new(Vec::new());
|
||||
self.activity = Arc::new(Vec::new());
|
||||
self.unread_count = 0;
|
||||
self.state = InboxReadState::default();
|
||||
self.state_loaded = false;
|
||||
// Drop any in-flight or pending run belonging to the previous user.
|
||||
self.refresh = RefreshGate::default();
|
||||
}
|
||||
|
||||
/// Events of the group rooted at `root`.
|
||||
fn group_events(&self, root: EventId) -> Option<Vec<Event>> {
|
||||
self.notifications
|
||||
.iter()
|
||||
.find(|item| item.root == root)
|
||||
.map(|item| item.events.clone())
|
||||
}
|
||||
|
||||
/// Every event in every group, archived groups included.
|
||||
fn all_notification_events(&self) -> Vec<Event> {
|
||||
self.notifications
|
||||
.iter()
|
||||
.flat_map(|item| item.events.iter().cloned())
|
||||
.collect()
|
||||
}
|
||||
Ok((notifications, activity, unread_count))
|
||||
}
|
||||
|
||||
/// `d` tag identifying the inbox state event of `me`.
|
||||
|
||||
@@ -13,9 +13,10 @@ pub use backend::{Backend, BackendEvent, user_grasp_list_servers};
|
||||
pub use checkouts::{CheckoutStatus, CheckoutsStore, pr_proposes_checkout};
|
||||
pub use git_store::GitStore;
|
||||
use gpui::{App, AppContext};
|
||||
pub use inbox::{Inbox, InboxStore};
|
||||
pub use inbox::{Inbox, query_inbox};
|
||||
pub use nostr_sdk::prelude::Timestamp;
|
||||
pub use profile::{Profile, ProfileStore};
|
||||
pub use refresh::{RefreshGate, RefreshRequest};
|
||||
pub use repo::RepoStore;
|
||||
pub use repos::{LocalReposStore, RepoActivityCounts, RepoListStore};
|
||||
use signed_nostr::new_backend;
|
||||
|
||||
Reference in New Issue
Block a user