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}