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}