191 lines
7.3 KiB
Rust
191 lines
7.3 KiB
Rust
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<String>, // 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::<ChatMessage>::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::<HtmlInputElement>() {
|
|
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::<Vec<ChatMessage>>(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::<web_sys::HtmlElement>() {
|
|
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::<HtmlInputElement>().unwrap();
|
|
let recipient = recipient_ref.cast::<HtmlInputElement>().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! {
|
|
<div class="chat-wrapper">
|
|
<header class="feed-header">
|
|
<h1 class="feed-title">{"💬 Nostr Chat"}</h1>
|
|
<div class="relay-status">
|
|
<span class={if *relay_connected { "status-dot online" } else { "status-dot offline" }}></span>
|
|
<small>{if *relay_connected { " Relays aktiv" } else { " Verbinde..." }}</small>
|
|
</div>
|
|
</header>
|
|
<div class="recipient-box">
|
|
<input ref={recipient_ref} type="text" placeholder="Empfänger npub..." class="recipient-input" />
|
|
</div>
|
|
<div class="chat-list">
|
|
{ for (*messages).iter().map(|msg| html! {
|
|
<div class={classes!(
|
|
if msg.is_incoming { "chat-item incoming" } else { "chat-item outgoing" },
|
|
if msg.id.is_none() { "pending" } else { "confirmed" } // Optional: CSS für "Sende..." Status
|
|
)}>
|
|
<div class="chat-meta">
|
|
{ if msg.is_incoming { "Empfangen" } else { "Du" } }
|
|
{ if msg.id.is_none() { " (sendet...)" } else { "" } }
|
|
</div>
|
|
<div class="chat-content">{ &msg.content }</div>
|
|
</div>
|
|
}) }
|
|
<div ref={chat_bottom_ref}></div>
|
|
</div>
|
|
<form class="chat-input-container" onsubmit={on_send}>
|
|
<input ref={input_ref} type="text" placeholder="Nachricht schreiben..." class="chat-input" autocomplete="off" />
|
|
<button type="submit" class="chat-send-btn">{"↑"}</button>
|
|
</form>
|
|
</div>
|
|
}
|
|
}
|