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