notifications_panel.rs

  1use crate::{
  2    h_stack, prelude::*, static_new_notification_items_2, utils::naive_format_distance_from_now,
  3    v_stack, Avatar, ButtonOrIconButton, ClickHandler, Icon, IconElement, Label, LineHeightStyle,
  4    ListHeader, ListHeaderMeta, ListSeparator, PublicPlayer, TextColor, UnreadIndicator,
  5};
  6use gpui::prelude::*;
  7
  8#[derive(Component)]
  9pub struct NotificationsPanel {
 10    id: ElementId,
 11}
 12
 13impl NotificationsPanel {
 14    pub fn new(id: impl Into<ElementId>) -> Self {
 15        Self { id: id.into() }
 16    }
 17
 18    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
 19        div()
 20            .id(self.id.clone())
 21            .flex()
 22            .flex_col()
 23            .size_full()
 24            .bg(cx.theme().colors().surface_background)
 25            .child(
 26                ListHeader::new("Notifications").meta(Some(ListHeaderMeta::Tools(vec![
 27                    Icon::AtSign,
 28                    Icon::BellOff,
 29                    Icon::MailOpen,
 30                ]))),
 31            )
 32            .child(ListSeparator::new())
 33            .child(
 34                v_stack()
 35                    .id("notifications-panel-scroll-view")
 36                    .py_1()
 37                    .overflow_y_scroll()
 38                    .flex_1()
 39                    .child(
 40                        div()
 41                            .mx_2()
 42                            .p_1()
 43                            // TODO: Add cursor style
 44                            // .cursor(Cursor::IBeam)
 45                            .bg(cx.theme().colors().element_background)
 46                            .border()
 47                            .border_color(cx.theme().colors().border_variant)
 48                            .child(
 49                                Label::new("Search...")
 50                                    .color(TextColor::Placeholder)
 51                                    .line_height_style(LineHeightStyle::UILabel),
 52                            ),
 53                    )
 54                    .child(v_stack().px_1().children(static_new_notification_items_2())),
 55            )
 56    }
 57}
 58
 59pub struct NotificationAction<V: 'static> {
 60    button: ButtonOrIconButton<V>,
 61    tooltip: SharedString,
 62    /// Shows after action is chosen
 63    ///
 64    /// For example, if the action is "Accept" the taken message could be:
 65    ///
 66    /// - `(None,"Accepted")` - "Accepted"
 67    ///
 68    /// - `(Some(Icon::Check),"Accepted")` - ✓ "Accepted"
 69    taken_message: (Option<Icon>, SharedString),
 70}
 71
 72impl<V: 'static> NotificationAction<V> {
 73    pub fn new(
 74        button: impl Into<ButtonOrIconButton<V>>,
 75        tooltip: impl Into<SharedString>,
 76        (icon, taken_message): (Option<Icon>, impl Into<SharedString>),
 77    ) -> Self {
 78        Self {
 79            button: button.into(),
 80            tooltip: tooltip.into(),
 81            taken_message: (icon, taken_message.into()),
 82        }
 83    }
 84}
 85
 86pub enum ActorOrIcon {
 87    Actor(PublicPlayer),
 88    Icon(Icon),
 89}
 90
 91pub struct NotificationMeta<V: 'static> {
 92    items: Vec<(Option<Icon>, SharedString, Option<ClickHandler<V>>)>,
 93}
 94
 95struct NotificationHandlers<V: 'static> {
 96    click: Option<ClickHandler<V>>,
 97}
 98
 99impl<V: 'static> Default for NotificationHandlers<V> {
100    fn default() -> Self {
101        Self { click: None }
102    }
103}
104
105#[derive(Component)]
106pub struct Notification<V: 'static> {
107    id: ElementId,
108    slot: ActorOrIcon,
109    message: SharedString,
110    date_received: NaiveDateTime,
111    meta: Option<NotificationMeta<V>>,
112    actions: Option<[NotificationAction<V>; 2]>,
113    unread: bool,
114    new: bool,
115    action_taken: Option<NotificationAction<V>>,
116    handlers: NotificationHandlers<V>,
117}
118
119impl<V> Notification<V> {
120    fn new(
121        id: ElementId,
122        message: SharedString,
123        date_received: NaiveDateTime,
124        slot: ActorOrIcon,
125        click_action: Option<ClickHandler<V>>,
126    ) -> Self {
127        let handlers = if click_action.is_some() {
128            NotificationHandlers {
129                click: click_action,
130            }
131        } else {
132            NotificationHandlers::default()
133        };
134
135        Self {
136            id,
137            date_received,
138            message,
139            meta: None,
140            slot,
141            actions: None,
142            unread: true,
143            new: false,
144            action_taken: None,
145            handlers,
146        }
147    }
148
149    /// Creates a new notification with an actor slot.
150    ///
151    /// Requires a click action.
152    pub fn new_actor_message(
153        id: impl Into<ElementId>,
154        message: impl Into<SharedString>,
155        date_received: NaiveDateTime,
156        actor: PublicPlayer,
157        click_action: ClickHandler<V>,
158    ) -> Self {
159        Self::new(
160            id.into(),
161            message.into(),
162            date_received,
163            ActorOrIcon::Actor(actor),
164            Some(click_action),
165        )
166    }
167
168    /// Creates a new notification with an icon slot.
169    ///
170    /// Requires a click action.
171    pub fn new_icon_message(
172        id: impl Into<ElementId>,
173        message: impl Into<SharedString>,
174        date_received: NaiveDateTime,
175        icon: Icon,
176        click_action: ClickHandler<V>,
177    ) -> Self {
178        Self::new(
179            id.into(),
180            message.into(),
181            date_received,
182            ActorOrIcon::Icon(icon),
183            Some(click_action),
184        )
185    }
186
187    /// Creates a new notification with an actor slot
188    /// and a Call To Action row.
189    ///
190    /// Cannot take a click action due to required actions.
191    pub fn new_actor_with_actions(
192        id: impl Into<ElementId>,
193        message: impl Into<SharedString>,
194        date_received: NaiveDateTime,
195        actor: PublicPlayer,
196        actions: [NotificationAction<V>; 2],
197    ) -> Self {
198        Self::new(
199            id.into(),
200            message.into(),
201            date_received,
202            ActorOrIcon::Actor(actor),
203            None,
204        )
205        .actions(actions)
206    }
207
208    /// Creates a new notification with an icon slot
209    /// and a Call To Action row.
210    ///
211    /// Cannot take a click action due to required actions.
212    pub fn new_icon_with_actions(
213        id: impl Into<ElementId>,
214        message: impl Into<SharedString>,
215        date_received: NaiveDateTime,
216        icon: Icon,
217        actions: [NotificationAction<V>; 2],
218    ) -> Self {
219        Self::new(
220            id.into(),
221            message.into(),
222            date_received,
223            ActorOrIcon::Icon(icon),
224            None,
225        )
226        .actions(actions)
227    }
228
229    fn on_click(mut self, handler: ClickHandler<V>) -> Self {
230        self.handlers.click = Some(handler);
231        self
232    }
233
234    pub fn actions(mut self, actions: [NotificationAction<V>; 2]) -> Self {
235        self.actions = Some(actions);
236        self
237    }
238
239    pub fn meta(mut self, meta: NotificationMeta<V>) -> Self {
240        self.meta = Some(meta);
241        self
242    }
243
244    fn render_meta_items(&self, cx: &mut ViewContext<V>) -> impl Component<V> {
245        if let Some(meta) = &self.meta {
246            h_stack().children(
247                meta.items
248                    .iter()
249                    .map(|(icon, text, _)| {
250                        let mut meta_el = div();
251                        if let Some(icon) = icon {
252                            meta_el = meta_el.child(IconElement::new(icon.clone()));
253                        }
254                        meta_el.child(Label::new(text.clone()).color(TextColor::Muted))
255                    })
256                    .collect::<Vec<_>>(),
257            )
258        } else {
259            div()
260        }
261    }
262
263    fn render_slot(&self, cx: &mut ViewContext<V>) -> impl Component<V> {
264        match &self.slot {
265            ActorOrIcon::Actor(actor) => Avatar::new(actor.avatar.clone()).render(),
266            ActorOrIcon::Icon(icon) => IconElement::new(icon.clone()).render(),
267        }
268    }
269
270    fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
271        div()
272            .relative()
273            .id(self.id.clone())
274            .p_1()
275            .flex()
276            .flex_col()
277            .w_full()
278            .children(
279                Some(
280                    div()
281                        .absolute()
282                        .left(px(3.0))
283                        .top_3()
284                        .z_index(2)
285                        .child(UnreadIndicator::new()),
286                )
287                .filter(|_| self.unread),
288            )
289            .child(
290                v_stack()
291                    .z_index(1)
292                    .gap_1()
293                    .w_full()
294                    .child(
295                        h_stack()
296                            .w_full()
297                            .gap_2()
298                            .child(self.render_slot(cx))
299                            .child(div().flex_1().child(Label::new(self.message.clone()))),
300                    )
301                    .child(
302                        h_stack()
303                            .justify_between()
304                            .child(
305                                h_stack()
306                                    .gap_1()
307                                    .child(
308                                        Label::new(naive_format_distance_from_now(
309                                            self.date_received,
310                                            true,
311                                            true,
312                                        ))
313                                        .color(TextColor::Muted),
314                                    )
315                                    .child(self.render_meta_items(cx)),
316                            )
317                            .child(match (self.actions, self.action_taken) {
318                                // Show nothing
319                                (None, _) => div(),
320                                // Show the taken_message
321                                (Some(_), Some(action_taken)) => h_stack()
322                                    .children(action_taken.taken_message.0.map(|icon| {
323                                        IconElement::new(icon).color(crate::TextColor::Muted)
324                                    }))
325                                    .child(
326                                        Label::new(action_taken.taken_message.1.clone())
327                                            .color(TextColor::Muted),
328                                    ),
329                                // Show the actions
330                                (Some(actions), None) => {
331                                    h_stack().children(actions.map(|action| match action.button {
332                                        ButtonOrIconButton::Button(button) => {
333                                            Component::render(button)
334                                        }
335                                        ButtonOrIconButton::IconButton(icon_button) => {
336                                            Component::render(icon_button)
337                                        }
338                                    }))
339                                }
340                            }),
341                    ),
342            )
343    }
344}
345
346use chrono::NaiveDateTime;
347use gpui::{px, Styled};
348#[cfg(feature = "stories")]
349pub use stories::*;
350
351#[cfg(feature = "stories")]
352mod stories {
353    use super::*;
354    use crate::{Panel, Story};
355    use gpui::{Div, Render};
356
357    pub struct NotificationsPanelStory;
358
359    impl Render for NotificationsPanelStory {
360        type Element = Div<Self>;
361
362        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
363            Story::container(cx)
364                .child(Story::title_for::<_, NotificationsPanel>(cx))
365                .child(Story::label(cx, "Default"))
366                .child(
367                    Panel::new("panel", cx).child(NotificationsPanel::new("notifications_panel")),
368                )
369        }
370    }
371}