@@ -1,5 +1,9 @@
-use crate::{prelude::*, static_new_notification_items, Icon, ListHeaderMeta};
-use crate::{List, ListHeader};
+use crate::{
+ h_stack, prelude::*, static_new_notification_items, v_stack, Avatar, Button, Icon, IconButton,
+ IconElement, Label, LabelColor, LineHeightStyle, ListHeaderMeta, ListSeparator, Stack,
+ UnreadIndicator,
+};
+use crate::{ClickHandler, ListHeader};
#[derive(Component)]
pub struct NotificationsPanel {
@@ -16,33 +20,367 @@ impl NotificationsPanel {
.id(self.id.clone())
.flex()
.flex_col()
- .w_full()
- .h_full()
+ .size_full()
.bg(cx.theme().colors().surface)
.child(
- div()
- .id("header")
- .w_full()
- .flex()
- .flex_col()
+ ListHeader::new("Notifications").meta(Some(ListHeaderMeta::Tools(vec![
+ Icon::AtSign,
+ Icon::BellOff,
+ Icon::MailOpen,
+ ]))),
+ )
+ .child(ListSeparator::new())
+ .child(
+ v_stack()
+ .id("notifications-panel-scroll-view")
+ .py_1()
.overflow_y_scroll()
+ .flex_1()
.child(
- List::new(static_new_notification_items())
- .toggle(ToggleState::Toggled)
- .header(
- ListHeader::new("Notifications")
- .toggle(ToggleState::Toggled)
- .meta(Some(ListHeaderMeta::Tools(vec![
- Icon::AtSign,
- Icon::BellOff,
- Icon::MailOpen,
- ]))),
+ div()
+ .mx_2()
+ .p_1()
+ // TODO: Add cursor style
+ // .cursor(Cursor::IBeam)
+ .bg(cx.theme().colors().element)
+ .border()
+ .border_color(cx.theme().colors().border_variant)
+ .child(
+ Label::new("Search...")
+ .color(LabelColor::Placeholder)
+ .line_height_style(LineHeightStyle::UILabel),
+ ),
+ )
+ .children(static_new_notification_items()),
+ )
+ }
+}
+
+pub enum NotificationItem<V: 'static> {
+ Message(Notification<V>),
+ // WithEdgeHeader(Notification<V>),
+ WithRequiredActions(NotificationWithActions<V>),
+}
+
+pub enum ButtonOrIconButton<V: 'static> {
+ Button(Button<V>),
+ IconButton(IconButton<V>),
+}
+
+pub struct NotificationAction<V: 'static> {
+ button: ButtonOrIconButton<V>,
+ tooltip: SharedString,
+ /// Shows after action is chosen
+ ///
+ /// For example, if the action is "Accept" the taken message could be:
+ ///
+ /// - `(None,"Accepted")` - "Accepted"
+ ///
+ /// - `(Some(Icon::Check),"Accepted")` - ✓ "Accepted"
+ taken_message: (Option<Icon>, SharedString),
+}
+
+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),
+}
+
+pub struct NotificationMeta<V: 'static> {
+ items: Vec<(Option<Icon>, SharedString, Option<ClickHandler<V>>)>,
+}
+
+struct NotificationHandlers<V: 'static> {
+ click: Option<ClickHandler<V>>,
+}
+
+impl<V: 'static> Default for NotificationHandlers<V> {
+ fn default() -> Self {
+ Self { click: None }
+ }
+}
+
+#[derive(Component)]
+pub struct Notification<V: 'static> {
+ id: ElementId,
+ slot: ActorOrIcon,
+ message: SharedString,
+ date_received: NaiveDateTime,
+ meta: Option<NotificationMeta<V>>,
+ actions: Option<[NotificationAction<V>; 2]>,
+ unread: bool,
+ new: bool,
+ action_taken: Option<NotificationAction<V>>,
+ handlers: NotificationHandlers<V>,
+}
+
+impl<V> Notification<V> {
+ fn new(
+ id: ElementId,
+ message: SharedString,
+ slot: ActorOrIcon,
+ click_action: Option<ClickHandler<V>>,
+ ) -> Self {
+ let handlers = if click_action.is_some() {
+ NotificationHandlers {
+ click: click_action,
+ }
+ } else {
+ NotificationHandlers::default()
+ };
+
+ Self {
+ id,
+ date_received: DateTime::parse_from_rfc3339("1969-07-20T00:00:00Z")
+ .unwrap()
+ .naive_local(),
+ message,
+ meta: None,
+ slot,
+ actions: None,
+ unread: true,
+ new: false,
+ action_taken: None,
+ handlers,
+ }
+ }
+
+ /// Creates a new notification with an actor slot.
+ ///
+ /// Requires a click action.
+ pub fn new_actor_message(
+ id: impl Into<ElementId>,
+ message: SharedString,
+ actor: PublicActor,
+ click_action: ClickHandler<V>,
+ ) -> Self {
+ Self::new(
+ id.into(),
+ message,
+ ActorOrIcon::Actor(actor),
+ Some(click_action),
+ )
+ }
+
+ /// Creates a new notification with an icon slot.
+ ///
+ /// Requires a click action.
+ pub fn new_icon_message(
+ id: impl Into<ElementId>,
+ message: SharedString,
+ icon: Icon,
+ click_action: ClickHandler<V>,
+ ) -> Self {
+ Self::new(
+ id.into(),
+ message,
+ ActorOrIcon::Icon(icon),
+ Some(click_action),
+ )
+ }
+
+ /// Creates a new notification with an actor slot
+ /// and a Call To Action row.
+ ///
+ /// Cannot take a click action due to required actions.
+ pub fn new_actor_with_actions(
+ id: impl Into<ElementId>,
+ message: SharedString,
+ actor: PublicActor,
+ click_action: ClickHandler<V>,
+ actions: [NotificationAction<V>; 2],
+ ) -> Self {
+ Self::new(id.into(), message, ActorOrIcon::Actor(actor), None).actions(actions)
+ }
+
+ /// Creates a new notification with an icon slot
+ /// and a Call To Action row.
+ ///
+ /// Cannot take a click action due to required actions.
+ pub fn new_icon_with_actions(
+ id: impl Into<ElementId>,
+ message: SharedString,
+ icon: Icon,
+ click_action: ClickHandler<V>,
+ actions: [NotificationAction<V>; 2],
+ ) -> Self {
+ Self::new(id.into(), message, ActorOrIcon::Icon(icon), None).actions(actions)
+ }
+
+ fn on_click(mut self, handler: ClickHandler<V>) -> Self {
+ self.handlers.click = Some(handler);
+ self
+ }
+
+ pub fn actions(mut self, actions: [NotificationAction<V>; 2]) -> Self {
+ self.actions = Some(actions);
+ self
+ }
+
+ pub fn meta(mut self, meta: NotificationMeta<V>) -> Self {
+ self.meta = Some(meta);
+ self
+ }
+
+ fn render_meta_items(&self, cx: &mut ViewContext<V>) -> impl Component<V> {
+ if let Some(meta) = &self.meta {
+ h_stack().children(
+ meta.items
+ .iter()
+ .map(|(icon, text, _)| {
+ let mut meta_el = div();
+ if let Some(icon) = icon {
+ meta_el = meta_el.child(IconElement::new(icon.clone()));
+ }
+ meta_el.child(Label::new(text.clone()).color(LabelColor::Muted))
+ })
+ .collect::<Vec<_>>(),
+ )
+ } else {
+ div()
+ }
+ }
+
+ 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(),
+ ActorOrIcon::Icon(icon) => IconElement::new(icon.clone()).render(),
+ }
+ }
+
+ fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+ div()
+ .relative()
+ .id(self.id.clone())
+ .children(
+ Some(
+ div()
+ .absolute()
+ .left(px(3.0))
+ .top_3()
+ .child(UnreadIndicator::new()),
+ )
+ .filter(|_| self.unread),
+ )
+ .child(
+ v_stack()
+ .gap_1()
+ .child(
+ h_stack()
+ .gap_2()
+ .child(self.render_slot(cx))
+ .child(div().flex_1().child(Label::new(self.message.clone()))),
+ )
+ .child(
+ h_stack()
+ .justify_between()
+ .child(
+ h_stack()
+ .gap_1()
+ .child(
+ Label::new(
+ self.date_received.format("%m/%d/%Y").to_string(),
+ )
+ .color(LabelColor::Muted),
+ )
+ .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()
+
+ }
+
+ // 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<_>>(),
+
),
),
)
}
}
+use chrono::{DateTime, NaiveDateTime};
+use gpui2::{px, Styled};
#[cfg(feature = "stories")]
pub use stories::*;
@@ -8,8 +8,8 @@ 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, PaletteItem, Player,
- PlayerCallStatus, PlayerWithCallStatus, ScreenShareStatus, Symbol, Tab, ToggleState,
+ ListItem, ListSubHeader, Livestream, MicStatus, ModifierKeys, NotificationItem, PaletteItem,
+ Player, PlayerCallStatus, PlayerWithCallStatus, ScreenShareStatus, Symbol, Tab, ToggleState,
VideoStatus,
};
use crate::{HighlightedText, ListDetailsEntry};
@@ -326,6 +326,9 @@ pub fn static_players_with_call_status() -> Vec<PlayerWithCallStatus> {
]
}
+pub fn static_new_notification_items_2<V: 'static>() -> Vec<NotificationItem<V>> {
+ vec![]
+}
pub fn static_new_notification_items<V: 'static>() -> Vec<ListItem<V>> {
vec![
ListItem::Header(ListSubHeader::new("New")),
@@ -351,6 +354,52 @@ pub fn static_new_notification_items<V: 'static>() -> Vec<ListItem<V>> {
ListItem::Details(ListDetailsEntry::new(
"as-cii accepted your contact request.",
)),
+ ListItem::Details(
+ ListDetailsEntry::new("You were added as an admin on the #gpui2 channel.").seen(true),
+ ),
+ ListItem::Details(ListDetailsEntry::new(
+ "osiewicz accepted your contact request.",
+ )),
+ ListItem::Details(ListDetailsEntry::new(
+ "ConradIrwin accepted your contact request.",
+ )),
+ ListItem::Details(
+ ListDetailsEntry::new("nathansobo invited you to a stream in #gpui2.")
+ .seen(true)
+ .meta("This stream has ended."),
+ ),
+ ListItem::Details(ListDetailsEntry::new(
+ "nathansobo accepted your contact request.",
+ )),
+ ListItem::Header(ListSubHeader::new("Earlier")),
+ ListItem::Details(
+ ListDetailsEntry::new("mikaylamaki added you as a contact.").actions(vec![
+ Button::new("Decline"),
+ Button::new("Accept").variant(crate::ButtonVariant::Filled),
+ ]),
+ ),
+ ListItem::Details(
+ ListDetailsEntry::new("maxdeviant invited you to a stream in #design.")
+ .seen(true)
+ .meta("This stream has ended."),
+ ),
+ ListItem::Details(ListDetailsEntry::new(
+ "as-cii accepted your contact request.",
+ )),
+ ListItem::Details(
+ ListDetailsEntry::new("You were added as an admin on the #gpui2 channel.").seen(true),
+ ),
+ ListItem::Details(ListDetailsEntry::new(
+ "osiewicz accepted your contact request.",
+ )),
+ ListItem::Details(ListDetailsEntry::new(
+ "ConradIrwin accepted your contact request.",
+ )),
+ ListItem::Details(
+ ListDetailsEntry::new("nathansobo invited you to a stream in #gpui2.")
+ .seen(true)
+ .meta("This stream has ended."),
+ ),
]
.into_iter()
.map(From::from)