//! Notification history component with LocalStorage persistence. //! //! Shows last 100 notifications across sessions. use leptos::prelude::*; use serde::{Deserialize, Serialize}; use uuid::Uuid; use super::chat_types::ChatMessage; use super::modals::Modal; const STORAGE_KEY: &str = "chattyness_notification_history"; const MAX_HISTORY_SIZE: usize = 100; /// A stored notification entry for history. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct HistoryEntry { pub id: Uuid, pub sender_name: String, pub content: String, pub timestamp: i64, pub is_whisper: bool, /// Type of notification (e.g., "whisper", "system", "mod"). pub notification_type: String, } impl HistoryEntry { /// Create from a whisper chat message. pub fn from_whisper(msg: &ChatMessage) -> Self { Self { id: Uuid::new_v4(), sender_name: msg.display_name.clone(), content: msg.content.clone(), timestamp: msg.timestamp, is_whisper: true, notification_type: "whisper".to_string(), } } } /// Load history from LocalStorage. #[cfg(feature = "hydrate")] pub fn load_history() -> Vec { let window = match web_sys::window() { Some(w) => w, None => return Vec::new(), }; let storage = match window.local_storage() { Ok(Some(s)) => s, _ => return Vec::new(), }; let json = match storage.get_item(STORAGE_KEY) { Ok(Some(j)) => j, _ => return Vec::new(), }; serde_json::from_str(&json).unwrap_or_default() } /// Save history to LocalStorage. #[cfg(feature = "hydrate")] pub fn save_history(history: &[HistoryEntry]) { let window = match web_sys::window() { Some(w) => w, None => return, }; let storage = match window.local_storage() { Ok(Some(s)) => s, _ => return, }; if let Ok(json) = serde_json::to_string(history) { let _ = storage.set_item(STORAGE_KEY, &json); } } /// Add an entry to history, maintaining max size. #[cfg(feature = "hydrate")] pub fn add_to_history(entry: HistoryEntry) { let mut history = load_history(); history.insert(0, entry); if history.len() > MAX_HISTORY_SIZE { history.truncate(MAX_HISTORY_SIZE); } save_history(&history); } /// SSR stubs #[cfg(not(feature = "hydrate"))] pub fn load_history() -> Vec { Vec::new() } #[cfg(not(feature = "hydrate"))] pub fn save_history(_history: &[HistoryEntry]) {} #[cfg(not(feature = "hydrate"))] pub fn add_to_history(_entry: HistoryEntry) {} /// Notification history modal component. #[component] pub fn NotificationHistoryModal( #[prop(into)] open: Signal, on_close: Callback<()>, /// Callback when user wants to reply to a message. on_reply: Callback, /// Callback when user wants to see conversation context. on_context: Callback, ) -> impl IntoView { // Load history when modal opens let history = Signal::derive(move || { if open.get() { load_history() } else { Vec::new() } }); view! {
"No notifications yet"

} >
    {sender_name} {format_timestamp(entry.timestamp)} "whisper"

    {entry.content.clone()}

    } } } />
// Footer hint
"Press " "Esc" " to close"
} } /// Format a timestamp for display. fn format_timestamp(timestamp: i64) -> String { #[cfg(feature = "hydrate")] { let date = js_sys::Date::new(&wasm_bindgen::JsValue::from_f64(timestamp as f64)); let hours = date.get_hours(); let minutes = date.get_minutes(); format!("{:02}:{:02}", hours, minutes) } #[cfg(not(feature = "hydrate"))] { let _ = timestamp; String::new() } }