Skip to main content

liora_components/
notification.rs

1use crate::motion::pop_in;
2use gpui::{
3    App, Context, Entity, Global, IntoElement, Render, SharedString, Window, div, prelude::*, px,
4};
5use liora_core::{Config, push_passive_portal};
6use liora_icons::Icon;
7use liora_icons_lucide::IconName;
8use std::time::Duration;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum NotificationType {
12    Info,
13    Success,
14    Warning,
15    Error,
16}
17
18#[derive(Clone)]
19pub struct NotificationItem {
20    pub id: usize,
21    pub title: SharedString,
22    pub description: Option<SharedString>,
23    pub msg_type: NotificationType,
24}
25
26pub struct NotificationManager {
27    notifications: Vec<NotificationItem>,
28    next_id: usize,
29}
30
31pub struct NotificationManagerGlobal(pub Entity<NotificationManager>);
32impl Global for NotificationManagerGlobal {}
33
34impl NotificationManager {
35    pub fn new() -> Self {
36        Self {
37            notifications: vec![],
38            next_id: 0,
39        }
40    }
41
42    pub fn init(cx: &mut App) {
43        if !cx.has_global::<NotificationManagerGlobal>() {
44            let manager = cx.new(|_| Self::new());
45            cx.set_global(NotificationManagerGlobal(manager));
46        }
47    }
48
49    pub fn show(
50        title: impl Into<SharedString>,
51        description: Option<SharedString>,
52        msg_type: NotificationType,
53        cx: &mut App,
54    ) {
55        Self::init(cx);
56        let manager = cx.global::<NotificationManagerGlobal>().0.clone();
57        let title = title.into();
58
59        manager.update(cx, |this, cx| {
60            let id = this.next_id;
61            this.notifications.push(NotificationItem {
62                id,
63                title: title.clone(),
64                description: description.clone(),
65                msg_type,
66            });
67            this.next_id += 1;
68
69            let async_cx = cx.to_async();
70            let executor = cx.background_executor().clone();
71            cx.foreground_executor()
72                .spawn(async move {
73                    executor.timer(Duration::from_secs(4)).await;
74                    async_cx.update(|cx| {
75                        if cx.has_global::<NotificationManagerGlobal>() {
76                            let manager = cx.global::<NotificationManagerGlobal>().0.clone();
77                            manager.update(cx, |this, cx| {
78                                this.notifications.retain(|n| n.id != id);
79                                cx.notify();
80                            });
81                        }
82                    });
83                })
84                .detach();
85
86            cx.notify();
87        });
88    }
89}
90
91impl Render for NotificationManager {
92    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
93        let items = self.notifications.clone();
94        if items.is_empty() {
95            return div();
96        }
97
98        let theme = cx.global::<Config>().theme.clone();
99
100        div()
101            .absolute()
102            .top_8()
103            .right_8()
104            .flex()
105            .flex_col()
106            .items_end()
107            .gap_4()
108            .children(items.into_iter().map(|item| {
109                let (color, icon) = match item.msg_type {
110                    NotificationType::Info => (theme.primary.base, IconName::Info),
111                    NotificationType::Success => (theme.success.base, IconName::Check),
112                    NotificationType::Warning => (theme.warning.base, IconName::TriangleAlert),
113                    NotificationType::Error => (theme.danger.base, IconName::CircleX),
114                };
115
116                pop_in(
117                    ("liora-notification", item.id),
118                    div()
119                        .w(px(320.0))
120                        .bg(theme.neutral.card)
121                        .border_1()
122                        .border_color(theme.neutral.border)
123                        .p_4()
124                        .rounded(px(theme.radius.md))
125                        .shadow_lg()
126                        .flex()
127                        .flex_row()
128                        .gap_3()
129                        .child(Icon::new(icon).size(px(24.0)).color(color))
130                        .child(
131                            div()
132                                .flex_1()
133                                .flex()
134                                .flex_col()
135                                .gap_1()
136                                .child(div().font_weight(gpui::FontWeight::BOLD).child(item.title))
137                                .when_some(item.description, |s, d| {
138                                    s.child(
139                                        div().text_sm().text_color(theme.neutral.text_3).child(d),
140                                    )
141                                }),
142                        ),
143                )
144            }))
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    #[test]
151    fn notifications_use_liora_motion() {
152        let source = include_str!("notification.rs")
153            .split("#[cfg(test)]")
154            .next()
155            .unwrap();
156
157        assert!(source.contains("pop_in("));
158        assert!(source.contains("liora-notification"));
159    }
160}
161
162pub fn show_notification(
163    title: impl Into<SharedString>,
164    description: Option<SharedString>,
165    msg_type: NotificationType,
166    cx: &mut App,
167) {
168    NotificationManager::show(title, description, msg_type, cx);
169}
170
171pub fn render_notifications(cx: &mut App) {
172    if cx.has_global::<NotificationManagerGlobal>() {
173        let manager = cx.global::<NotificationManagerGlobal>().0.clone();
174        if !manager.read(cx).notifications.is_empty() {
175            push_passive_portal(move |_window, _cx| manager.clone().into_any_element(), cx);
176        }
177    }
178}