Load more notifications when scrolling down

Max Brunsfeld created

Change summary

crates/collab/src/rpc.rs                       |  5 
crates/collab_ui/src/chat_panel.rs             |  2 
crates/collab_ui/src/notification_panel.rs     | 27 ++++--
crates/gpui/src/elements/list.rs               | 11 +
crates/notifications/src/notification_store.rs | 81 ++++++++++++++++---
crates/rpc/proto/zed.proto                     |  1 
6 files changed, 97 insertions(+), 30 deletions(-)

Detailed changes

crates/collab/src/rpc.rs 🔗

@@ -3184,7 +3184,10 @@ async fn get_notifications(
                 .map(|id| db::NotificationId::from_proto(id)),
         )
         .await?;
-    response.send(proto::GetNotificationsResponse { notifications })?;
+    response.send(proto::GetNotificationsResponse {
+        done: notifications.len() < NOTIFICATION_COUNT_PER_PAGE,
+        notifications,
+    })?;
     Ok(())
 }
 

crates/collab_ui/src/chat_panel.rs 🔗

@@ -134,7 +134,7 @@ impl ChatPanel {
             ListState::<Self>::new(0, Orientation::Bottom, 1000., move |this, ix, cx| {
                 this.render_message(ix, cx)
             });
-        message_list.set_scroll_handler(|visible_range, this, cx| {
+        message_list.set_scroll_handler(|visible_range, _, this, cx| {
             if visible_range.start < MESSAGE_LOADING_THRESHOLD {
                 this.load_more_messages(&LoadMoreMessages, cx);
             }

crates/collab_ui/src/notification_panel.rs 🔗

@@ -1,7 +1,4 @@
-use crate::{
-    chat_panel::ChatPanel, format_timestamp, is_channels_feature_enabled, render_avatar,
-    NotificationPanelSettings,
-};
+use crate::{chat_panel::ChatPanel, format_timestamp, render_avatar, NotificationPanelSettings};
 use anyhow::Result;
 use channel::ChannelStore;
 use client::{Client, Notification, User, UserStore};
@@ -29,6 +26,7 @@ use workspace::{
     Workspace,
 };
 
+const LOADING_THRESHOLD: usize = 30;
 const MARK_AS_READ_DELAY: Duration = Duration::from_secs(1);
 const TOAST_DURATION: Duration = Duration::from_secs(5);
 const NOTIFICATION_PANEL_KEY: &'static str = "NotificationPanel";
@@ -98,11 +96,21 @@ impl NotificationPanel {
             })
             .detach();
 
-            let notification_list =
+            let mut notification_list =
                 ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
                     this.render_notification(ix, cx)
                         .unwrap_or_else(|| Empty::new().into_any())
                 });
+            notification_list.set_scroll_handler(|visible_range, count, this, cx| {
+                if count.saturating_sub(visible_range.end) < LOADING_THRESHOLD {
+                    if let Some(task) = this
+                        .notification_store
+                        .update(cx, |store, cx| store.load_more_notifications(false, cx))
+                    {
+                        task.detach();
+                    }
+                }
+            });
 
             let mut this = Self {
                 fs,
@@ -653,15 +661,14 @@ impl Panel for NotificationPanel {
 
     fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
         self.active = active;
-        if active {
-            if !is_channels_feature_enabled(cx) {
-                cx.emit(Event::Dismissed);
-            }
+        if self.notification_store.read(cx).notification_count() == 0 {
+            cx.emit(Event::Dismissed);
         }
     }
 
     fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
-        (settings::get::<NotificationPanelSettings>(cx).button && is_channels_feature_enabled(cx))
+        (settings::get::<NotificationPanelSettings>(cx).button
+            && self.notification_store.read(cx).notification_count() > 0)
             .then(|| "icons/bell.svg")
     }
 

crates/gpui/src/elements/list.rs 🔗

@@ -30,7 +30,7 @@ struct StateInner<V> {
     orientation: Orientation,
     overdraw: f32,
     #[allow(clippy::type_complexity)]
-    scroll_handler: Option<Box<dyn FnMut(Range<usize>, &mut V, &mut ViewContext<V>)>>,
+    scroll_handler: Option<Box<dyn FnMut(Range<usize>, usize, &mut V, &mut ViewContext<V>)>>,
 }
 
 #[derive(Clone, Copy, Debug, Default, PartialEq)]
@@ -420,7 +420,7 @@ impl<V: 'static> ListState<V> {
 
     pub fn set_scroll_handler(
         &mut self,
-        handler: impl FnMut(Range<usize>, &mut V, &mut ViewContext<V>) + 'static,
+        handler: impl FnMut(Range<usize>, usize, &mut V, &mut ViewContext<V>) + 'static,
     ) {
         self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
     }
@@ -533,7 +533,12 @@ impl<V: 'static> StateInner<V> {
 
         if self.scroll_handler.is_some() {
             let visible_range = self.visible_range(height, scroll_top);
-            self.scroll_handler.as_mut().unwrap()(visible_range, view, cx);
+            self.scroll_handler.as_mut().unwrap()(
+                visible_range,
+                self.items.summary().count,
+                view,
+                cx,
+            );
         }
 
         cx.notify();

crates/notifications/src/notification_store.rs 🔗

@@ -21,6 +21,7 @@ pub struct NotificationStore {
     channel_messages: HashMap<u64, ChannelMessage>,
     channel_store: ModelHandle<ChannelStore>,
     notifications: SumTree<NotificationEntry>,
+    loaded_all_notifications: bool,
     _watch_connection_status: Task<Option<()>>,
     _subscriptions: Vec<client::Subscription>,
 }
@@ -83,9 +84,10 @@ impl NotificationStore {
                 let this = this.upgrade(&cx)?;
                 match status {
                     client::Status::Connected { .. } => {
-                        this.update(&mut cx, |this, cx| this.handle_connect(cx))
-                            .await
-                            .log_err()?;
+                        if let Some(task) = this.update(&mut cx, |this, cx| this.handle_connect(cx))
+                        {
+                            task.await.log_err()?;
+                        }
                     }
                     _ => this.update(&mut cx, |this, cx| this.handle_disconnect(cx)),
                 }
@@ -96,6 +98,7 @@ impl NotificationStore {
         Self {
             channel_store: ChannelStore::global(cx),
             notifications: Default::default(),
+            loaded_all_notifications: false,
             channel_messages: Default::default(),
             _watch_connection_status: watch_connection_status,
             _subscriptions: vec![
@@ -142,22 +145,46 @@ impl NotificationStore {
         None
     }
 
-    pub fn load_more_notifications(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
-        let request = self
-            .client
-            .request(proto::GetNotifications { before_id: None });
-        cx.spawn(|this, cx| async move {
+    pub fn load_more_notifications(
+        &self,
+        clear_old: bool,
+        cx: &mut ModelContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        if self.loaded_all_notifications && !clear_old {
+            return None;
+        }
+
+        let before_id = if clear_old {
+            None
+        } else {
+            self.notifications.first().map(|entry| entry.id)
+        };
+        let request = self.client.request(proto::GetNotifications { before_id });
+        Some(cx.spawn(|this, mut cx| async move {
             let response = request.await?;
-            Self::add_notifications(this, false, response.notifications, cx).await?;
+            this.update(&mut cx, |this, _| {
+                this.loaded_all_notifications = response.done
+            });
+            Self::add_notifications(
+                this,
+                response.notifications,
+                AddNotificationsOptions {
+                    is_new: false,
+                    clear_old,
+                    includes_first: response.done,
+                },
+                cx,
+            )
+            .await?;
             Ok(())
-        })
+        }))
     }
 
-    fn handle_connect(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
+    fn handle_connect(&mut self, cx: &mut ModelContext<Self>) -> Option<Task<Result<()>>> {
         self.notifications = Default::default();
         self.channel_messages = Default::default();
         cx.notify();
-        self.load_more_notifications(cx)
+        self.load_more_notifications(true, cx)
     }
 
     fn handle_disconnect(&mut self, cx: &mut ModelContext<Self>) {
@@ -172,8 +199,12 @@ impl NotificationStore {
     ) -> Result<()> {
         Self::add_notifications(
             this,
-            true,
             envelope.payload.notification.into_iter().collect(),
+            AddNotificationsOptions {
+                is_new: true,
+                clear_old: false,
+                includes_first: false,
+            },
             cx,
         )
         .await
@@ -193,8 +224,8 @@ impl NotificationStore {
 
     async fn add_notifications(
         this: ModelHandle<Self>,
-        is_new: bool,
         notifications: Vec<proto::Notification>,
+        options: AddNotificationsOptions,
         mut cx: AsyncAppContext,
     ) -> Result<()> {
         let mut user_ids = Vec::new();
@@ -256,6 +287,20 @@ impl NotificationStore {
             })
             .await?;
         this.update(&mut cx, |this, cx| {
+            if options.clear_old {
+                cx.emit(NotificationEvent::NotificationsUpdated {
+                    old_range: 0..this.notifications.summary().count,
+                    new_count: 0,
+                });
+                this.notifications = SumTree::default();
+                this.channel_messages.clear();
+                this.loaded_all_notifications = false;
+            }
+
+            if options.includes_first {
+                this.loaded_all_notifications = true;
+            }
+
             this.channel_messages
                 .extend(messages.into_iter().filter_map(|message| {
                     if let ChannelMessageId::Saved(id) = message.id {
@@ -269,7 +314,7 @@ impl NotificationStore {
                 notifications
                     .into_iter()
                     .map(|notification| (notification.id, Some(notification))),
-                is_new,
+                options.is_new,
                 cx,
             );
         });
@@ -406,3 +451,9 @@ impl<'a> sum_tree::Dimension<'a, NotificationSummary> for UnreadCount {
         self.0 += summary.unread_count;
     }
 }
+
+struct AddNotificationsOptions {
+    is_new: bool,
+    clear_old: bool,
+    includes_first: bool,
+}

crates/rpc/proto/zed.proto 🔗

@@ -1616,6 +1616,7 @@ message AddNotification {
 
 message GetNotificationsResponse {
     repeated Notification notifications = 1;
+    bool done = 2;
 }
 
 message DeleteNotification {