Avoid flickering toast when mentioned in already-visible chat channel

Max Brunsfeld and Piotr created

Co-authored-by: Piotr <piotr@zed.dev>

Change summary

crates/collab_ui/src/chat_panel.rs         | 13 ++++++++--
crates/collab_ui/src/notification_panel.rs | 27 ++++++++++++++++++++++++
styles/src/style_tree/chat_panel.ts        |  1 
3 files changed, 37 insertions(+), 4 deletions(-)

Detailed changes

crates/collab_ui/src/chat_panel.rs 🔗

@@ -54,6 +54,7 @@ pub struct ChatPanel {
     pending_serialization: Task<Option<()>>,
     subscriptions: Vec<gpui::Subscription>,
     workspace: WeakViewHandle<Workspace>,
+    is_scrolled_to_bottom: bool,
     has_focus: bool,
     markdown_data: HashMap<ChannelMessageId, RichText>,
 }
@@ -131,13 +132,14 @@ impl ChatPanel {
         });
 
         let mut message_list =
-            ListState::<Self>::new(0, Orientation::Bottom, 1000., move |this, ix, cx| {
+            ListState::<Self>::new(0, Orientation::Bottom, 10., 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, count, this, cx| {
             if visible_range.start < MESSAGE_LOADING_THRESHOLD {
                 this.load_more_messages(&LoadMoreMessages, cx);
             }
+            this.is_scrolled_to_bottom = visible_range.end == count;
         });
 
         cx.add_view(|cx| {
@@ -155,6 +157,7 @@ impl ChatPanel {
                 has_focus: false,
                 subscriptions: Vec::new(),
                 workspace: workspace_handle,
+                is_scrolled_to_bottom: true,
                 active: false,
                 width: None,
                 markdown_data: Default::default(),
@@ -198,6 +201,10 @@ impl ChatPanel {
         })
     }
 
+    pub fn is_scrolled_to_bottom(&self) -> bool {
+        self.is_scrolled_to_bottom
+    }
+
     pub fn active_chat(&self) -> Option<ModelHandle<ChannelChat>> {
         self.active_chat.as_ref().map(|(chat, _)| chat.clone())
     }
@@ -310,7 +317,7 @@ impl ChatPanel {
     }
 
     fn acknowledge_last_message(&mut self, cx: &mut ViewContext<'_, '_, ChatPanel>) {
-        if self.active {
+        if self.active && self.is_scrolled_to_bottom {
             if let Some((chat, _)) = &self.active_chat {
                 chat.update(cx, |chat, cx| {
                     chat.acknowledge_last_message(cx);

crates/collab_ui/src/notification_panel.rs 🔗

@@ -460,6 +460,28 @@ impl NotificationPanel {
         }
     }
 
+    fn is_showing_notification(&self, notification: &Notification, cx: &AppContext) -> bool {
+        if let Notification::ChannelMessageMention { channel_id, .. } = &notification {
+            if let Some(workspace) = self.workspace.upgrade(cx) {
+                return workspace
+                    .read_with(cx, |workspace, cx| {
+                        if let Some(panel) = workspace.panel::<ChatPanel>(cx) {
+                            return panel.read_with(cx, |panel, cx| {
+                                panel.is_scrolled_to_bottom()
+                                    && panel.active_chat().map_or(false, |chat| {
+                                        chat.read(cx).channel().id == *channel_id
+                                    })
+                            });
+                        }
+                        false
+                    })
+                    .unwrap_or_default();
+            }
+        }
+
+        false
+    }
+
     fn render_sign_in_prompt(
         &self,
         theme: &Arc<Theme>,
@@ -523,6 +545,10 @@ impl NotificationPanel {
     }
 
     fn add_toast(&mut self, entry: &NotificationEntry, cx: &mut ViewContext<Self>) {
+        if self.is_showing_notification(&entry.notification, cx) {
+            return;
+        }
+
         let Some(NotificationPresenter { actor, text, .. }) = self.present_notification(entry, cx)
         else {
             return;
@@ -540,6 +566,7 @@ impl NotificationPanel {
 
         self.workspace
             .update(cx, |workspace, cx| {
+                workspace.dismiss_notification::<NotificationToast>(0, cx);
                 workspace.show_notification(0, cx, |cx| {
                     let workspace = cx.weak_handle();
                     cx.add_view(|_| NotificationToast {

styles/src/style_tree/chat_panel.ts 🔗

@@ -2,7 +2,6 @@ import { background, border, foreground, text } from "./components"
 import { icon_button } from "../component/icon_button"
 import { useTheme, with_opacity } from "../theme"
 import { interactive } from "../element"
-import { Color } from "ayu/dist/color"
 
 export default function chat_panel(): any {
     const theme = useTheme()