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