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