Add a badge to the bell icon on new notifications (#6751)

David Rachmaninoff created

It changes the icon if a new notification event is consumed and changes
it back to normal upon toggling NotificationPanel.

Added a new field to NotificationPanel:
	- have_unseen_notifications: bool

Added a new icon asset
	- IconName::BellBadged => "assets/icons/bell_badged.svg"

Release Notes:

- Added a badge to bell icon for new notifications
([#6721](https://github.com/zed-industries/zed/issues/6721)).

Change summary

assets/icons/bell_badged.svg               |  5 +++
crates/collab_ui/src/notification_panel.rs | 37 ++++++++++++++++++++---
crates/ui/src/components/icon.rs           |  2 +
3 files changed, 39 insertions(+), 5 deletions(-)

Detailed changes

assets/icons/bell_badged.svg 🔗

@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bell">
+    <path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/>
+    <path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/>
+    <circle cx="19" cy="19" r="4" fill="white"/>
+</svg>

crates/collab_ui/src/notification_panel.rs 🔗

@@ -47,6 +47,7 @@ pub struct NotificationPanel {
     local_timezone: UtcOffset,
     focus_handle: FocusHandle,
     mark_as_read_tasks: HashMap<u64, Task<Result<()>>>,
+    unseen_notifications: Vec<NotificationEntry>,
 }
 
 #[derive(Serialize, Deserialize)]
@@ -141,6 +142,7 @@ impl NotificationPanel {
                 active: false,
                 mark_as_read_tasks: HashMap::default(),
                 width: None,
+                unseen_notifications: Vec::new(),
             };
 
             let mut old_dock_position = this.position(cx);
@@ -441,6 +443,10 @@ impl NotificationPanel {
     }
 
     fn is_showing_notification(&self, notification: &Notification, cx: &ViewContext<Self>) -> bool {
+        if !self.active {
+            return false;
+        }
+
         if let Notification::ChannelMessageMention { channel_id, .. } = &notification {
             if let Some(workspace) = self.workspace.upgrade() {
                 return if let Some(panel) = workspace.read(cx).panel::<ChatPanel>(cx) {
@@ -465,9 +471,17 @@ impl NotificationPanel {
         cx: &mut ViewContext<Self>,
     ) {
         match event {
-            NotificationEvent::NewNotification { entry } => self.add_toast(entry, cx),
+            NotificationEvent::NewNotification { entry } => {
+                if !self.is_showing_notification(&entry.notification, cx) {
+                    self.unseen_notifications.push(entry.clone());
+                }
+                self.add_toast(entry, cx);
+            }
             NotificationEvent::NotificationRemoved { entry }
-            | NotificationEvent::NotificationRead { entry } => self.remove_toast(entry.id, cx),
+            | NotificationEvent::NotificationRead { entry } => {
+                self.unseen_notifications.retain(|n| n.id != entry.id);
+                self.remove_toast(entry.id, cx);
+            }
             NotificationEvent::NotificationsUpdated {
                 old_range,
                 new_count,
@@ -650,15 +664,28 @@ impl Panel for NotificationPanel {
 
     fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
         self.active = active;
+
+        if self.active {
+            self.unseen_notifications = Vec::new();
+            cx.notify();
+        }
+
         if self.notification_store.read(cx).notification_count() == 0 {
             cx.emit(Event::Dismissed);
         }
     }
 
     fn icon(&self, cx: &gpui::WindowContext) -> Option<IconName> {
-        (NotificationPanelSettings::get_global(cx).button
-            && self.notification_store.read(cx).notification_count() > 0)
-            .then(|| IconName::Bell)
+        let show_button = NotificationPanelSettings::get_global(cx).button;
+        if !show_button {
+            return None;
+        }
+
+        if self.unseen_notifications.is_empty() {
+            return Some(IconName::Bell);
+        }
+
+        Some(IconName::BellBadged)
     }
 
     fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {

crates/ui/src/components/icon.rs 🔗

@@ -39,6 +39,7 @@ pub enum IconName {
     Bell,
     BellOff,
     BellRing,
+    BellBadged,
     Bolt,
     CaseSensitive,
     Check,
@@ -130,6 +131,7 @@ impl IconName {
             IconName::Bell => "icons/bell.svg",
             IconName::BellOff => "icons/bell_off.svg",
             IconName::BellRing => "icons/bell_ring.svg",
+            IconName::BellBadged => "icons/bell_badged.svg",
             IconName::Bolt => "icons/bolt.svg",
             IconName::CaseSensitive => "icons/case_insensitive.svg",
             IconName::Check => "icons/check.svg",