Detailed changes
@@ -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",
@@ -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,
@@ -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);
@@ -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);
}
});
});
@@ -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
@@ -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(§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<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(¬ification_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> {
@@ -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);
}
@@ -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, ¬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<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(¬ification_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 {}
@@ -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(),
- };
- }
-}
@@ -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(),
@@ -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 {
@@ -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(),
],
@@ -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(),
+ )
}
}
@@ -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"),
@@ -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",
@@ -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.
}
```