Remove notification panel (#50204)

Anthony Eid created

After chat functionality was removed, this panel became redundant. It
only displayed three notification types: incoming contact requests,
accepted contact requests, and channel invitations.

This PR moves those notifications into the collab experience by adding
toast popups and a badge count to the collab panel. It also removes the
notification-panel-specific settings, documentation, and Vim command.

Before you mark this PR as ready for review, make sure that you have:
- [ ] Added a solid test coverage and/or screenshots from doing manual
testing
- [x] Done a self-review taking into account security and performance
aspects
- [x] Aligned any UI changes with the [UI
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)

Release Notes:

- Removed the notification panel from Zed

Change summary

Cargo.lock                                             |   3 
assets/settings/default.json                           |  10 
crates/agent_settings/src/agent_settings.rs            |  15 
crates/agent_ui/src/agent_ui.rs                        |   2 
crates/collab_ui/Cargo.toml                            |   3 
crates/collab_ui/src/collab_panel.rs                   | 362 +++++
crates/collab_ui/src/collab_ui.rs                      |   4 
crates/collab_ui/src/notification_panel.rs             | 727 ------------
crates/collab_ui/src/panel_settings.rs                 |  20 
crates/settings/src/vscode_import.rs                   |   2 
crates/settings_content/src/settings_content.rs        |  25 
crates/settings_ui/src/page_data.rs                    |  91 -
crates/ui/src/components/collab/collab_notification.rs |  56 
crates/vim/src/command.rs                              |   1 
crates/zed/src/zed.rs                                  |  16 
docs/src/visual-customization.md                       |  14 
16 files changed, 395 insertions(+), 956 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3208,7 +3208,6 @@ dependencies = [
  "anyhow",
  "call",
  "channel",
- "chrono",
  "client",
  "collections",
  "db",
@@ -3217,7 +3216,6 @@ dependencies = [
  "fuzzy",
  "gpui",
  "livekit_client",
- "log",
  "menu",
  "notifications",
  "picker",
@@ -3232,7 +3230,6 @@ dependencies = [
  "theme",
  "theme_settings",
  "time",
- "time_format",
  "title_bar",
  "ui",
  "util",

assets/settings/default.json 🔗

@@ -936,16 +936,6 @@
     // For example: typing `:wave:` gets replaced with `👋`.
     "auto_replace_emoji_shortcode": true,
   },
-  "notification_panel": {
-    // Whether to show the notification panel button in the status bar.
-    "button": true,
-    // Where to dock the notification panel. Can be 'left' or 'right'.
-    "dock": "right",
-    // Default width of the notification panel.
-    "default_width": 380,
-    // Whether to show a badge on the notification panel icon with the count of unread notifications.
-    "show_count_badge": false,
-  },
   "agent": {
     // Whether the inline assistant should use streaming tools, when available
     "inline_assistant_use_streaming_tools": true,

crates/agent_settings/src/agent_settings.rs 🔗

@@ -31,7 +31,6 @@ pub struct PanelLayout {
     pub(crate) outline_panel_dock: Option<DockSide>,
     pub(crate) collaboration_panel_dock: Option<DockPosition>,
     pub(crate) git_panel_dock: Option<DockPosition>,
-    pub(crate) notification_panel_button: Option<bool>,
 }
 
 impl PanelLayout {
@@ -41,7 +40,6 @@ impl PanelLayout {
         outline_panel_dock: Some(DockSide::Right),
         collaboration_panel_dock: Some(DockPosition::Right),
         git_panel_dock: Some(DockPosition::Right),
-        notification_panel_button: Some(false),
     };
 
     const EDITOR: Self = Self {
@@ -50,7 +48,6 @@ impl PanelLayout {
         outline_panel_dock: Some(DockSide::Left),
         collaboration_panel_dock: Some(DockPosition::Left),
         git_panel_dock: Some(DockPosition::Left),
-        notification_panel_button: Some(true),
     };
 
     pub fn is_agent_layout(&self) -> bool {
@@ -68,7 +65,6 @@ impl PanelLayout {
             outline_panel_dock: content.outline_panel.as_ref().and_then(|p| p.dock),
             collaboration_panel_dock: content.collaboration_panel.as_ref().and_then(|p| p.dock),
             git_panel_dock: content.git_panel.as_ref().and_then(|p| p.dock),
-            notification_panel_button: content.notification_panel.as_ref().and_then(|p| p.button),
         }
     }
 
@@ -78,7 +74,6 @@ impl PanelLayout {
         settings.outline_panel.get_or_insert_default().dock = self.outline_panel_dock;
         settings.collaboration_panel.get_or_insert_default().dock = self.collaboration_panel_dock;
         settings.git_panel.get_or_insert_default().dock = self.git_panel_dock;
-        settings.notification_panel.get_or_insert_default().button = self.notification_panel_button;
     }
 
     fn write_diff_to(&self, current_merged: &PanelLayout, settings: &mut SettingsContent) {
@@ -98,10 +93,6 @@ impl PanelLayout {
         if self.git_panel_dock != current_merged.git_panel_dock {
             settings.git_panel.get_or_insert_default().dock = self.git_panel_dock;
         }
-        if self.notification_panel_button != current_merged.notification_panel_button {
-            settings.notification_panel.get_or_insert_default().button =
-                self.notification_panel_button;
-        }
     }
 
     fn backfill_to(&self, user_layout: &PanelLayout, settings: &mut SettingsContent) {
@@ -121,10 +112,6 @@ impl PanelLayout {
         if user_layout.git_panel_dock.is_none() {
             settings.git_panel.get_or_insert_default().dock = self.git_panel_dock;
         }
-        if user_layout.notification_panel_button.is_none() {
-            settings.notification_panel.get_or_insert_default().button =
-                self.notification_panel_button;
-        }
     }
 }
 
@@ -1257,7 +1244,6 @@ mod tests {
         assert_eq!(user_layout.outline_panel_dock, None);
         assert_eq!(user_layout.collaboration_panel_dock, None);
         assert_eq!(user_layout.git_panel_dock, None);
-        assert_eq!(user_layout.notification_panel_button, None);
 
         // User sets a combination that doesn't match either preset:
         // agent on the left but project panel also on the left.
@@ -1480,7 +1466,6 @@ mod tests {
                 Some(DockPosition::Left)
             );
             assert_eq!(user_layout.git_panel_dock, Some(DockPosition::Left));
-            assert_eq!(user_layout.notification_panel_button, Some(true));
 
             // Now switch defaults to agent V2.
             set_agent_v2_defaults(cx);

crates/agent_ui/src/agent_ui.rs 🔗

@@ -524,7 +524,6 @@ pub fn init(
                     defaults.collaboration_panel.get_or_insert_default().dock =
                         Some(DockPosition::Right);
                     defaults.git_panel.get_or_insert_default().dock = Some(DockPosition::Right);
-                    defaults.notification_panel.get_or_insert_default().button = Some(false);
                 } else {
                     defaults.agent.get_or_insert_default().dock = Some(DockPosition::Right);
                     defaults.project_panel.get_or_insert_default().dock = Some(DockSide::Left);
@@ -532,7 +531,6 @@ pub fn init(
                     defaults.collaboration_panel.get_or_insert_default().dock =
                         Some(DockPosition::Left);
                     defaults.git_panel.get_or_insert_default().dock = Some(DockPosition::Left);
-                    defaults.notification_panel.get_or_insert_default().button = Some(true);
                 }
             });
         });

crates/collab_ui/Cargo.toml 🔗

@@ -32,7 +32,6 @@ test-support = [
 anyhow.workspace = true
 call.workspace = true
 channel.workspace = true
-chrono.workspace = true
 client.workspace = true
 collections.workspace = true
 db.workspace = true
@@ -41,7 +40,6 @@ futures.workspace = true
 fuzzy.workspace = true
 gpui.workspace = true
 livekit_client.workspace = true
-log.workspace = true
 menu.workspace = true
 notifications.workspace = true
 picker.workspace = true
@@ -56,7 +54,6 @@ telemetry.workspace = true
 theme.workspace = true
 theme_settings.workspace = true
 time.workspace = true
-time_format.workspace = true
 title_bar.workspace = true
 ui.workspace = true
 util.workspace = true

crates/collab_ui/src/collab_panel.rs 🔗

@@ -6,7 +6,7 @@ use crate::{CollaborationPanelSettings, channel_view::ChannelView};
 use anyhow::Context as _;
 use call::ActiveCall;
 use channel::{Channel, ChannelEvent, ChannelStore};
-use client::{ChannelId, Client, Contact, User, UserStore};
+use client::{ChannelId, Client, Contact, Notification, User, UserStore};
 use collections::{HashMap, HashSet};
 use contact_finder::ContactFinder;
 use db::kvp::KeyValueStore;
@@ -21,6 +21,7 @@ use gpui::{
 };
 
 use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrevious};
+use notifications::{NotificationEntry, NotificationEvent, NotificationStore};
 use project::{Fs, Project};
 use rpc::{
     ErrorCode, ErrorExt,
@@ -29,19 +30,23 @@ use rpc::{
 use serde::{Deserialize, Serialize};
 use settings::Settings;
 use smallvec::SmallVec;
-use std::{mem, sync::Arc};
+use std::{mem, sync::Arc, time::Duration};
 use theme::ActiveTheme;
 use theme_settings::ThemeSettings;
 use ui::{
-    Avatar, AvatarAvailabilityIndicator, ContextMenu, CopyButton, Facepile, HighlightedLabel,
-    IconButtonShape, Indicator, ListHeader, ListItem, Tab, Tooltip, prelude::*, tooltip_container,
+    Avatar, AvatarAvailabilityIndicator, CollabNotification, ContextMenu, CopyButton, Facepile,
+    HighlightedLabel, IconButtonShape, Indicator, ListHeader, ListItem, Tab, Tooltip, prelude::*,
+    tooltip_container,
 };
 use util::{ResultExt, TryFutureExt, maybe};
 use workspace::{
     CopyRoomId, Deafen, LeaveCall, MultiWorkspace, Mute, OpenChannelNotes, OpenChannelNotesById,
     ScreenShare, ShareProject, Workspace,
     dock::{DockPosition, Panel, PanelEvent},
-    notifications::{DetachAndPromptErr, NotifyResultExt},
+    notifications::{
+        DetachAndPromptErr, Notification as WorkspaceNotification, NotificationId, NotifyResultExt,
+        SuppressEvent,
+    },
 };
 
 const FILTER_OCCUPIED_CHANNELS_KEY: &str = "filter_occupied_channels";
@@ -87,6 +92,7 @@ struct ChannelMoveClipboard {
 }
 
 const COLLABORATION_PANEL_KEY: &str = "CollaborationPanel";
+const TOAST_DURATION: Duration = Duration::from_secs(5);
 
 pub fn init(cx: &mut App) {
     cx.observe_new(|workspace: &mut Workspace, _, _| {
@@ -267,6 +273,9 @@ pub struct CollabPanel {
     collapsed_channels: Vec<ChannelId>,
     filter_occupied_channels: bool,
     workspace: WeakEntity<Workspace>,
+    notification_store: Entity<NotificationStore>,
+    current_notification_toast: Option<(u64, Task<()>)>,
+    mark_as_read_tasks: HashMap<u64, Task<anyhow::Result<()>>>,
 }
 
 #[derive(Serialize, Deserialize)]
@@ -394,6 +403,9 @@ impl CollabPanel {
                 channel_editing_state: None,
                 selection: None,
                 channel_store: ChannelStore::global(cx),
+                notification_store: NotificationStore::global(cx),
+                current_notification_toast: None,
+                mark_as_read_tasks: HashMap::default(),
                 user_store: workspace.user_store().clone(),
                 project: workspace.project().clone(),
                 subscriptions: Vec::default(),
@@ -437,6 +449,11 @@ impl CollabPanel {
                     }
                 },
             ));
+            this.subscriptions.push(cx.subscribe_in(
+                &this.notification_store,
+                window,
+                Self::on_notification_event,
+            ));
 
             this
         })
@@ -2665,26 +2682,28 @@ impl CollabPanel {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> AnyElement {
-        let entry = &self.entries[ix];
+        let entry = self.entries[ix].clone();
 
         let is_selected = self.selection == Some(ix);
         match entry {
             ListEntry::Header(section) => {
-                let is_collapsed = self.collapsed_sections.contains(section);
-                self.render_header(*section, is_selected, is_collapsed, cx)
+                let is_collapsed = self.collapsed_sections.contains(&section);
+                self.render_header(section, is_selected, is_collapsed, cx)
+                    .into_any_element()
+            }
+            ListEntry::Contact { contact, calling } => {
+                self.mark_contact_request_accepted_notifications_read(contact.user.id, cx);
+                self.render_contact(&contact, calling, is_selected, cx)
                     .into_any_element()
             }
-            ListEntry::Contact { contact, calling } => self
-                .render_contact(contact, *calling, is_selected, cx)
-                .into_any_element(),
             ListEntry::ContactPlaceholder => self
                 .render_contact_placeholder(is_selected, cx)
                 .into_any_element(),
             ListEntry::IncomingRequest(user) => self
-                .render_contact_request(user, true, is_selected, cx)
+                .render_contact_request(&user, true, is_selected, cx)
                 .into_any_element(),
             ListEntry::OutgoingRequest(user) => self
-                .render_contact_request(user, false, is_selected, cx)
+                .render_contact_request(&user, false, is_selected, cx)
                 .into_any_element(),
             ListEntry::Channel {
                 channel,
@@ -2694,9 +2713,9 @@ impl CollabPanel {
                 ..
             } => self
                 .render_channel(
-                    channel,
-                    *depth,
-                    *has_children,
+                    &channel,
+                    depth,
+                    has_children,
                     is_selected,
                     ix,
                     string_match.as_ref(),
@@ -2704,10 +2723,10 @@ impl CollabPanel {
                 )
                 .into_any_element(),
             ListEntry::ChannelEditor { depth } => self
-                .render_channel_editor(*depth, window, cx)
+                .render_channel_editor(depth, window, cx)
                 .into_any_element(),
             ListEntry::ChannelInvite(channel) => self
-                .render_channel_invite(channel, is_selected, cx)
+                .render_channel_invite(&channel, is_selected, cx)
                 .into_any_element(),
             ListEntry::CallParticipant {
                 user,
@@ -2715,7 +2734,7 @@ impl CollabPanel {
                 is_pending,
                 role,
             } => self
-                .render_call_participant(user, *peer_id, *is_pending, *role, is_selected, cx)
+                .render_call_participant(&user, peer_id, is_pending, role, is_selected, cx)
                 .into_any_element(),
             ListEntry::ParticipantProject {
                 project_id,
@@ -2724,20 +2743,20 @@ impl CollabPanel {
                 is_last,
             } => self
                 .render_participant_project(
-                    *project_id,
-                    worktree_root_names,
-                    *host_user_id,
-                    *is_last,
+                    project_id,
+                    &worktree_root_names,
+                    host_user_id,
+                    is_last,
                     is_selected,
                     window,
                     cx,
                 )
                 .into_any_element(),
             ListEntry::ParticipantScreen { peer_id, is_last } => self
-                .render_participant_screen(*peer_id, *is_last, is_selected, window, cx)
+                .render_participant_screen(peer_id, is_last, is_selected, window, cx)
                 .into_any_element(),
             ListEntry::ChannelNotes { channel_id } => self
-                .render_channel_notes(*channel_id, is_selected, window, cx)
+                .render_channel_notes(channel_id, is_selected, window, cx)
                 .into_any_element(),
         }
     }
@@ -3397,6 +3416,178 @@ impl CollabPanel {
             item.child(self.channel_name_editor.clone())
         }
     }
+
+    fn on_notification_event(
+        &mut self,
+        _: &Entity<NotificationStore>,
+        event: &NotificationEvent,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        match event {
+            NotificationEvent::NewNotification { entry } => {
+                self.add_toast(entry, cx);
+                cx.notify();
+            }
+            NotificationEvent::NotificationRemoved { entry }
+            | NotificationEvent::NotificationRead { entry } => {
+                self.remove_toast(entry.id, cx);
+                cx.notify();
+            }
+            NotificationEvent::NotificationsUpdated { .. } => {
+                cx.notify();
+            }
+        }
+    }
+
+    fn present_notification(
+        &self,
+        entry: &NotificationEntry,
+        cx: &App,
+    ) -> Option<(Option<Arc<User>>, String)> {
+        let user_store = self.user_store.read(cx);
+        match &entry.notification {
+            Notification::ContactRequest { sender_id } => {
+                let requester = user_store.get_cached_user(*sender_id)?;
+                Some((
+                    Some(requester.clone()),
+                    format!("{} wants to add you as a contact", requester.github_login),
+                ))
+            }
+            Notification::ContactRequestAccepted { responder_id } => {
+                let responder = user_store.get_cached_user(*responder_id)?;
+                Some((
+                    Some(responder.clone()),
+                    format!("{} accepted your contact request", responder.github_login),
+                ))
+            }
+            Notification::ChannelInvitation {
+                channel_name,
+                inviter_id,
+                ..
+            } => {
+                let inviter = user_store.get_cached_user(*inviter_id)?;
+                Some((
+                    Some(inviter.clone()),
+                    format!(
+                        "{} invited you to join the #{channel_name} channel",
+                        inviter.github_login
+                    ),
+                ))
+            }
+        }
+    }
+
+    fn add_toast(&mut self, entry: &NotificationEntry, cx: &mut Context<Self>) {
+        let Some((actor, text)) = self.present_notification(entry, cx) else {
+            return;
+        };
+
+        let notification = entry.notification.clone();
+        let needs_response = matches!(
+            notification,
+            Notification::ContactRequest { .. } | Notification::ChannelInvitation { .. }
+        );
+
+        let notification_id = entry.id;
+
+        self.current_notification_toast = Some((
+            notification_id,
+            cx.spawn(async move |this, cx| {
+                cx.background_executor().timer(TOAST_DURATION).await;
+                this.update(cx, |this, cx| this.remove_toast(notification_id, cx))
+                    .ok();
+            }),
+        ));
+
+        let collab_panel = cx.entity().downgrade();
+        self.workspace
+            .update(cx, |workspace, cx| {
+                let id = NotificationId::unique::<CollabNotificationToast>();
+
+                workspace.dismiss_notification(&id, cx);
+                workspace.show_notification(id, cx, |cx| {
+                    let workspace = cx.entity().downgrade();
+                    cx.new(|cx| CollabNotificationToast {
+                        actor,
+                        text,
+                        notification: needs_response.then(|| notification),
+                        workspace,
+                        collab_panel: collab_panel.clone(),
+                        focus_handle: cx.focus_handle(),
+                    })
+                })
+            })
+            .ok();
+    }
+
+    fn mark_notification_read(&mut self, notification_id: u64, cx: &mut Context<Self>) {
+        let client = self.client.clone();
+        self.mark_as_read_tasks
+            .entry(notification_id)
+            .or_insert_with(|| {
+                cx.spawn(async move |this, cx| {
+                    let request_result = client
+                        .request(proto::MarkNotificationRead { notification_id })
+                        .await;
+
+                    this.update(cx, |this, _| {
+                        this.mark_as_read_tasks.remove(&notification_id);
+                    })?;
+
+                    request_result?;
+                    Ok(())
+                })
+            });
+    }
+
+    fn mark_contact_request_accepted_notifications_read(
+        &mut self,
+        contact_user_id: u64,
+        cx: &mut Context<Self>,
+    ) {
+        let notification_ids = self.notification_store.read_with(cx, |store, _| {
+            (0..store.notification_count())
+                .filter_map(|index| {
+                    let entry = store.notification_at(index)?;
+                    if entry.is_read {
+                        return None;
+                    }
+
+                    match &entry.notification {
+                        Notification::ContactRequestAccepted { responder_id }
+                            if *responder_id == contact_user_id =>
+                        {
+                            Some(entry.id)
+                        }
+                        _ => None,
+                    }
+                })
+                .collect::<Vec<_>>()
+        });
+
+        for notification_id in notification_ids {
+            self.mark_notification_read(notification_id, cx);
+        }
+    }
+
+    fn remove_toast(&mut self, notification_id: u64, cx: &mut Context<Self>) {
+        if let Some((current_id, _)) = &self.current_notification_toast {
+            if *current_id == notification_id {
+                self.dismiss_toast(cx);
+            }
+        }
+    }
+
+    fn dismiss_toast(&mut self, cx: &mut Context<Self>) {
+        self.current_notification_toast.take();
+        self.workspace
+            .update(cx, |workspace, cx| {
+                let id = NotificationId::unique::<CollabNotificationToast>();
+                workspace.dismiss_notification(&id, cx)
+            })
+            .ok();
+    }
 }
 
 fn render_tree_branch(
@@ -3516,12 +3707,38 @@ impl Panel for CollabPanel {
         CollaborationPanelSettings::get_global(cx).default_width
     }
 
+    fn set_active(&mut self, active: bool, _window: &mut Window, cx: &mut Context<Self>) {
+        if active && self.current_notification_toast.is_some() {
+            self.current_notification_toast.take();
+            let workspace = self.workspace.clone();
+            cx.defer(move |cx| {
+                workspace
+                    .update(cx, |workspace, cx| {
+                        let id = NotificationId::unique::<CollabNotificationToast>();
+                        workspace.dismiss_notification(&id, cx)
+                    })
+                    .ok();
+            });
+        }
+    }
+
     fn icon(&self, _window: &Window, cx: &App) -> Option<ui::IconName> {
         CollaborationPanelSettings::get_global(cx)
             .button
             .then_some(ui::IconName::UserGroup)
     }
 
+    fn icon_label(&self, _window: &Window, cx: &App) -> Option<String> {
+        let user_store = self.user_store.read(cx);
+        let count = user_store.incoming_contact_requests().len()
+            + self.channel_store.read(cx).channel_invitations().len();
+        if count == 0 {
+            None
+        } else {
+            Some(count.to_string())
+        }
+    }
+
     fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
         Some("Collab Panel")
     }
@@ -3702,6 +3919,101 @@ impl Render for JoinChannelTooltip {
     }
 }
 
+pub struct CollabNotificationToast {
+    actor: Option<Arc<User>>,
+    text: String,
+    notification: Option<Notification>,
+    workspace: WeakEntity<Workspace>,
+    collab_panel: WeakEntity<CollabPanel>,
+    focus_handle: FocusHandle,
+}
+
+impl Focusable for CollabNotificationToast {
+    fn focus_handle(&self, _cx: &App) -> FocusHandle {
+        self.focus_handle.clone()
+    }
+}
+
+impl WorkspaceNotification for CollabNotificationToast {}
+
+impl CollabNotificationToast {
+    fn focus_collab_panel(&self, window: &mut Window, cx: &mut Context<Self>) {
+        let workspace = self.workspace.clone();
+        window.defer(cx, move |window, cx| {
+            workspace
+                .update(cx, |workspace, cx| {
+                    workspace.focus_panel::<CollabPanel>(window, cx)
+                })
+                .ok();
+        })
+    }
+
+    fn respond(&mut self, accept: bool, window: &mut Window, cx: &mut Context<Self>) {
+        if let Some(notification) = self.notification.take() {
+            self.collab_panel
+                .update(cx, |collab_panel, cx| match notification {
+                    Notification::ContactRequest { sender_id } => {
+                        collab_panel.respond_to_contact_request(sender_id, accept, window, cx);
+                    }
+                    Notification::ChannelInvitation { channel_id, .. } => {
+                        collab_panel.respond_to_channel_invite(ChannelId(channel_id), accept, cx);
+                    }
+                    Notification::ContactRequestAccepted { .. } => {}
+                })
+                .ok();
+        }
+        cx.emit(DismissEvent);
+    }
+}
+
+impl Render for CollabNotificationToast {
+    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let needs_response = self.notification.is_some();
+
+        let accept_button = if needs_response {
+            Button::new("accept", "Accept").on_click(cx.listener(|this, _, window, cx| {
+                this.respond(true, window, cx);
+                cx.stop_propagation();
+            }))
+        } else {
+            Button::new("dismiss", "Dismiss").on_click(cx.listener(|_, _, _, cx| {
+                cx.emit(DismissEvent);
+            }))
+        };
+
+        let decline_button = if needs_response {
+            Button::new("decline", "Decline").on_click(cx.listener(|this, _, window, cx| {
+                this.respond(false, window, cx);
+                cx.stop_propagation();
+            }))
+        } else {
+            Button::new("close", "Close").on_click(cx.listener(|_, _, _, cx| {
+                cx.emit(DismissEvent);
+            }))
+        };
+
+        let avatar_uri = self
+            .actor
+            .as_ref()
+            .map(|user| user.avatar_uri.clone())
+            .unwrap_or_default();
+
+        div()
+            .id("collab_notification_toast")
+            .on_click(cx.listener(|this, _, window, cx| {
+                this.focus_collab_panel(window, cx);
+                cx.emit(DismissEvent);
+            }))
+            .child(
+                CollabNotification::new(avatar_uri, accept_button, decline_button)
+                    .child(Label::new(self.text.clone())),
+            )
+    }
+}
+
+impl EventEmitter<DismissEvent> for CollabNotificationToast {}
+impl EventEmitter<SuppressEvent> for CollabNotificationToast {}
+
 #[cfg(any(test, feature = "test-support"))]
 impl CollabPanel {
     pub fn entries_as_strings(&self) -> Vec<String> {

crates/collab_ui/src/collab_ui.rs 🔗

@@ -1,7 +1,6 @@
 mod call_stats_modal;
 pub mod channel_view;
 pub mod collab_panel;
-pub mod notification_panel;
 pub mod notifications;
 mod panel_settings;
 
@@ -12,7 +11,7 @@ use gpui::{
     App, Pixels, PlatformDisplay, Size, WindowBackgroundAppearance, WindowBounds,
     WindowDecorations, WindowKind, WindowOptions, point,
 };
-pub use panel_settings::{CollaborationPanelSettings, NotificationPanelSettings};
+pub use panel_settings::CollaborationPanelSettings;
 use release_channel::ReleaseChannel;
 use ui::px;
 use workspace::AppState;
@@ -22,7 +21,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut App) {
     call_stats_modal::init(cx);
     channel_view::init(cx);
     collab_panel::init(cx);
-    notification_panel::init(cx);
     notifications::init(app_state, cx);
     title_bar::init(cx);
 }

crates/collab_ui/src/notification_panel.rs 🔗

@@ -1,727 +0,0 @@
-use crate::NotificationPanelSettings;
-use anyhow::Result;
-use channel::ChannelStore;
-use client::{ChannelId, Client, Notification, User, UserStore};
-use collections::HashMap;
-use futures::StreamExt;
-use gpui::{
-    AnyElement, App, AsyncWindowContext, ClickEvent, Context, DismissEvent, Element, Entity,
-    EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ListAlignment,
-    ListScrollEvent, ListState, ParentElement, Render, StatefulInteractiveElement, Styled, Task,
-    WeakEntity, Window, actions, div, img, list, px,
-};
-use notifications::{NotificationEntry, NotificationEvent, NotificationStore};
-use project::Fs;
-use rpc::proto;
-
-use settings::{Settings, SettingsStore};
-use std::{sync::Arc, time::Duration};
-use time::{OffsetDateTime, UtcOffset};
-use ui::{
-    Avatar, Button, Icon, IconButton, IconName, Label, Tab, Tooltip, h_flex, prelude::*, v_flex,
-};
-use util::ResultExt;
-use workspace::notifications::{
-    Notification as WorkspaceNotification, NotificationId, SuppressEvent,
-};
-use workspace::{
-    Workspace,
-    dock::{DockPosition, Panel, PanelEvent},
-};
-
-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: &str = "NotificationPanel";
-
-pub struct NotificationPanel {
-    client: Arc<Client>,
-    user_store: Entity<UserStore>,
-    channel_store: Entity<ChannelStore>,
-    notification_store: Entity<NotificationStore>,
-    fs: Arc<dyn Fs>,
-    active: bool,
-    notification_list: ListState,
-    subscriptions: Vec<gpui::Subscription>,
-    workspace: WeakEntity<Workspace>,
-    current_notification_toast: Option<(u64, Task<()>)>,
-    local_timezone: UtcOffset,
-    focus_handle: FocusHandle,
-    mark_as_read_tasks: HashMap<u64, Task<Result<()>>>,
-    unseen_notifications: Vec<NotificationEntry>,
-}
-
-#[derive(Debug)]
-pub enum Event {
-    DockPositionChanged,
-    Focus,
-    Dismissed,
-}
-
-pub struct NotificationPresenter {
-    pub actor: Option<Arc<client::User>>,
-    pub text: String,
-    pub icon: &'static str,
-    pub needs_response: bool,
-}
-
-actions!(
-    notification_panel,
-    [
-        /// Toggles the notification panel.
-        Toggle,
-        /// Toggles focus on the notification panel.
-        ToggleFocus
-    ]
-);
-
-pub fn init(cx: &mut App) {
-    cx.observe_new(|workspace: &mut Workspace, _, _| {
-        workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
-            workspace.toggle_panel_focus::<NotificationPanel>(window, cx);
-        });
-        workspace.register_action(|workspace, _: &Toggle, window, cx| {
-            if !workspace.toggle_panel_focus::<NotificationPanel>(window, cx) {
-                workspace.close_panel::<NotificationPanel>(window, cx);
-            }
-        });
-    })
-    .detach();
-}
-
-impl NotificationPanel {
-    pub fn new(
-        workspace: &mut Workspace,
-        window: &mut Window,
-        cx: &mut Context<Workspace>,
-    ) -> Entity<Self> {
-        let fs = workspace.app_state().fs.clone();
-        let client = workspace.app_state().client.clone();
-        let user_store = workspace.app_state().user_store.clone();
-        let workspace_handle = workspace.weak_handle();
-
-        cx.new(|cx| {
-            let mut status = client.status();
-            cx.spawn_in(window, async move |this, cx| {
-                while (status.next().await).is_some() {
-                    if this
-                        .update(cx, |_: &mut Self, cx| {
-                            cx.notify();
-                        })
-                        .is_err()
-                    {
-                        break;
-                    }
-                }
-            })
-            .detach();
-
-            let notification_list = ListState::new(0, ListAlignment::Top, px(1000.));
-            notification_list.set_scroll_handler(cx.listener(
-                |this, event: &ListScrollEvent, _, cx| {
-                    if event.count.saturating_sub(event.visible_range.end) < LOADING_THRESHOLD
-                        && let Some(task) = this
-                            .notification_store
-                            .update(cx, |store, cx| store.load_more_notifications(false, cx))
-                    {
-                        task.detach();
-                    }
-                },
-            ));
-
-            let local_offset = chrono::Local::now().offset().local_minus_utc();
-            let mut this = Self {
-                fs,
-                client,
-                user_store,
-                local_timezone: UtcOffset::from_whole_seconds(local_offset).unwrap(),
-                channel_store: ChannelStore::global(cx),
-                notification_store: NotificationStore::global(cx),
-                notification_list,
-                workspace: workspace_handle,
-                focus_handle: cx.focus_handle(),
-                subscriptions: Default::default(),
-                current_notification_toast: None,
-                active: false,
-                mark_as_read_tasks: Default::default(),
-                unseen_notifications: Default::default(),
-            };
-
-            let mut old_dock_position = this.position(window, cx);
-            this.subscriptions.extend([
-                cx.observe(&this.notification_store, |_, _, cx| cx.notify()),
-                cx.subscribe_in(
-                    &this.notification_store,
-                    window,
-                    Self::on_notification_event,
-                ),
-                cx.observe_global_in::<SettingsStore>(
-                    window,
-                    move |this: &mut Self, window, cx| {
-                        let new_dock_position = this.position(window, cx);
-                        if new_dock_position != old_dock_position {
-                            old_dock_position = new_dock_position;
-                            cx.emit(Event::DockPositionChanged);
-                        }
-                        cx.notify();
-                    },
-                ),
-            ]);
-            this
-        })
-    }
-
-    pub fn load(
-        workspace: WeakEntity<Workspace>,
-        cx: AsyncWindowContext,
-    ) -> Task<Result<Entity<Self>>> {
-        cx.spawn(async move |cx| {
-            workspace.update_in(cx, |workspace, window, cx| Self::new(workspace, window, cx))
-        })
-    }
-
-    fn render_notification(
-        &mut self,
-        ix: usize,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Option<AnyElement> {
-        let entry = self.notification_store.read(cx).notification_at(ix)?;
-        let notification_id = entry.id;
-        let now = OffsetDateTime::now_utc();
-        let timestamp = entry.timestamp;
-        let NotificationPresenter {
-            actor,
-            text,
-            needs_response,
-            ..
-        } = self.present_notification(entry, cx)?;
-
-        let response = entry.response;
-        let notification = entry.notification.clone();
-
-        if self.active && !entry.is_read {
-            self.did_render_notification(notification_id, &notification, window, cx);
-        }
-
-        let relative_timestamp = time_format::format_localized_timestamp(
-            timestamp,
-            now,
-            self.local_timezone,
-            time_format::TimestampFormat::Relative,
-        );
-
-        let absolute_timestamp = time_format::format_localized_timestamp(
-            timestamp,
-            now,
-            self.local_timezone,
-            time_format::TimestampFormat::Absolute,
-        );
-
-        Some(
-            div()
-                .id(ix)
-                .flex()
-                .flex_row()
-                .size_full()
-                .px_2()
-                .py_1()
-                .gap_2()
-                .hover(|style| style.bg(cx.theme().colors().element_hover))
-                .children(actor.map(|actor| {
-                    img(actor.avatar_uri.clone())
-                        .flex_none()
-                        .w_8()
-                        .h_8()
-                        .rounded_full()
-                }))
-                .child(
-                    v_flex()
-                        .gap_1()
-                        .size_full()
-                        .overflow_hidden()
-                        .child(Label::new(text))
-                        .child(
-                            h_flex()
-                                .child(
-                                    div()
-                                        .id("notification_timestamp")
-                                        .hover(|style| {
-                                            style
-                                                .bg(cx.theme().colors().element_selected)
-                                                .rounded_sm()
-                                        })
-                                        .child(Label::new(relative_timestamp).color(Color::Muted))
-                                        .tooltip(move |_, cx| {
-                                            Tooltip::simple(absolute_timestamp.clone(), cx)
-                                        }),
-                                )
-                                .children(if let Some(is_accepted) = response {
-                                    Some(div().flex().flex_grow().justify_end().child(Label::new(
-                                        if is_accepted {
-                                            "You accepted"
-                                        } else {
-                                            "You declined"
-                                        },
-                                    )))
-                                } else if needs_response {
-                                    Some(
-                                        h_flex()
-                                            .flex_grow()
-                                            .justify_end()
-                                            .child(Button::new("decline", "Decline").on_click({
-                                                let notification = notification.clone();
-                                                let entity = cx.entity();
-                                                move |_, _, cx| {
-                                                    entity.update(cx, |this, cx| {
-                                                        this.respond_to_notification(
-                                                            notification.clone(),
-                                                            false,
-                                                            cx,
-                                                        )
-                                                    });
-                                                }
-                                            }))
-                                            .child(Button::new("accept", "Accept").on_click({
-                                                let notification = notification.clone();
-                                                let entity = cx.entity();
-                                                move |_, _, cx| {
-                                                    entity.update(cx, |this, cx| {
-                                                        this.respond_to_notification(
-                                                            notification.clone(),
-                                                            true,
-                                                            cx,
-                                                        )
-                                                    });
-                                                }
-                                            })),
-                                    )
-                                } else {
-                                    None
-                                }),
-                        ),
-                )
-                .into_any(),
-        )
-    }
-
-    fn present_notification(
-        &self,
-        entry: &NotificationEntry,
-        cx: &App,
-    ) -> Option<NotificationPresenter> {
-        let user_store = self.user_store.read(cx);
-        let channel_store = self.channel_store.read(cx);
-        match entry.notification {
-            Notification::ContactRequest { sender_id } => {
-                let requester = user_store.get_cached_user(sender_id)?;
-                Some(NotificationPresenter {
-                    icon: "icons/plus.svg",
-                    text: format!("{} wants to add you as a contact", requester.github_login),
-                    needs_response: user_store.has_incoming_contact_request(requester.id),
-                    actor: Some(requester),
-                })
-            }
-            Notification::ContactRequestAccepted { responder_id } => {
-                let responder = user_store.get_cached_user(responder_id)?;
-                Some(NotificationPresenter {
-                    icon: "icons/plus.svg",
-                    text: format!("{} accepted your contact invite", responder.github_login),
-                    needs_response: false,
-                    actor: Some(responder),
-                })
-            }
-            Notification::ChannelInvitation {
-                ref channel_name,
-                channel_id,
-                inviter_id,
-            } => {
-                let inviter = user_store.get_cached_user(inviter_id)?;
-                Some(NotificationPresenter {
-                    icon: "icons/hash.svg",
-                    text: format!(
-                        "{} invited you to join the #{channel_name} channel",
-                        inviter.github_login
-                    ),
-                    needs_response: channel_store.has_channel_invitation(ChannelId(channel_id)),
-                    actor: Some(inviter),
-                })
-            }
-        }
-    }
-
-    fn did_render_notification(
-        &mut self,
-        notification_id: u64,
-        notification: &Notification,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        let should_mark_as_read = match notification {
-            Notification::ContactRequestAccepted { .. } => true,
-            Notification::ContactRequest { .. } | Notification::ChannelInvitation { .. } => false,
-        };
-
-        if should_mark_as_read {
-            self.mark_as_read_tasks
-                .entry(notification_id)
-                .or_insert_with(|| {
-                    let client = self.client.clone();
-                    cx.spawn_in(window, async move |this, cx| {
-                        cx.background_executor().timer(MARK_AS_READ_DELAY).await;
-                        client
-                            .request(proto::MarkNotificationRead { notification_id })
-                            .await?;
-                        this.update(cx, |this, _| {
-                            this.mark_as_read_tasks.remove(&notification_id);
-                        })?;
-                        Ok(())
-                    })
-                });
-        }
-    }
-
-    fn on_notification_event(
-        &mut self,
-        _: &Entity<NotificationStore>,
-        event: &NotificationEvent,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        match event {
-            NotificationEvent::NewNotification { entry } => {
-                self.unseen_notifications.push(entry.clone());
-                self.add_toast(entry, window, cx);
-            }
-            NotificationEvent::NotificationRemoved { entry }
-            | NotificationEvent::NotificationRead { entry } => {
-                self.unseen_notifications.retain(|n| n.id != entry.id);
-                self.remove_toast(entry.id, cx);
-            }
-            NotificationEvent::NotificationsUpdated {
-                old_range,
-                new_count,
-            } => {
-                self.notification_list.splice(old_range.clone(), *new_count);
-                cx.notify();
-            }
-        }
-    }
-
-    fn add_toast(
-        &mut self,
-        entry: &NotificationEntry,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        let Some(NotificationPresenter { actor, text, .. }) = self.present_notification(entry, cx)
-        else {
-            return;
-        };
-
-        let notification_id = entry.id;
-        self.current_notification_toast = Some((
-            notification_id,
-            cx.spawn_in(window, async move |this, cx| {
-                cx.background_executor().timer(TOAST_DURATION).await;
-                this.update(cx, |this, cx| this.remove_toast(notification_id, cx))
-                    .ok();
-            }),
-        ));
-
-        self.workspace
-            .update(cx, |workspace, cx| {
-                let id = NotificationId::unique::<NotificationToast>();
-
-                workspace.dismiss_notification(&id, cx);
-                workspace.show_notification(id, cx, |cx| {
-                    let workspace = cx.entity().downgrade();
-                    cx.new(|cx| NotificationToast {
-                        actor,
-                        text,
-                        workspace,
-                        focus_handle: cx.focus_handle(),
-                    })
-                })
-            })
-            .ok();
-    }
-
-    fn remove_toast(&mut self, notification_id: u64, cx: &mut Context<Self>) {
-        if let Some((current_id, _)) = &self.current_notification_toast
-            && *current_id == notification_id
-        {
-            self.current_notification_toast.take();
-            self.workspace
-                .update(cx, |workspace, cx| {
-                    let id = NotificationId::unique::<NotificationToast>();
-                    workspace.dismiss_notification(&id, cx)
-                })
-                .ok();
-        }
-    }
-
-    fn respond_to_notification(
-        &mut self,
-        notification: Notification,
-        response: bool,
-
-        cx: &mut Context<Self>,
-    ) {
-        self.notification_store.update(cx, |store, cx| {
-            store.respond_to_notification(notification, response, cx);
-        });
-    }
-}
-
-impl Render for NotificationPanel {
-    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        v_flex()
-            .size_full()
-            .child(
-                h_flex()
-                    .justify_between()
-                    .px_2()
-                    .py_1()
-                    // Match the height of the tab bar so they line up.
-                    .h(Tab::container_height(cx))
-                    .border_b_1()
-                    .border_color(cx.theme().colors().border)
-                    .child(Label::new("Notifications"))
-                    .child(Icon::new(IconName::Envelope)),
-            )
-            .map(|this| {
-                if !self.client.status().borrow().is_connected() {
-                    this.child(
-                        v_flex()
-                            .gap_2()
-                            .p_4()
-                            .child(
-                                Button::new("connect_prompt_button", "Connect")
-                                    .start_icon(Icon::new(IconName::Github).color(Color::Muted))
-                                    .style(ButtonStyle::Filled)
-                                    .full_width()
-                                    .on_click({
-                                        let client = self.client.clone();
-                                        move |_, window, cx| {
-                                            let client = client.clone();
-                                            window
-                                                .spawn(cx, async move |cx| {
-                                                    match client.connect(true, cx).await {
-                                                        util::ConnectionResult::Timeout => {
-                                                            log::error!("Connection timeout");
-                                                        }
-                                                        util::ConnectionResult::ConnectionReset => {
-                                                            log::error!("Connection reset");
-                                                        }
-                                                        util::ConnectionResult::Result(r) => {
-                                                            r.log_err();
-                                                        }
-                                                    }
-                                                })
-                                                .detach()
-                                        }
-                                    }),
-                            )
-                            .child(
-                                div().flex().w_full().items_center().child(
-                                    Label::new("Connect to view notifications.")
-                                        .color(Color::Muted)
-                                        .size(LabelSize::Small),
-                                ),
-                            ),
-                    )
-                } else if self.notification_list.item_count() == 0 {
-                    this.child(
-                        v_flex().p_4().child(
-                            div().flex().w_full().items_center().child(
-                                Label::new("You have no notifications.")
-                                    .color(Color::Muted)
-                                    .size(LabelSize::Small),
-                            ),
-                        ),
-                    )
-                } else {
-                    this.child(
-                        list(
-                            self.notification_list.clone(),
-                            cx.processor(|this, ix, window, cx| {
-                                this.render_notification(ix, window, cx)
-                                    .unwrap_or_else(|| div().into_any())
-                            }),
-                        )
-                        .size_full(),
-                    )
-                }
-            })
-    }
-}
-
-impl Focusable for NotificationPanel {
-    fn focus_handle(&self, _: &App) -> FocusHandle {
-        self.focus_handle.clone()
-    }
-}
-
-impl EventEmitter<Event> for NotificationPanel {}
-impl EventEmitter<PanelEvent> for NotificationPanel {}
-
-impl Panel for NotificationPanel {
-    fn persistent_name() -> &'static str {
-        "NotificationPanel"
-    }
-
-    fn panel_key() -> &'static str {
-        NOTIFICATION_PANEL_KEY
-    }
-
-    fn position(&self, _: &Window, cx: &App) -> DockPosition {
-        NotificationPanelSettings::get_global(cx).dock
-    }
-
-    fn position_is_valid(&self, position: DockPosition) -> bool {
-        matches!(position, DockPosition::Left | DockPosition::Right)
-    }
-
-    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
-        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
-            settings.notification_panel.get_or_insert_default().dock = Some(position.into())
-        });
-    }
-
-    fn default_size(&self, _: &Window, cx: &App) -> Pixels {
-        NotificationPanelSettings::get_global(cx).default_width
-    }
-
-    fn set_active(&mut self, active: bool, _: &mut Window, cx: &mut Context<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, _: &Window, cx: &App) -> Option<IconName> {
-        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::BellDot)
-    }
-
-    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
-        Some("Notification Panel")
-    }
-
-    fn icon_label(&self, _window: &Window, cx: &App) -> Option<String> {
-        if !NotificationPanelSettings::get_global(cx).show_count_badge {
-            return None;
-        }
-        let count = self.notification_store.read(cx).unread_notification_count();
-        if count == 0 {
-            None
-        } else {
-            Some(count.to_string())
-        }
-    }
-
-    fn toggle_action(&self) -> Box<dyn gpui::Action> {
-        Box::new(ToggleFocus)
-    }
-
-    fn activation_priority(&self) -> u32 {
-        4
-    }
-}
-
-pub struct NotificationToast {
-    actor: Option<Arc<User>>,
-    text: String,
-    workspace: WeakEntity<Workspace>,
-    focus_handle: FocusHandle,
-}
-
-impl Focusable for NotificationToast {
-    fn focus_handle(&self, _cx: &App) -> FocusHandle {
-        self.focus_handle.clone()
-    }
-}
-
-impl WorkspaceNotification for NotificationToast {}
-
-impl NotificationToast {
-    fn focus_notification_panel(&self, window: &mut Window, cx: &mut Context<Self>) {
-        let workspace = self.workspace.clone();
-        window.defer(cx, move |window, cx| {
-            workspace
-                .update(cx, |workspace, cx| {
-                    workspace.focus_panel::<NotificationPanel>(window, cx)
-                })
-                .ok();
-        })
-    }
-}
-
-impl Render for NotificationToast {
-    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        let user = self.actor.clone();
-
-        let suppress = window.modifiers().shift;
-        let (close_id, close_icon) = if suppress {
-            ("suppress", IconName::Minimize)
-        } else {
-            ("close", IconName::Close)
-        };
-
-        h_flex()
-            .id("notification_panel_toast")
-            .elevation_3(cx)
-            .p_2()
-            .justify_between()
-            .children(user.map(|user| Avatar::new(user.avatar_uri.clone())))
-            .child(Label::new(self.text.clone()))
-            .on_modifiers_changed(cx.listener(|_, _, _, cx| cx.notify()))
-            .child(
-                IconButton::new(close_id, close_icon)
-                    .tooltip(move |_window, cx| {
-                        if suppress {
-                            Tooltip::for_action(
-                                "Suppress.\nClose with click.",
-                                &workspace::SuppressNotification,
-                                cx,
-                            )
-                        } else {
-                            Tooltip::for_action(
-                                "Close.\nSuppress with shift-click",
-                                &menu::Cancel,
-                                cx,
-                            )
-                        }
-                    })
-                    .on_click(cx.listener(move |_, _: &ClickEvent, _, cx| {
-                        if suppress {
-                            cx.emit(SuppressEvent);
-                        } else {
-                            cx.emit(DismissEvent);
-                        }
-                    })),
-            )
-            .on_click(cx.listener(|this, _, window, cx| {
-                this.focus_notification_panel(window, cx);
-                cx.emit(DismissEvent);
-            }))
-    }
-}
-
-impl EventEmitter<DismissEvent> for NotificationToast {}
-impl EventEmitter<SuppressEvent> for NotificationToast {}

crates/collab_ui/src/panel_settings.rs 🔗

@@ -10,14 +10,6 @@ pub struct CollaborationPanelSettings {
     pub default_width: Pixels,
 }
 
-#[derive(Debug, RegisterSetting)]
-pub struct NotificationPanelSettings {
-    pub button: bool,
-    pub dock: DockPosition,
-    pub default_width: Pixels,
-    pub show_count_badge: bool,
-}
-
 impl Settings for CollaborationPanelSettings {
     fn from_settings(content: &settings::SettingsContent) -> Self {
         let panel = content.collaboration_panel.as_ref().unwrap();
@@ -29,15 +21,3 @@ impl Settings for CollaborationPanelSettings {
         }
     }
 }
-
-impl Settings for NotificationPanelSettings {
-    fn from_settings(content: &settings::SettingsContent) -> Self {
-        let panel = content.notification_panel.as_ref().unwrap();
-        return Self {
-            button: panel.button.unwrap(),
-            dock: panel.dock.unwrap().into(),
-            default_width: panel.default_width.map(px).unwrap(),
-            show_count_badge: panel.show_count_badge.unwrap(),
-        };
-    }
-}

crates/settings/src/vscode_import.rs 🔗

@@ -198,7 +198,7 @@ impl VsCodeSettings {
             log: None,
             message_editor: None,
             node: self.node_binary_settings(),
-            notification_panel: None,
+
             outline_panel: self.outline_panel_settings_content(),
             preview_tabs: self.preview_tabs_settings_content(),
             project: self.project_settings_content(),

crates/settings_content/src/settings_content.rs 🔗

@@ -174,9 +174,6 @@ pub struct SettingsContent {
     /// Configuration for Node-related features
     pub node: Option<NodeBinarySettings>,
 
-    /// Configuration for the Notification Panel
-    pub notification_panel: Option<NotificationPanelSettingsContent>,
-
     pub proxy: Option<String>,
 
     /// The URL of the Zed server to connect to.
@@ -631,28 +628,6 @@ pub struct ScrollbarSettings {
     pub show: Option<ShowScrollbar>,
 }
 
-#[with_fallible_options]
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq)]
-pub struct NotificationPanelSettingsContent {
-    /// Whether to show the panel button in the status bar.
-    ///
-    /// Default: true
-    pub button: Option<bool>,
-    /// Where to dock the panel.
-    ///
-    /// Default: right
-    pub dock: Option<DockPosition>,
-    /// Default width of the panel in pixels.
-    ///
-    /// Default: 300
-    #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
-    pub default_width: Option<f32>,
-    /// Whether to show a badge on the notification panel icon with the count of unread notifications.
-    ///
-    /// Default: false
-    pub show_count_badge: Option<bool>,
-}
-
 #[with_fallible_options]
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq)]
 pub struct PanelSettingsContent {

crates/settings_ui/src/page_data.rs 🔗

@@ -5579,96 +5579,6 @@ fn panels_page() -> SettingsPage {
         ]
     }
 
-    fn notification_panel_section() -> [SettingsPageItem; 5] {
-        [
-            SettingsPageItem::SectionHeader("Notification Panel"),
-            SettingsPageItem::SettingItem(SettingItem {
-                title: "Notification Panel Button",
-                description: "Show the notification panel button in the status bar.",
-                field: Box::new(SettingField {
-                    json_path: Some("notification_panel.button"),
-                    pick: |settings_content| {
-                        settings_content
-                            .notification_panel
-                            .as_ref()?
-                            .button
-                            .as_ref()
-                    },
-                    write: |settings_content, value| {
-                        settings_content
-                            .notification_panel
-                            .get_or_insert_default()
-                            .button = value;
-                    },
-                }),
-                metadata: None,
-                files: USER,
-            }),
-            SettingsPageItem::SettingItem(SettingItem {
-                title: "Notification Panel Dock",
-                description: "Where to dock the notification panel.",
-                field: Box::new(SettingField {
-                    json_path: Some("notification_panel.dock"),
-                    pick: |settings_content| {
-                        settings_content.notification_panel.as_ref()?.dock.as_ref()
-                    },
-                    write: |settings_content, value| {
-                        settings_content
-                            .notification_panel
-                            .get_or_insert_default()
-                            .dock = value;
-                    },
-                }),
-                metadata: None,
-                files: USER,
-            }),
-            SettingsPageItem::SettingItem(SettingItem {
-                title: "Notification Panel Default Width",
-                description: "Default width of the notification panel in pixels.",
-                field: Box::new(SettingField {
-                    json_path: Some("notification_panel.default_width"),
-                    pick: |settings_content| {
-                        settings_content
-                            .notification_panel
-                            .as_ref()?
-                            .default_width
-                            .as_ref()
-                    },
-                    write: |settings_content, value| {
-                        settings_content
-                            .notification_panel
-                            .get_or_insert_default()
-                            .default_width = value;
-                    },
-                }),
-                metadata: None,
-                files: USER,
-            }),
-            SettingsPageItem::SettingItem(SettingItem {
-                title: "Show Count Badge",
-                description: "Show a badge on the notification panel icon with the count of unread notifications.",
-                field: Box::new(SettingField {
-                    json_path: Some("notification_panel.show_count_badge"),
-                    pick: |settings_content| {
-                        settings_content
-                            .notification_panel
-                            .as_ref()?
-                            .show_count_badge
-                            .as_ref()
-                    },
-                    write: |settings_content, value| {
-                        settings_content
-                            .notification_panel
-                            .get_or_insert_default()
-                            .show_count_badge = value;
-                    },
-                }),
-                metadata: None,
-                files: USER,
-            }),
-        ]
-    }
-
     fn collaboration_panel_section() -> [SettingsPageItem; 4] {
         [
             SettingsPageItem::SectionHeader("Collaboration Panel"),
@@ -5841,7 +5751,6 @@ fn panels_page() -> SettingsPage {
             outline_panel_section(),
             git_panel_section(),
             debugger_panel_section(),
-            notification_panel_section(),
             collaboration_panel_section(),
             agent_panel_section(),
         ],

crates/ui/src/components/collab/collab_notification.rs 🔗

@@ -67,7 +67,7 @@ impl Component for CollabNotification {
         let avatar = "https://avatars.githubusercontent.com/u/67129314?v=4";
         let container = || div().h(px(72.)).w(px(400.)); // Size of the actual notification window
 
-        let examples = vec![
+        let call_examples = vec![
             single_example(
                 "Incoming Call",
                 container()
@@ -129,6 +129,58 @@ impl Component for CollabNotification {
             ),
         ];
 
-        Some(example_group(examples).vertical().into_any_element())
+        let toast_examples = vec![
+            single_example(
+                "Contact Request",
+                container()
+                    .child(
+                        CollabNotification::new(
+                            avatar,
+                            Button::new("accept", "Accept"),
+                            Button::new("decline", "Decline"),
+                        )
+                        .child(Label::new("maxbrunsfeld wants to add you as a contact")),
+                    )
+                    .into_any_element(),
+            ),
+            single_example(
+                "Contact Request Accepted",
+                container()
+                    .child(
+                        CollabNotification::new(
+                            avatar,
+                            Button::new("dismiss", "Dismiss"),
+                            Button::new("close", "Close"),
+                        )
+                        .child(Label::new("maxbrunsfeld accepted your contact request")),
+                    )
+                    .into_any_element(),
+            ),
+            single_example(
+                "Channel Invitation",
+                container()
+                    .child(
+                        CollabNotification::new(
+                            avatar,
+                            Button::new("accept", "Accept"),
+                            Button::new("decline", "Decline"),
+                        )
+                        .child(Label::new(
+                            "maxbrunsfeld invited you to join the #zed channel",
+                        )),
+                    )
+                    .into_any_element(),
+            ),
+        ];
+
+        Some(
+            v_flex()
+                .gap_6()
+                .child(example_group_with_title("Calls & Projects", call_examples).vertical())
+                .child(
+                    example_group_with_title("Contact & Channel Toasts", toast_examples).vertical(),
+                )
+                .into_any_element(),
+        )
     }
 }

crates/vim/src/command.rs 🔗

@@ -1782,7 +1782,6 @@ fn generate_commands(_: &App) -> Vec<VimCommand> {
         VimCommand::str(("te", "rm"), "terminal_panel::Toggle"),
         VimCommand::str(("T", "erm"), "terminal_panel::Toggle"),
         VimCommand::str(("C", "ollab"), "collab_panel::ToggleFocus"),
-        VimCommand::str(("No", "tifications"), "notification_panel::ToggleFocus"),
         VimCommand::str(("A", "I"), "agent::ToggleFocus"),
         VimCommand::str(("G", "it"), "git_panel::ToggleFocus"),
         VimCommand::str(("D", "ebug"), "debug_panel::ToggleFocus"),

crates/zed/src/zed.rs 🔗

@@ -652,10 +652,6 @@ fn initialize_panels(window: &mut Window, cx: &mut Context<Workspace>) -> Task<a
         let git_panel = GitPanel::load(workspace_handle.clone(), cx.clone());
         let channels_panel =
             collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
-        let notification_panel = collab_ui::notification_panel::NotificationPanel::load(
-            workspace_handle.clone(),
-            cx.clone(),
-        );
         let debug_panel = DebugPanel::load(workspace_handle.clone(), cx);
 
         async fn add_panel_when_ready(
@@ -679,7 +675,6 @@ fn initialize_panels(window: &mut Window, cx: &mut Context<Workspace>) -> Task<a
             add_panel_when_ready(terminal_panel, workspace_handle.clone(), cx.clone()),
             add_panel_when_ready(git_panel, workspace_handle.clone(), cx.clone()),
             add_panel_when_ready(channels_panel, workspace_handle.clone(), cx.clone()),
-            add_panel_when_ready(notification_panel, workspace_handle.clone(), cx.clone()),
             add_panel_when_ready(debug_panel, workspace_handle.clone(), cx.clone()),
             initialize_agent_panel(workspace_handle, cx.clone()).map(|r| r.log_err()),
         );
@@ -1037,16 +1032,6 @@ fn register_actions(
                 workspace.toggle_panel_focus::<collab_ui::collab_panel::CollabPanel>(window, cx);
             },
         )
-        .register_action(
-            |workspace: &mut Workspace,
-             _: &collab_ui::notification_panel::ToggleFocus,
-             window: &mut Window,
-             cx: &mut Context<Workspace>| {
-                workspace.toggle_panel_focus::<collab_ui::notification_panel::NotificationPanel>(
-                    window, cx,
-                );
-            },
-        )
         .register_action(
             |workspace: &mut Workspace,
              _: &terminal_panel::ToggleFocus,
@@ -4962,7 +4947,6 @@ mod tests {
                 "multi_workspace",
                 "new_process_modal",
                 "notebook",
-                "notification_panel",
                 "onboarding",
                 "outline",
                 "outline_panel",

docs/src/visual-customization.md 🔗

@@ -105,7 +105,7 @@ To disable this behavior use:
   // "outline_panel": {"button": false },
   // "collaboration_panel": {"button": false },
   // "git_panel": {"button": false },
-  // "notification_panel": {"button": false },
+
   // "agent": {"button": false },
   // "debugger": {"button": false },
   // "diagnostics": {"button": false },
@@ -588,16 +588,6 @@ See [Terminal settings](./reference/all-settings.md#terminal) for additional non
     "dock": "left", // Where to dock: left, right
     "default_width": 240 // Default width of the collaboration panel.
   },
-  "show_call_status_icon": true, // Shown call status in the OS status bar.
-
-  // Notification Panel
-  "notification_panel": {
-    // Whether to show the notification panel button in the status bar.
-    "button": true,
-    // Where to dock the notification panel. Can be 'left' or 'right'.
-    "dock": "right",
-    // Default width of the notification panel.
-    "default_width": 380
-  }
+  "show_call_status_icon": true // Shown call status in the OS status bar.
 }
 ```