1use core::time::Duration;
14
15use blockworx_kernel::{Event, Notice};
16use blockworx_tools::tool::Action;
17use dioxus::prelude::*;
18use dioxus::web::WebEventExt as _;
19
20use crate::shell::{Band, Shell, after, measured};
21
22const DWELL: Duration = Duration::from_millis(2_400);
24
25#[must_use]
32pub fn acknowledgements(notices: &[Notice]) -> Vec<(String, Option<usize>)> {
33 let mut failures = 0;
34 notices
35 .iter()
36 .map(|notice| match notice {
37 Notice::Failure(said) => {
38 let at = failures;
39 failures += 1;
40 (said.clone(), Some(at))
41 }
42 Notice::Standing(said) => (said.clone(), None),
43 })
44 .collect()
45}
46
47#[component]
48pub fn Notices(shell: Shell) -> Element {
49 let chrome = shell.chrome();
50 let rows = acknowledgements(&chrome.read().notices);
51 if rows.is_empty() {
52 return rsx! {};
53 }
54 let shell = use_hook(|| CopyValue::new(shell));
55 rsx! {
56 div {
57 class: "absolute right-4 top-4 z-20 flex max-w-[40%] flex-col gap-2 rounded-2xl \
58 bg-white/90 p-3 text-sm shadow-lg ring-1 ring-zinc-950/5 backdrop-blur-xl \
59 dark:bg-zinc-800/90 dark:ring-white/10",
60 role: "alert",
61 onmounted: move |event| {
62 if let Some(element) = event.try_as_web_event() {
63 covers(&shell.read(), &element);
64 }
65 },
66 onresize: move |event| {
67 if let Some(entry) = event.try_as_web_event() {
68 covers(&shell.read(), &entry.target());
69 }
70 },
71 for (said , failure) in rows {
72 div { key: "{said}", class: "flex items-start gap-3",
73 span { class: "min-w-0 flex-1 text-zinc-700 dark:text-zinc-200", "{said}" }
74 if let Some(at) = failure {
75 button {
76 class: "flex-none rounded-lg px-2 py-1 text-xs font-medium \
77 text-zinc-500 transition hover:bg-zinc-950/6 \
78 dark:text-zinc-400 dark:hover:bg-white/10",
79 title: "Done with this message",
80 "data-dismiss": "{at}",
81 onclick: move |_| {
82 shell.read().say(Event::Action(Action::AcknowledgeFailure(at)));
83 },
84 "Dismiss"
85 }
86 }
87 }
88 }
89 }
90 }
91}
92
93#[component]
98pub fn Toast(shell: Shell) -> Element {
99 let reported = shell.reported();
100 let mut showing = use_signal(|| None::<String>);
101 let mut holding = use_signal(|| 0_u64);
102 use_effect(move || {
103 let Some(what) = reported.read().what.clone() else {
104 return;
105 };
106 let taken = *holding.peek() + 1;
107 holding.set(taken);
108 showing.set(Some(what));
109 after(DWELL, move || {
110 if *holding.peek() == taken {
111 showing.set(None);
112 }
113 });
114 });
115 let Some(what) = showing.read().clone() else {
116 return rsx! {};
117 };
118 rsx! {
119 div {
120 class: "pointer-events-none absolute bottom-28 left-1/2 z-40 -translate-x-1/2 \
121 rounded-2xl bg-zinc-900 px-4 py-2.5 text-sm text-zinc-50 shadow-xl \
122 dark:bg-zinc-100 dark:text-zinc-900",
123 role: "alert",
124 "data-toast": "true",
125 "{what}"
126 }
127 }
128}
129
130fn covers(shell: &Shell, element: &web_sys::Element) {
131 if let Some(at) = measured(element) {
132 shell.covers(Band::Notices, at);
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
144 fn a_dismissal_counts_failures_and_not_rows() {
145 let rows = acknowledgements(&[
146 Notice::Failure("Could not open it".to_owned()),
147 Notice::Standing("Read-only".to_owned()),
148 Notice::Failure("Could not write it".to_owned()),
149 ]);
150 assert_eq!(
151 rows.iter().map(|(_, at)| *at).collect::<Vec<_>>(),
152 vec![Some(0), None, Some(1)],
153 );
154 }
155
156 #[test]
159 fn a_standing_fact_carries_no_acknowledgement() {
160 let rows = acknowledgements(&[Notice::Standing("Read-only".to_owned())]);
161 assert_eq!(rows, vec![("Read-only".to_owned(), None)]);
162 }
163}