From c5845ec04cb9deaf8712501a53d8d7f284011e4f Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Tue, 7 Apr 2026 12:12:02 -0400 Subject: [PATCH] Remove notification panel (#50204) 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 --- 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 +- .../settings_content/src/settings_content.rs | 25 - crates/settings_ui/src/page_data.rs | 91 --- .../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(-) delete mode 100644 crates/collab_ui/src/notification_panel.rs diff --git a/Cargo.lock b/Cargo.lock index 3fccd850ae697925330d15ed6b72804c39f4795e..f426d0da3392240300d15ca174013a6bdbdbb31d 100644 --- a/Cargo.lock +++ b/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", diff --git a/assets/settings/default.json b/assets/settings/default.json index a32e1b27aee08bf2676922fea3790a99b7d7844b..97fbcd546e09beefa9ff7a67e33806f3faf561d1 100644 --- a/assets/settings/default.json +++ b/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, diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index 5d6dca9322482daecf7525f79ead63b4471b7a53..a04de2ed3be69d3f5791419a32e427fa0c26791e 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -31,7 +31,6 @@ pub struct PanelLayout { pub(crate) outline_panel_dock: Option, pub(crate) collaboration_panel_dock: Option, pub(crate) git_panel_dock: Option, - pub(crate) notification_panel_button: Option, } 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); diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 58b52d9ea2eb10a4f7f483402b98c4be4b08924f..429bc184f5d889990599c196910ae8d0feb28da1 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/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); } }); }); diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index efcba05456955e308e5a00e938bf3092d894efeb..920f620e0ea2d48f514c5e0af598add193f80d98 100644 --- a/crates/collab_ui/Cargo.toml +++ b/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 diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 327ef1cf6003eb959bd0926d67d2b0ed3b4ab0ba..1cff27ac6b2f3c61f7a90c4a9ca6749d4b1e48b7 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/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, filter_occupied_channels: bool, workspace: WeakEntity, + notification_store: Entity, + current_notification_toast: Option<(u64, Task<()>)>, + mark_as_read_tasks: HashMap>>, } #[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, ) -> 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(§ion); + 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, + event: &NotificationEvent, + _window: &mut Window, + cx: &mut Context, + ) { + 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>, 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) { + 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::(); + + 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) { + 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(¬ification_id); + })?; + + request_result?; + Ok(()) + }) + }); + } + + fn mark_contact_request_accepted_notifications_read( + &mut self, + contact_user_id: u64, + cx: &mut Context, + ) { + 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::>() + }); + + for notification_id in notification_ids { + self.mark_notification_read(notification_id, cx); + } + } + + fn remove_toast(&mut self, notification_id: u64, cx: &mut Context) { + 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.current_notification_toast.take(); + self.workspace + .update(cx, |workspace, cx| { + let id = NotificationId::unique::(); + 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) { + 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::(); + workspace.dismiss_notification(&id, cx) + }) + .ok(); + }); + } + } + fn icon(&self, _window: &Window, cx: &App) -> Option { CollaborationPanelSettings::get_global(cx) .button .then_some(ui::IconName::UserGroup) } + fn icon_label(&self, _window: &Window, cx: &App) -> Option { + 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>, + text: String, + notification: Option, + workspace: WeakEntity, + collab_panel: WeakEntity, + 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) { + let workspace = self.workspace.clone(); + window.defer(cx, move |window, cx| { + workspace + .update(cx, |workspace, cx| { + workspace.focus_panel::(window, cx) + }) + .ok(); + }) + } + + fn respond(&mut self, accept: bool, window: &mut Window, cx: &mut Context) { + 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) -> 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 for CollabNotificationToast {} +impl EventEmitter for CollabNotificationToast {} + #[cfg(any(test, feature = "test-support"))] impl CollabPanel { pub fn entries_as_strings(&self) -> Vec { diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 107b2ffa7f625d98dd9c54bb6bbf75df8b72d020..f9c463c0690343a3b4b1b9a048134265326a9f50 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/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, 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); } diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs deleted file mode 100644 index d7fef4873c687ab23a25b3144ba902cf4c42c137..0000000000000000000000000000000000000000 --- a/crates/collab_ui/src/notification_panel.rs +++ /dev/null @@ -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, - user_store: Entity, - channel_store: Entity, - notification_store: Entity, - fs: Arc, - active: bool, - notification_list: ListState, - subscriptions: Vec, - workspace: WeakEntity, - current_notification_toast: Option<(u64, Task<()>)>, - local_timezone: UtcOffset, - focus_handle: FocusHandle, - mark_as_read_tasks: HashMap>>, - unseen_notifications: Vec, -} - -#[derive(Debug)] -pub enum Event { - DockPositionChanged, - Focus, - Dismissed, -} - -pub struct NotificationPresenter { - pub actor: Option>, - 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::(window, cx); - }); - workspace.register_action(|workspace, _: &Toggle, window, cx| { - if !workspace.toggle_panel_focus::(window, cx) { - workspace.close_panel::(window, cx); - } - }); - }) - .detach(); -} - -impl NotificationPanel { - pub fn new( - workspace: &mut Workspace, - window: &mut Window, - cx: &mut Context, - ) -> Entity { - 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::( - 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, - cx: AsyncWindowContext, - ) -> Task>> { - 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, - ) -> Option { - 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, ¬ification, 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 { - 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, - ) { - 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(¬ification_id); - })?; - Ok(()) - }) - }); - } - } - - fn on_notification_event( - &mut self, - _: &Entity, - event: &NotificationEvent, - window: &mut Window, - cx: &mut Context, - ) { - 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, - ) { - 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::(); - - 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) { - 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::(); - workspace.dismiss_notification(&id, cx) - }) - .ok(); - } - } - - fn respond_to_notification( - &mut self, - notification: Notification, - response: bool, - - cx: &mut Context, - ) { - 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) -> 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 for NotificationPanel {} -impl EventEmitter 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) { - 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.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 { - 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 { - 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 { - Box::new(ToggleFocus) - } - - fn activation_priority(&self) -> u32 { - 4 - } -} - -pub struct NotificationToast { - actor: Option>, - text: String, - workspace: WeakEntity, - 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) { - let workspace = self.workspace.clone(); - window.defer(cx, move |window, cx| { - workspace - .update(cx, |workspace, cx| { - workspace.focus_panel::(window, cx) - }) - .ok(); - }) - } -} - -impl Render for NotificationToast { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> 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 for NotificationToast {} -impl EventEmitter for NotificationToast {} diff --git a/crates/collab_ui/src/panel_settings.rs b/crates/collab_ui/src/panel_settings.rs index 938d33159e9adb7a9e63ceb73219b70724efee17..3d6de1015a3751751c13c8ccb6d4c5639755be20 100644 --- a/crates/collab_ui/src/panel_settings.rs +++ b/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(), - }; - } -} diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index 1211cbd8a4519ea295773eb0d979b48258908311..4c7ce085aed5ad0cf7c48308b4211815cf5aad75 100644 --- a/crates/settings/src/vscode_import.rs +++ b/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(), diff --git a/crates/settings_content/src/settings_content.rs b/crates/settings_content/src/settings_content.rs index 6c60a7010f7cfc5b4fadf9a8cc386fe6e3267abc..3c3c0f600769b8437dc56016426eee4f84d2fc7a 100644 --- a/crates/settings_content/src/settings_content.rs +++ b/crates/settings_content/src/settings_content.rs @@ -174,9 +174,6 @@ pub struct SettingsContent { /// Configuration for Node-related features pub node: Option, - /// Configuration for the Notification Panel - pub notification_panel: Option, - pub proxy: Option, /// The URL of the Zed server to connect to. @@ -631,28 +628,6 @@ pub struct ScrollbarSettings { pub show: Option, } -#[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, - /// Where to dock the panel. - /// - /// Default: right - pub dock: Option, - /// Default width of the panel in pixels. - /// - /// Default: 300 - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub default_width: Option, - /// Whether to show a badge on the notification panel icon with the count of unread notifications. - /// - /// Default: false - pub show_count_badge: Option, -} - #[with_fallible_options] #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq)] pub struct PanelSettingsContent { diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index 259ee2cf261f9e435a5431ddf3c470640daf41f9..c77bf5a326c6b48dea2c85f0744de0066d8c0236 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/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(), ], diff --git a/crates/ui/src/components/collab/collab_notification.rs b/crates/ui/src/components/collab/collab_notification.rs index 0c3fca84e9b9fb3246de20b9b1f077202fa3ebdb..28d28b0a292076a575a5443b80eae9b788e2b62e 100644 --- a/crates/ui/src/components/collab/collab_notification.rs +++ b/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(), + ) } } diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index fd19a5dc400a24b9f27617c44bd71fe38073c757..06fa6ead775809c3df775d959fb080a93ee84aad 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -1782,7 +1782,6 @@ fn generate_commands(_: &App) -> Vec { 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"), diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 03e128415e1aa8390d1b95816755d3644064dada..293125c0089e0a4315eb9c28f30be5f840bd6052 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -652,10 +652,6 @@ fn initialize_panels(window: &mut Window, cx: &mut Context) -> Task) -> Task(window, cx); }, ) - .register_action( - |workspace: &mut Workspace, - _: &collab_ui::notification_panel::ToggleFocus, - window: &mut Window, - cx: &mut Context| { - workspace.toggle_panel_focus::( - 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", diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md index 3c285bc3d10fc3bcb5fba6f735304ede438104a3..7597cdac293dd842b6a6a9f5747551a6f172bbf3 100644 --- a/docs/src/visual-customization.md +++ b/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. } ```