notifications_panel.rs

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