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}