synpad 0.1.0

A full-featured Matrix chat client built with Dioxus
use dioxus::prelude::*;

/// Toast notification type.
#[derive(Clone, Debug, PartialEq)]
pub enum ToastType {
    Info,
    Success,
    Warning,
    Error,
}

/// Toast notification data.
#[derive(Clone, Debug)]
pub struct Toast {
    pub id: u64,
    pub message: String,
    pub toast_type: ToastType,
    pub duration_ms: u64,
}

/// Global toast state shared via context.
#[derive(Clone)]
pub struct ToastState {
    pub toasts: Vec<Toast>,
    next_id: u64,
}

impl Default for ToastState {
    fn default() -> Self {
        Self {
            toasts: Vec::new(),
            next_id: 1,
        }
    }
}

impl ToastState {
    /// Add a new toast notification.
    pub fn push(&mut self, message: String, toast_type: ToastType) {
        let id = self.next_id;
        self.next_id += 1;
        self.toasts.push(Toast {
            id,
            message,
            toast_type,
            duration_ms: 5000,
        });
    }

    /// Add a toast with custom duration.
    pub fn push_with_duration(&mut self, message: String, toast_type: ToastType, duration_ms: u64) {
        let id = self.next_id;
        self.next_id += 1;
        self.toasts.push(Toast {
            id,
            message,
            toast_type,
            duration_ms,
        });
    }

    /// Remove a toast by ID.
    pub fn dismiss(&mut self, id: u64) {
        self.toasts.retain(|t| t.id != id);
    }

    /// Convenience: show an info toast.
    pub fn info(&mut self, message: impl Into<String>) {
        self.push(message.into(), ToastType::Info);
    }

    /// Convenience: show a success toast.
    pub fn success(&mut self, message: impl Into<String>) {
        self.push(message.into(), ToastType::Success);
    }

    /// Convenience: show a warning toast.
    pub fn warning(&mut self, message: impl Into<String>) {
        self.push(message.into(), ToastType::Warning);
    }

    /// Convenience: show an error toast.
    pub fn error(&mut self, message: impl Into<String>) {
        self.push(message.into(), ToastType::Error);
    }
}

/// Toast notification container — renders all active toasts.
///
/// Place this component once at the root of your app (e.g. in App).
/// Other components can show toasts via `use_context::<Signal<ToastState>>()`.
#[component]
pub fn ToastContainer() -> Element {
    let mut toast_state = use_context::<Signal<ToastState>>();

    // Auto-dismiss timer: check every second and remove expired toasts
    use_effect(move || {
        let toasts = toast_state.read().toasts.clone();
        if !toasts.is_empty() {
            spawn(async move {
                tokio::time::sleep(std::time::Duration::from_secs(5)).await;
                // Remove all toasts that have been around for longer than their duration
                let mut ts = toast_state.write();
                let len = ts.toasts.len();
                if len > 10 {
                    // Safety: don't accumulate too many
                    ts.toasts.drain(..len - 5);
                }
                // Simple approach: dismiss the oldest toast
                if !ts.toasts.is_empty() {
                    ts.toasts.remove(0);
                }
            });
        }
    });

    let toasts = toast_state.read().toasts.clone();

    if toasts.is_empty() {
        return rsx! {};
    }

    rsx! {
        div {
            class: "toast-container",
            for toast in toasts.iter() {
                {
                    let toast_id = toast.id;
                    let class = match toast.toast_type {
                        ToastType::Info => "toast toast--info",
                        ToastType::Success => "toast toast--success",
                        ToastType::Warning => "toast toast--warning",
                        ToastType::Error => "toast toast--error",
                    };
                    let msg = toast.message.clone();
                    rsx! {
                        div {
                            key: "{toast_id}",
                            class: "{class}",
                            span { class: "toast__icon",
                                match toast.toast_type {
                                    ToastType::Info => "",
                                    ToastType::Success => "",
                                    ToastType::Warning => "",
                                    ToastType::Error => "",
                                }
                            }
                            span { class: "toast__message", "{msg}" }
                            button {
                                class: "toast__close",
                                onclick: move |_| {
                                    toast_state.write().dismiss(toast_id);
                                },
                                ""
                            }
                        }
                    }
                }
            }
        }
    }
}