use gloo_timers::callback::Interval; use js_sys::Date; use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::spawn_local; use web_sys::HtmlInputElement; use yew::prelude::*; #[wasm_bindgen] extern "C" { #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"])] async fn invoke(cmd: &str, args: JsValue) -> JsValue; } #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] #[serde(rename_all = "camelCase")] pub struct ChatMessage { pub id: Option, // ID ist None für optimistische Nachrichten pub content: String, pub is_incoming: bool, pub created_at: u64, } #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct ChatArgs { content: String, receiver_npub: String, } #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct GetArgs { receiver_npub: String, } #[function_component(Chat)] pub fn chat() -> Html { let messages = use_state(|| Vec::::new()); let relay_connected = use_state(|| false); let recipient_ref = use_node_ref(); let input_ref = use_node_ref(); let chat_bottom_ref = use_node_ref(); // --- Polling Logic mit Smart Merge --- { let messages = messages.clone(); let relay_connected = relay_connected.clone(); let recipient_ref = recipient_ref.clone(); use_effect(move || { let interval = Interval::new(3000, move || { let messages = messages.clone(); let relay_connected = relay_connected.clone(); if let Some(recipient) = recipient_ref.cast::() { let npub = recipient.value().trim().to_string(); if npub.is_empty() { return; } spawn_local(async move { let args = serde_wasm_bindgen::to_value(&GetArgs { receiver_npub: npub, }) .unwrap(); let fetched: JsValue = invoke("get_nostr_messages", args).await; if let Ok(relay_msgs) = serde_wasm_bindgen::from_value::>(fetched) { if !relay_msgs.is_empty() { relay_connected.set(true); } let current_local_msgs = (*messages).clone(); // SMART MERGE LOGIC: // 1. Take all messages from the relay. // 2. Add local "optimistic" messages (id == None) that haven't appeared in relay_msgs yet. let mut merged = relay_msgs.clone(); for local_msg in current_local_msgs.iter().filter(|m| m.id.is_none()) { // Check if this message is now confirmed (by matching content) let already_confirmed = relay_msgs.iter().any(|rm| rm.content == local_msg.content); if !already_confirmed { merged.push(local_msg.clone()); } } merged.sort_by_key(|m| m.created_at); // Only update state if something actually changed to avoid flickering if merged.len() != current_local_msgs.len() || merged.last().map(|m| &m.id) != current_local_msgs.last().map(|m| &m.id) { messages.set(merged); } } }); } }); move || drop(interval) }); } // --- Autoscroll --- { let chat_bottom_ref = chat_bottom_ref.clone(); let messages_len = messages.len(); use_effect_with(messages_len, move |_| { if let Some(element) = chat_bottom_ref.cast::() { element.scroll_into_view(); } || () }); } let on_send = { let messages = messages.clone(); let input_ref = input_ref.clone(); let recipient_ref = recipient_ref.clone(); Callback::from(move |e: SubmitEvent| { e.prevent_default(); let input = input_ref.cast::().unwrap(); let recipient = recipient_ref.cast::().unwrap(); let content = input.value(); let receiver_npub = recipient.value(); if !content.trim().is_empty() && !receiver_npub.trim().is_empty() { // Optimistic UI Update: Nachricht sofort ohne ID anzeigen let mut current = (*messages).clone(); current.push(ChatMessage { id: None, content: content.clone(), is_incoming: false, created_at: (Date::now() / 1000.0) as u64, }); messages.set(current); input.set_value(""); spawn_local(async move { let args = serde_wasm_bindgen::to_value(&ChatArgs { content, receiver_npub, }) .unwrap(); let _ = invoke("send_nostr_message", args).await; }); } }) }; html! {

{"💬 Nostr Chat"}

{if *relay_connected { " Relays aktiv" } else { " Verbinde..." }}
{ for (*messages).iter().map(|msg| html! {
{ if msg.is_incoming { "Empfangen" } else { "Du" } } { if msg.id.is_none() { " (sendet...)" } else { "" } }
{ &msg.content }
}) }
} }