Implement Notifications

Nate Butler and Marshall Bowers created

Co-Authored-By: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com>

Change summary

crates/ui2/src/components/notifications_panel.rs | 153 ++++++-----------
crates/ui2/src/elements/icon.rs                  |   2 
crates/ui2/src/prelude.rs                        |  18 ++
crates/ui2/src/static_data.rs                    |  37 +++
4 files changed, 110 insertions(+), 100 deletions(-)

Detailed changes

crates/ui2/src/components/notifications_panel.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
     h_stack, prelude::*, static_new_notification_items, v_stack, Avatar, Button, Icon, IconButton,
-    IconElement, Label, LabelColor, LineHeightStyle, ListHeaderMeta, ListSeparator, Stack,
+    IconElement, Label, LabelColor, LineHeightStyle, ListHeaderMeta, ListSeparator,
     UnreadIndicator,
 };
 use crate::{ClickHandler, ListHeader};
@@ -67,6 +67,18 @@ pub enum ButtonOrIconButton<V: 'static> {
     IconButton(IconButton<V>),
 }
 
+impl<V: 'static> From<Button<V>> for ButtonOrIconButton<V> {
+    fn from(value: Button<V>) -> Self {
+        Self::Button(value)
+    }
+}
+
+impl<V: 'static> From<IconButton<V>> for ButtonOrIconButton<V> {
+    fn from(value: IconButton<V>) -> Self {
+        Self::IconButton(value)
+    }
+}
+
 pub struct NotificationAction<V: 'static> {
     button: ButtonOrIconButton<V>,
     tooltip: SharedString,
@@ -80,19 +92,25 @@ pub struct NotificationAction<V: 'static> {
     taken_message: (Option<Icon>, SharedString),
 }
 
+impl<V: 'static> NotificationAction<V> {
+    pub fn new(
+        button: impl Into<ButtonOrIconButton<V>>,
+        tooltip: impl Into<SharedString>,
+        (icon, taken_message): (Option<Icon>, impl Into<SharedString>),
+    ) -> Self {
+        Self {
+            button: button.into(),
+            tooltip: tooltip.into(),
+            taken_message: (icon, taken_message.into()),
+        }
+    }
+}
+
 pub struct NotificationWithActions<V: 'static> {
     notification: Notification<V>,
     actions: [NotificationAction<V>; 2],
 }
 
-/// Represents a person with a Zed account's public profile.
-/// All data in this struct should be considered public.
-pub struct PublicActor {
-    username: SharedString,
-    avatar: SharedString,
-    is_contact: bool,
-}
-
 pub enum ActorOrIcon {
     Actor(PublicActor),
     Icon(Icon),
@@ -162,13 +180,13 @@ impl<V> Notification<V> {
     /// Requires a click action.
     pub fn new_actor_message(
         id: impl Into<ElementId>,
-        message: SharedString,
+        message: impl Into<SharedString>,
         actor: PublicActor,
         click_action: ClickHandler<V>,
     ) -> Self {
         Self::new(
             id.into(),
-            message,
+            message.into(),
             ActorOrIcon::Actor(actor),
             Some(click_action),
         )
@@ -179,13 +197,13 @@ impl<V> Notification<V> {
     /// Requires a click action.
     pub fn new_icon_message(
         id: impl Into<ElementId>,
-        message: SharedString,
+        message: impl Into<SharedString>,
         icon: Icon,
         click_action: ClickHandler<V>,
     ) -> Self {
         Self::new(
             id.into(),
-            message,
+            message.into(),
             ActorOrIcon::Icon(icon),
             Some(click_action),
         )
@@ -197,12 +215,11 @@ impl<V> Notification<V> {
     /// Cannot take a click action due to required actions.
     pub fn new_actor_with_actions(
         id: impl Into<ElementId>,
-        message: SharedString,
+        message: impl Into<SharedString>,
         actor: PublicActor,
-        click_action: ClickHandler<V>,
         actions: [NotificationAction<V>; 2],
     ) -> Self {
-        Self::new(id.into(), message, ActorOrIcon::Actor(actor), None).actions(actions)
+        Self::new(id.into(), message.into(), ActorOrIcon::Actor(actor), None).actions(actions)
     }
 
     /// Creates a new notification with an icon slot
@@ -211,12 +228,11 @@ impl<V> Notification<V> {
     /// Cannot take a click action due to required actions.
     pub fn new_icon_with_actions(
         id: impl Into<ElementId>,
-        message: SharedString,
+        message: impl Into<SharedString>,
         icon: Icon,
-        click_action: ClickHandler<V>,
         actions: [NotificationAction<V>; 2],
     ) -> Self {
-        Self::new(id.into(), message, ActorOrIcon::Icon(icon), None).actions(actions)
+        Self::new(id.into(), message.into(), ActorOrIcon::Icon(icon), None).actions(actions)
     }
 
     fn on_click(mut self, handler: ClickHandler<V>) -> Self {
@@ -253,45 +269,6 @@ impl<V> Notification<V> {
         }
     }
 
-    fn render_actions(&self, cx: &mut ViewContext<V>) -> impl Component<V> {
-        // match (&self.actions, &self.action_taken) {
-        //     // Show nothing
-        //     (None, _) => div(),
-        //     // Show the taken_message
-        //     (Some(_), Some(action_taken)) => h_stack()
-        //         .children(
-        //             action_taken
-        //                 .taken_message
-        //                 .0
-        //                 .map(|icon| IconElement::new(icon).color(crate::IconColor::Muted)),
-        //         )
-        //         .child(Label::new(action_taken.taken_message.1.clone()).color(LabelColor::Muted)),
-        //     // Show the actions
-        //     (Some(actions), None) => h_stack()
-        //         .children(actions.iter().map(actiona.ction.tton     Component::render(button),Component::render(icon_button))),
-        //         }))
-        //         .collect::<Vec<_>>(),
-        }
-
-        // if let Some(actions) = &self.actions {
-        //     let action_children = actions
-        //         .iter()
-        //         .map(|action| match &action.button {
-        //             ButtonOrIconButton::Button(button) => {
-        //                 div().class("action_button").child(button.label.clone())
-        //             }
-        //             ButtonOrIconButton::IconButton(icon_button) => div()
-        //                 .class("action_icon_button")
-        //                 .child(icon_button.icon.to_string()),
-        //         })
-        //         .collect::<Vec<_>>();
-
-        //     el = el.child(h_stack().children(action_children));
-        // } else {
-        //     el = el.child(h_stack().child(div()));
-        // }
-    }
-
     fn render_slot(&self, cx: &mut ViewContext<V>) -> impl Component<V> {
         match &self.slot {
             ActorOrIcon::Actor(actor) => Avatar::new(actor.avatar.clone()).render(),
@@ -336,44 +313,30 @@ impl<V> Notification<V> {
                                     )
                                     .child(self.render_meta_items(cx)),
                             )
-                            .child(
-
-                                match (self.actions, self.action_taken) {
-                                    // Show nothing
-                                        (None, _) => div(),
-                                        // Show the taken_message
-                                            (Some(_), Some(action_taken)) => h_stack()
-                                                .children(
-                                                    action_taken
-                                                        .taken_message
-                                                        .0
-                                                        .map(|icon| IconElement::new(icon).color(crate::IconColor::Muted)),
-                                                )
-                                                .child(Label::new(action_taken.taken_message.1.clone()).color(LabelColor::Muted)),
-                                            // Show the actions
-                                            (Some(actions), None) => h_stack()
-
+                            .child(match (self.actions, self.action_taken) {
+                                // Show nothing
+                                (None, _) => div(),
+                                // Show the taken_message
+                                (Some(_), Some(action_taken)) => h_stack()
+                                    .children(action_taken.taken_message.0.map(|icon| {
+                                        IconElement::new(icon).color(crate::IconColor::Muted)
+                                    }))
+                                    .child(
+                                        Label::new(action_taken.taken_message.1.clone())
+                                            .color(LabelColor::Muted),
+                                    ),
+                                // Show the actions
+                                (Some(actions), None) => {
+                                    h_stack().children(actions.map(|action| match action.button {
+                                        ButtonOrIconButton::Button(button) => {
+                                            Component::render(button)
+                                        }
+                                        ButtonOrIconButton::IconButton(icon_button) => {
+                                            Component::render(icon_button)
+                                        }
+                                    }))
                                 }
-
-                                // match (&self.actions, &self.action_taken) {
-                                //     // Show nothing
-                                //     (None, _) => div(),
-                                //     // Show the taken_message
-                                //     (Some(_), Some(action_taken)) => h_stack()
-                                //         .children(
-                                //             action_taken
-                                //                 .taken_message
-                                //                 .0
-                                //                 .map(|icon| IconElement::new(icon).color(crate::IconColor::Muted)),
-                                //         )
-                                //         .child(Label::new(action_taken.taken_message.1.clone()).color(LabelColor::Muted)),
-                                //     // Show the actions
-                                //     (Some(actions), None) => h_stack()
-                                //         .children(actions.iter().map(actiona.ction.tton     Component::render(button),Component::render(icon_button))),
-                                //         }))
-                                //         .collect::<Vec<_>>(),
-
-                            ),
+                            }),
                     ),
             )
     }

crates/ui2/src/elements/icon.rs 🔗

@@ -49,6 +49,7 @@ pub enum Icon {
     AudioOff,
     AudioOn,
     Bolt,
+    Check,
     ChevronDown,
     ChevronLeft,
     ChevronRight,
@@ -105,6 +106,7 @@ impl Icon {
             Icon::AudioOff => "icons/speaker-off.svg",
             Icon::AudioOn => "icons/speaker-loud.svg",
             Icon::Bolt => "icons/bolt.svg",
+            Icon::Check => "icons/check.svg",
             Icon::ChevronDown => "icons/chevron_down.svg",
             Icon::ChevronLeft => "icons/chevron_left.svg",
             Icon::ChevronRight => "icons/chevron_right.svg",

crates/ui2/src/prelude.rs 🔗

@@ -19,6 +19,24 @@ pub fn ui_size(cx: &mut WindowContext, size: f32) -> Rems {
     rems(*settings.ui_scale * UI_SCALE_RATIO * size)
 }
 
+/// Represents a person with a Zed account's public profile.
+/// All data in this struct should be considered public.
+pub struct PublicActor {
+    pub username: SharedString,
+    pub avatar: SharedString,
+    pub is_contact: bool,
+}
+
+impl PublicActor {
+    pub fn new(username: impl Into<SharedString>, avatar: impl Into<SharedString>) -> Self {
+        Self {
+            username: username.into(),
+            avatar: avatar.into(),
+            is_contact: false,
+        }
+    }
+}
+
 #[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
 pub enum FileSystemStatus {
     #[default]

crates/ui2/src/static_data.rs 🔗

@@ -1,5 +1,6 @@
 use std::path::PathBuf;
 use std::str::FromStr;
+use std::sync::Arc;
 
 use gpui2::{AppContext, ViewContext};
 use rand::Rng;
@@ -7,12 +8,13 @@ use theme2::ActiveTheme;
 
 use crate::{
     Buffer, BufferRow, BufferRows, Button, EditorPane, FileSystemStatus, GitStatus,
-    HighlightedLine, Icon, Keybinding, Label, LabelColor, ListEntry, ListEntrySize, ListHeaderMeta,
-    ListItem, ListSubHeader, Livestream, MicStatus, ModifierKeys, NotificationItem, PaletteItem,
-    Player, PlayerCallStatus, PlayerWithCallStatus, ScreenShareStatus, Symbol, Tab, ToggleState,
-    VideoStatus,
+    HighlightedLine, Icon, Keybinding, Label, LabelColor, ListEntry, ListEntrySize, ListSubHeader,
+    Livestream, MicStatus, ModifierKeys, Notification, NotificationItem, PaletteItem, Player,
+    PlayerCallStatus, PlayerWithCallStatus, PublicActor, ScreenShareStatus, Symbol, Tab,
+    ToggleState, VideoStatus,
 };
 use crate::{HighlightedText, ListDetailsEntry};
+use crate::{ListItem, NotificationAction};
 
 pub fn static_tabs_example() -> Vec<Tab> {
     vec![
@@ -327,8 +329,33 @@ pub fn static_players_with_call_status() -> Vec<PlayerWithCallStatus> {
 }
 
 pub fn static_new_notification_items_2<V: 'static>() -> Vec<NotificationItem<V>> {
-    vec![]
+    vec![
+        NotificationItem::Message(Notification::new_icon_message(
+            "notif-1",
+            "You were mentioned in a note.",
+            Icon::AtSign,
+            Arc::new(|_, _| {}),
+        )),
+        NotificationItem::Message(Notification::new_actor_with_actions(
+            "notif-2",
+            "as-cii sent you a contact request.",
+            PublicActor::new("as-cii", "http://github.com/as-cii.png?s=50"),
+            [
+                NotificationAction::new(
+                    Button::new("Decline"),
+                    "Decline Request",
+                    (Some(Icon::XCircle), "Declined"),
+                ),
+                NotificationAction::new(
+                    Button::new("Accept").variant(crate::ButtonVariant::Filled),
+                    "Accept Request",
+                    (Some(Icon::Check), "Accepted"),
+                ),
+            ],
+        )),
+    ]
 }
+
 pub fn static_new_notification_items<V: 'static>() -> Vec<ListItem<V>> {
     vec![
         ListItem::Header(ListSubHeader::new("New")),