tallyweb_components/
message.rs

1use std::collections::HashMap;
2
3use chrono::Duration;
4use leptos::*;
5
6pub type MessageKey = usize;
7
8#[derive(Debug, Clone, PartialEq)]
9struct Notification {
10    kind: NotificationKind,
11    do_fade: bool,
12}
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15enum NotificationKind {
16    Message(bool, View),
17    Error(bool, View),
18    Success(bool, View),
19}
20
21impl NotificationKind {
22    fn get_view(&self) -> Option<View> {
23        match self {
24            NotificationKind::Message(_, msg) => Some(msg.clone()),
25            NotificationKind::Error(_, msg) => Some(msg.clone()),
26            NotificationKind::Success(_, msg) => Some(msg.clone()),
27        }
28    }
29}
30
31pub trait Handle: Clone + Copy + 'static {}
32#[derive(Debug, Clone, Copy)]
33pub struct WithHandle;
34impl Handle for WithHandle {}
35#[derive(Debug, Clone, Copy)]
36pub struct NoHandle;
37impl Handle for NoHandle {}
38
39#[derive(Debug, Clone, Copy)]
40pub struct MessageJar<T: Handle> {
41    messages: RwSignal<HashMap<MessageKey, Notification>>,
42    reset_time: Option<Duration>,
43    next_key: RwSignal<MessageKey>,
44    as_modal: bool,
45    phantomdata: std::marker::PhantomData<T>,
46}
47
48#[allow(dead_code)]
49impl<T: Handle + 'static> MessageJar<T> {
50    pub fn new(reset_time: Duration) -> Self {
51        Self {
52            messages: HashMap::new().into(),
53            reset_time: Some(reset_time),
54            as_modal: false,
55            next_key: 0.into(),
56            phantomdata: std::marker::PhantomData {},
57        }
58    }
59
60    fn get_ordered(&self) -> Signal<Vec<(MessageKey, Notification)>> {
61        create_read_slice(self.messages, |msgs| {
62            let mut entries = msgs
63                .iter()
64                .map(|(key, value)| (*key, value.clone()))
65                .collect::<Vec<_>>();
66            entries.sort_by(|a, b| a.0.cmp(&b.0));
67            entries
68        })
69    }
70
71    pub fn is_emtpy(&self) -> bool {
72        self.messages.get().is_empty()
73    }
74
75    pub fn clear(&self) {
76        self.messages.update(|list| list.clear())
77    }
78
79    pub fn without_timeout(self) -> Self {
80        Self {
81            reset_time: None,
82            ..self
83        }
84    }
85
86    pub fn with_timeout(self, reset_time: Duration) -> Self {
87        Self {
88            reset_time: Some(reset_time),
89            ..self
90        }
91    }
92
93    pub fn as_modal(self) -> Self {
94        Self {
95            as_modal: true,
96            ..self
97        }
98    }
99
100    fn add_msg(&self, msg: NotificationKind) -> MessageKey {
101        self.next_key.update(|k| *k += 1);
102        let key = self.next_key.get_untracked();
103        self.messages.update(|m| {
104            m.insert(
105                key,
106                Notification {
107                    kind: msg,
108                    do_fade: false,
109                },
110            );
111        });
112        key
113    }
114
115    fn msg_timeout_effect(self, key: MessageKey) {
116        if let Some(timeout) = self.reset_time {
117            set_timeout(move || self.fade_out(key), timeout.to_std().unwrap())
118        }
119    }
120
121    pub fn fade_out(self, key: MessageKey) {
122        self.messages.update(|m| {
123            if let Some(v) = m.get_mut(&key) {
124                v.do_fade = true
125            }
126        })
127    }
128
129    pub fn get_last_key(self) -> Signal<MessageKey> {
130        Signal::derive(self.next_key)
131    }
132}
133
134impl MessageJar<NoHandle> {
135    pub fn with_handle(self) -> MessageJar<WithHandle> {
136        unsafe { std::mem::transmute(self) }
137    }
138
139    pub fn set_msg(self, msg: impl ToString) {
140        let msg = msg.to_string();
141        let msg_lines = msg.lines();
142        let key = self.add_msg(NotificationKind::Message(
143            self.as_modal,
144            msg_lines
145                .map(|l| view! { <b>{l.to_string()}</b> })
146                .collect_view(),
147        ));
148        self.msg_timeout_effect(key);
149    }
150
151    pub fn set_msg_view(self, msg: impl IntoView + 'static) {
152        let msg = msg.into_view();
153        let key = self.add_msg(NotificationKind::Message(self.as_modal, msg.clone()));
154        self.msg_timeout_effect(key);
155    }
156
157    pub fn set_success(&self, msg: &str) {
158        let msg_lines = msg.lines();
159        let key = self.add_msg(NotificationKind::Success(
160            self.as_modal,
161            msg_lines
162                .map(|l| view! { <b>{l.to_string()}</b> })
163                .collect_view(),
164        ));
165
166        self.msg_timeout_effect(key)
167    }
168
169    pub fn set_success_view(&self, msg: impl IntoView) {
170        let key = self.add_msg(NotificationKind::Success(self.as_modal, msg.into_view()));
171        self.msg_timeout_effect(key);
172    }
173
174    pub fn set_err(self, err: impl ToString) {
175        let err = err.to_string();
176        let msg_lines = err.lines();
177        let key = self.add_msg(NotificationKind::Error(
178            self.as_modal,
179            msg_lines
180                .map(|l| view! { <b>{l.to_string()}</b> })
181                .collect_view(),
182        ));
183        self.msg_timeout_effect(key);
184    }
185
186    pub fn set_err_view(&self, err: impl IntoView) {
187        let key = self.add_msg(NotificationKind::Error(self.as_modal, err.into_view()));
188        self.msg_timeout_effect(key)
189    }
190
191    pub fn set_server_err(&self, err: &leptos::ServerFnError) {
192        match err {
193            ServerFnError::WrappedServerError(e) => self.set_err(e),
194            ServerFnError::Registration(e) => self.set_err(e),
195            ServerFnError::Request(e) => self.set_err(e),
196            ServerFnError::Response(e) => self.set_err(e),
197            ServerFnError::ServerError(e) => self.set_err(e),
198            ServerFnError::Deserialization(e) => self.set_err(e),
199            ServerFnError::Serialization(e) => self.set_err(e),
200            ServerFnError::Args(e) => self.set_err(e),
201            ServerFnError::MissingArg(e) => self.set_err(e),
202        }
203    }
204}
205
206impl MessageJar<WithHandle> {
207    pub fn set_msg(self, msg: impl ToString) -> MessageKey {
208        let msg = msg.to_string();
209        let msg_lines = msg.lines();
210        let key = self.add_msg(NotificationKind::Message(
211            self.as_modal,
212            msg_lines
213                .map(|l| view! { <b>{l.to_string()}</b> })
214                .collect_view(),
215        ));
216        self.msg_timeout_effect(key);
217        key
218    }
219
220    pub fn set_msg_view(self, msg: impl IntoView + 'static) -> MessageKey {
221        let msg = msg.into_view();
222        let key = self.add_msg(NotificationKind::Message(self.as_modal, msg.clone()));
223        self.msg_timeout_effect(key);
224        key
225    }
226
227    pub fn set_success(&self, msg: &str) -> MessageKey {
228        let msg_lines = msg.lines();
229        let key = self.add_msg(NotificationKind::Success(
230            self.as_modal,
231            msg_lines
232                .map(|l| view! { <b>{l.to_string()}</b> })
233                .collect_view(),
234        ));
235
236        self.msg_timeout_effect(key);
237        key
238    }
239
240    pub fn set_success_view(&self, msg: impl IntoView) -> MessageKey {
241        let key = self.add_msg(NotificationKind::Success(self.as_modal, msg.into_view()));
242        self.msg_timeout_effect(key);
243        key
244    }
245
246    pub fn set_err(self, err: impl ToString) -> MessageKey {
247        let err = err.to_string();
248        let msg_lines = err.lines();
249        let key = self.add_msg(NotificationKind::Error(
250            self.as_modal,
251            msg_lines
252                .map(|l| view! { <b>{l.to_string()}</b> })
253                .collect_view(),
254        ));
255        self.msg_timeout_effect(key);
256        key
257    }
258
259    pub fn set_err_view(&self, err: impl IntoView) -> MessageKey {
260        let key = self.add_msg(NotificationKind::Error(self.as_modal, err.into_view()));
261        self.msg_timeout_effect(key);
262        key
263    }
264
265    pub fn set_server_err(&self, err: &leptos::ServerFnError) -> MessageKey {
266        match err {
267            ServerFnError::WrappedServerError(e) => self.set_err(e),
268            ServerFnError::Registration(e) => self.set_err(e),
269            ServerFnError::Request(e) => self.set_err(e),
270            ServerFnError::Response(e) => self.set_err(e),
271            ServerFnError::ServerError(e) => self.set_err(e),
272            ServerFnError::Deserialization(e) => self.set_err(e),
273            ServerFnError::Serialization(e) => self.set_err(e),
274            ServerFnError::Args(e) => self.set_err(e),
275            ServerFnError::MissingArg(e) => self.set_err(e),
276        }
277    }
278}
279
280#[component]
281fn Message(key: MessageKey, jar: MessageJar<NoHandle>) -> impl IntoView {
282    if !jar.messages.get_untracked().contains_key(&key) {
283        return view! {}.into_view();
284    }
285
286    let kind = create_read_slice(jar.messages, move |map| map.get(&key).unwrap().kind.clone());
287
288    let border_style = move || match kind() {
289        NotificationKind::Message(_, _) => "border: 2px solid #ffe135",
290        NotificationKind::Error(_, _) => "color: tomato; border: 2px solid tomato;",
291        NotificationKind::Success(_, _) => "color: #28a745; border: 2px solid #28a745;",
292    };
293
294    let is_modal = move || match kind() {
295        NotificationKind::Message(is_modal, _) => is_modal,
296        NotificationKind::Error(is_modal, _) => is_modal,
297        NotificationKind::Success(is_modal, _) => is_modal,
298    };
299
300    let dialog_ref = create_node_ref::<html::Dialog>();
301    create_effect(move |_| {
302        if let Some(d) = dialog_ref() {
303            d.close();
304            if is_modal() {
305                let _ = d.show_modal();
306            } else {
307                d.show();
308            }
309        }
310    });
311
312    let dialog_class = create_read_slice(jar.messages, move |map| {
313        if map.get(&key).unwrap().do_fade {
314            String::from("fade-out")
315        } else {
316            String::from("")
317        }
318    });
319
320    let on_close_click = move |ev: ev::MouseEvent| {
321        ev.stop_propagation();
322        jar.fade_out(key)
323    };
324
325    let on_animend = move |_| {
326        jar.messages.update(|m| {
327            m.remove(&key);
328        })
329    };
330
331    view! {
332        <dialog
333            on:click=|ev| ev.stop_propagation()
334            node_ref=dialog_ref
335            class=dialog_class
336            style=border_style
337            on:animationend=on_animend
338        >
339            <div class="content">
340                <button class="close" on:click=on_close_click>
341                    <i class="fa-solid fa-xmark"></i>
342                </button>
343                {move || kind.get().get_view().unwrap_or(view! {}.into_view())}
344            </div>
345        </dialog>
346    }
347    .into_view()
348}
349
350#[component]
351pub fn ProvideMessageSystem() -> impl IntoView {
352    let msg_jar = MessageJar::new(Duration::seconds(5));
353    provide_context(msg_jar);
354
355    // on navigation clear any messages or errors from the message box
356    // let loc_memo = create_memo(move |_| {
357    //     let location = leptos_router::use_location();
358    //     location.state.with(|_| msg_box.clear())
359    // });
360    //
361
362    view! {
363        <Show when=move || !msg_jar.is_emtpy()>
364            <notification-box>
365                <For
366                    each=move || msg_jar.get_ordered().get().into_iter().rev()
367                    key=|(key, _)| *key
368                    children=move |(key, _)| {
369                        view! { <Message key jar=msg_jar /> }
370                    }
371                />
372
373            </notification-box>
374        </Show>
375    }
376}