use crate::prelude::{Action, Alert, AlertGroup, AlertType};
use chrono::{DateTime, Utc};
use core::cmp::Reverse;
use gloo_timers::callback::Timeout;
use std::fmt::Display;
use std::{collections::BinaryHeap, time::Duration};
use yew::{prelude::*, virtual_dom::VChild};
#[derive(Clone, Debug, Default)]
pub struct Toast {
pub title: String,
pub r#type: AlertType,
pub timeout: Option<Duration>,
pub body: Html,
pub actions: Vec<Action>,
}
impl From<&str> for Toast {
fn from(value: &str) -> Self {
Self::from(value.to_string())
}
}
impl From<String> for Toast {
fn from(value: String) -> Self {
Self {
title: value,
timeout: None,
body: Default::default(),
r#type: Default::default(),
actions: Vec::new(),
}
}
}
impl From<&String> for Toast {
fn from(value: &String) -> Self {
Self::from(value.clone())
}
}
pub trait ToToast {
fn to_toast(&self) -> Toast;
}
impl<T> ToToast for T
where
T: Display,
{
fn to_toast(&self) -> Toast {
Toast::from(self.to_string())
}
}
#[doc(hidden)]
#[derive(Debug)]
pub enum ToasterRequest {
Toast(Toast),
}
#[doc(hidden)]
pub enum ToastAction {
ShowToast(Toast),
}
#[derive(Clone, PartialEq)]
pub struct Toaster {
callback: Callback<ToastAction>,
}
impl Toaster {
pub fn toast(&self, toast: impl Into<Toast>) {
self.callback.emit(ToastAction::ShowToast(toast.into()))
}
}
#[derive(Clone, PartialEq, Properties)]
pub struct Props {
pub children: Html,
}
pub struct ToastEntry {
id: usize,
alert: VChild<Alert>,
timeout: Option<DateTime<Utc>>,
}
pub struct ToastViewer {
context: Toaster,
alerts: Vec<ToastEntry>,
counter: usize,
task: Option<Timeout>,
timeouts: BinaryHeap<Reverse<DateTime<Utc>>>,
}
pub enum ToastViewerMsg {
Perform(ToastAction),
Cleanup,
Close(usize),
}
impl Component for ToastViewer {
type Message = ToastViewerMsg;
type Properties = Props;
fn create(ctx: &Context<Self>) -> Self {
let context = Toaster {
callback: ctx.link().callback(ToastViewerMsg::Perform),
};
Self {
context,
alerts: Vec::new(),
counter: 0,
task: None,
timeouts: BinaryHeap::new(),
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
ToastViewerMsg::Perform(action) => self.perform(ctx, action),
ToastViewerMsg::Cleanup => self.cleanup(ctx),
ToastViewerMsg::Close(id) => self.remove_toast(id),
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let context = self.context.clone();
html! {
<ContextProvider<Toaster> {context}>
<AlertGroup toast=true>
{ for self.alerts.iter().map(|entry|entry.alert.clone()) }
</AlertGroup>
{ ctx.props().children.clone() }
</ContextProvider<Toaster>>
}
}
}
impl ToastViewer {
fn now() -> DateTime<Utc> {
Utc::now()
}
fn perform(&mut self, ctx: &Context<Self>, action: ToastAction) -> bool {
match action {
ToastAction::ShowToast(toast) => self.add_toast(ctx, toast),
}
true
}
fn add_toast(&mut self, ctx: &Context<Self>, toast: Toast) {
let now = Self::now();
let timeout = toast
.timeout
.and_then(|timeout| chrono::Duration::from_std(timeout).ok())
.map(|timeout| now + timeout);
let id = self.counter;
self.counter += 1;
let onclose = match toast.timeout {
None => Some(ctx.link().callback(move |_| ToastViewerMsg::Close(id))),
Some(_) => None,
};
self.alerts.push(ToastEntry {
id,
alert: html_nested! {
<Alert r#type={toast.r#type} title={toast.title} onclose={onclose} actions={toast.actions}>
{ toast.body }
</Alert>
},
timeout,
});
if let Some(timeout) = timeout {
self.schedule_cleanup(ctx, timeout);
}
}
fn schedule_cleanup(&mut self, ctx: &Context<Self>, timeout: DateTime<Utc>) {
log::debug!("Schedule cleanup: {:?}", timeout);
self.timeouts.push(Reverse(timeout));
self.trigger_next_cleanup(ctx);
}
fn trigger_next_cleanup(&mut self, ctx: &Context<Self>) {
if self.task.is_some() {
log::debug!("Already have a task");
return;
}
while let Some(next) = self.timeouts.pop() {
let timeout = next.0;
log::debug!("Next timeout: {:?}", timeout);
let duration = timeout - Self::now();
let duration = duration.to_std();
log::debug!("Duration: {:?}", duration);
if let Ok(duration) = duration {
let link = ctx.link().clone();
self.task = Some(Timeout::new(duration.as_millis() as u32, move || {
link.send_message(ToastViewerMsg::Cleanup);
}));
log::debug!("Scheduled cleanup: {:?}", duration);
break;
}
}
}
fn remove_toast(&mut self, id: usize) -> bool {
self.retain_alert(|entry| entry.id != id)
}
fn cleanup(&mut self, ctx: &Context<Self>) -> bool {
let now = Self::now();
self.task = None;
self.trigger_next_cleanup(ctx);
self.retain_alert(|alert| {
if let Some(timeout) = alert.timeout {
timeout > now
} else {
true
}
})
}
fn retain_alert<F>(&mut self, f: F) -> bool
where
F: Fn(&ToastEntry) -> bool,
{
let before = self.alerts.len();
self.alerts.retain(f);
before != self.alerts.len()
}
}
#[hook]
pub fn use_toaster() -> Option<Toaster> {
use_context()
}