Fixed the message loading for allready sendet messages (bug for loading self sended messages)

This commit is contained in:
2026-01-16 22:00:19 +01:00
parent c164a4ed1b
commit 881363e703
3 changed files with 113 additions and 84 deletions

Submodule src-tauri/easy-nostr updated: 9c50d8a3ba...7cebc490ca

View File

@@ -1,8 +1,8 @@
use easy_nostr::EasyNostr; use easy_nostr::EasyNostr;
use serde::Serialize; use serde::Serialize;
use std::sync::Arc;
use tauri::State; use tauri::State;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use std::sync::Arc;
use tokio::time::{sleep, Duration}; use tokio::time::{sleep, Duration};
pub struct NostrState(pub Mutex<Option<Arc<EasyNostr>>>); pub struct NostrState(pub Mutex<Option<Arc<EasyNostr>>>);
@@ -10,6 +10,7 @@ pub struct NostrState(pub Mutex<Option<Arc<EasyNostr>>>);
#[derive(Serialize)] #[derive(Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ChatMessage { pub struct ChatMessage {
pub id: String,
pub content: String, pub content: String,
pub is_incoming: bool, pub is_incoming: bool,
pub created_at: u64, pub created_at: u64,
@@ -18,26 +19,27 @@ pub struct ChatMessage {
async fn get_client(state: &State<'_, NostrState>) -> Result<Arc<EasyNostr>, String> { async fn get_client(state: &State<'_, NostrState>) -> Result<Arc<EasyNostr>, String> {
let mut guard = state.0.lock().await; let mut guard = state.0.lock().await;
if guard.is_none() { if guard.is_none() {
println!("[BACKEND] Verbinde zu Nostr Relays..."); println!("[BACKEND] Initializing EasyNostr...");
let easy = EasyNostr::new("nsec1rz87pcjnhcl9yfkyq2pn3mlptluvxdgdgw6fyhhg3zmzw2zpwn0sm2q82f") // Use your private key
.await let easy =
.map_err(|e| e.to_string())?; EasyNostr::new("nsec1rz87pcjnhcl9yfkyq2pn3mlptluvxdgdgw6fyhhg3zmzw2zpwn0sm2q82f")
.await
// Nutze eine breite Auswahl an stabilen Relays .map_err(|e| e.to_string())?;
easy.add_relays(vec![ easy.add_relays(vec![
"wss://relay.damus.io", "wss://relay.damus.io",
"wss://nos.lol", "wss://nos.lol",
"wss://relay.snort.social", "wss://relay.snort.social",
"wss://relay.malxte.de", "wss://relay.primal.net",
"wss://relay.0xchat.com", "wss://nostr.wine",
"wss://nostr.wine" ])
]).await.map_err(|e| e.to_string())?; .await
.map_err(|e| e.to_string())?;
*guard = Some(Arc::new(easy)); *guard = Some(Arc::new(easy));
// WICHTIG: Erhöhte Wartezeit beim Kaltstart, damit Relays DMs senden können // Short sleep to allow initial connection
println!("[BACKEND] Warte auf Relay-Handshake..."); sleep(Duration::from_millis(1500)).await;
sleep(Duration::from_millis(2000)).await;
} }
Ok(guard.as_ref().unwrap().clone()) Ok(guard.as_ref().unwrap().clone())
} }
@@ -48,26 +50,30 @@ pub async fn get_nostr_messages(
state: State<'_, NostrState>, state: State<'_, NostrState>,
) -> Result<Vec<ChatMessage>, String> { ) -> Result<Vec<ChatMessage>, String> {
let clean_npub = receiver_npub.trim().trim_matches('#').to_string(); let clean_npub = receiver_npub.trim().trim_matches('#').to_string();
if clean_npub.is_empty() { return Ok(vec![]); } if clean_npub.is_empty() {
return Ok(vec![]);
let client = get_client(&state).await?;
// Wir versuchen es bis zu 2 mal, falls das erste Mal 0 zurückkommt (Nostr-Latenz)
let mut messages = client.get_private_messages(&clean_npub).await.unwrap_or_default();
if messages.is_empty() {
sleep(Duration::from_millis(500)).await;
messages = client.get_private_messages(&clean_npub).await.unwrap_or_default();
} }
let mut chat_msgs: Vec<ChatMessage> = messages.into_iter().map(|m| ChatMessage { let client = get_client(&state).await?;
content: m.content,
is_incoming: m.is_incoming,
created_at: m.created_at.as_secs(),
}).collect();
chat_msgs.sort_by_key(|m| m.created_at); // easy-nostr now handles the 10s timeout and heavy filtering internally
println!("[BACKEND] {} Nachrichten für {} geladen.", chat_msgs.len(), clean_npub); let messages = client
.get_private_messages(&clean_npub)
.await
.map_err(|e| e.to_string())?;
// Map to frontend structure
let chat_msgs: Vec<ChatMessage> = messages
.into_iter()
.map(|m| ChatMessage {
id: m.id.to_string(),
content: m.content,
is_incoming: m.is_incoming,
created_at: m.created_at.as_secs(), // Convert Timestamp to u64
})
.collect();
// Sorting is already done in easy-nostr, but we ensure it here too
Ok(chat_msgs) Ok(chat_msgs)
} }
@@ -79,8 +85,11 @@ pub async fn send_nostr_message(
) -> Result<String, String> { ) -> Result<String, String> {
let clean_npub = receiver_npub.trim().trim_matches('#').to_string(); let clean_npub = receiver_npub.trim().trim_matches('#').to_string();
let client = get_client(&state).await?; let client = get_client(&state).await?;
let event_id = client.send_private_message(&clean_npub, &content)
let event_id = client
.send_private_message(&clean_npub, &content)
.await .await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
Ok(event_id.to_string()) Ok(event_id.to_string())
} }

View File

@@ -1,10 +1,10 @@
use gloo_timers::callback::Interval;
use js_sys::Date;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local; use wasm_bindgen_futures::spawn_local;
use web_sys::HtmlInputElement; use web_sys::HtmlInputElement;
use yew::prelude::*; use yew::prelude::*;
use gloo_timers::callback::Interval;
use js_sys::Date;
#[wasm_bindgen] #[wasm_bindgen]
extern "C" { extern "C" {
@@ -15,6 +15,7 @@ extern "C" {
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ChatMessage { pub struct ChatMessage {
pub id: Option<String>, // ID ist None für optimistische Nachrichten
pub content: String, pub content: String,
pub is_incoming: bool, pub is_incoming: bool,
pub created_at: u64, pub created_at: u64,
@@ -22,58 +23,81 @@ pub struct ChatMessage {
#[derive(Serialize)] #[derive(Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct ChatArgs { content: String, receiver_npub: String } struct ChatArgs {
content: String,
receiver_npub: String,
}
#[derive(Serialize)] #[derive(Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct GetArgs { receiver_npub: String } struct GetArgs {
receiver_npub: String,
}
#[function_component(Chat)] #[function_component(Chat)]
pub fn chat() -> Html { pub fn chat() -> Html {
let messages = use_state(|| Vec::<ChatMessage>::new()); let messages = use_state(|| Vec::<ChatMessage>::new());
let relay_connected = use_state(|| false); // Status für den Punkt let relay_connected = use_state(|| false);
let recipient_ref = use_node_ref(); let recipient_ref = use_node_ref();
let input_ref = use_node_ref(); let input_ref = use_node_ref();
let chat_bottom_ref = use_node_ref(); let chat_bottom_ref = use_node_ref();
// Polling Logik & Status Check // --- Polling Logic mit Smart Merge ---
{ {
let messages = messages.clone(); let messages = messages.clone();
let relay_connected = relay_connected.clone(); let relay_connected = relay_connected.clone();
let recipient_ref = recipient_ref.clone(); let recipient_ref = recipient_ref.clone();
use_effect(move || { use_effect(move || {
let interval = Interval::new(3000, move || { let interval = Interval::new(3000, move || {
let messages = messages.clone(); let messages = messages.clone();
let relay_connected = relay_connected.clone(); let relay_connected = relay_connected.clone();
if let Some(recipient) = recipient_ref.cast::<HtmlInputElement>() { if let Some(recipient) = recipient_ref.cast::<HtmlInputElement>() {
let npub = recipient.value(); let npub = recipient.value().trim().to_string();
let npub_clean = npub.trim().to_string(); if npub.is_empty() {
return;
}
spawn_local(async move { spawn_local(async move {
if !npub_clean.is_empty() { let args = serde_wasm_bindgen::to_value(&GetArgs {
let args = serde_wasm_bindgen::to_value(&GetArgs { receiver_npub: npub,
receiver_npub: npub_clean })
}).unwrap(); .unwrap();
let fetched: JsValue = invoke("get_nostr_messages", args).await;
let fetched: JsValue = invoke("get_nostr_messages", args).await;
if let Ok(relay_msgs) =
if let Ok(mut new_msgs) = serde_wasm_bindgen::from_value::<Vec<ChatMessage>>(fetched) { serde_wasm_bindgen::from_value::<Vec<ChatMessage>>(fetched)
new_msgs.sort_by_key(|m| m.created_at); {
if !relay_msgs.is_empty() {
// WICHTIG FÜR NEUSTART: relay_connected.set(true);
// Wenn lokal 0, aber Relay > 0 -> Sofort laden }
let current_len = (*messages).len();
if current_len == 0 && !new_msgs.is_empty() { let current_local_msgs = (*messages).clone();
messages.set(new_msgs);
relay_connected.set(true); // SMART MERGE LOGIC:
} else if new_msgs.len() > current_len { // 1. Take all messages from the relay.
messages.set(new_msgs); // 2. Add local "optimistic" messages (id == None) that haven't appeared in relay_msgs yet.
relay_connected.set(true); let mut merged = relay_msgs.clone();
} else if !new_msgs.is_empty() {
relay_connected.set(true); 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);
}
} }
}); });
} }
@@ -82,7 +106,7 @@ pub fn chat() -> Html {
}); });
} }
// Autoscroll // --- Autoscroll ---
{ {
let chat_bottom_ref = chat_bottom_ref.clone(); let chat_bottom_ref = chat_bottom_ref.clone();
let messages_len = messages.len(); let messages_len = messages.len();
@@ -102,13 +126,15 @@ pub fn chat() -> Html {
e.prevent_default(); e.prevent_default();
let input = input_ref.cast::<HtmlInputElement>().unwrap(); let input = input_ref.cast::<HtmlInputElement>().unwrap();
let recipient = recipient_ref.cast::<HtmlInputElement>().unwrap(); let recipient = recipient_ref.cast::<HtmlInputElement>().unwrap();
let content = input.value(); let content = input.value();
let receiver_npub = recipient.value(); let receiver_npub = recipient.value();
if !content.trim().is_empty() && !receiver_npub.trim().is_empty() { if !content.trim().is_empty() && !receiver_npub.trim().is_empty() {
// Optimistic UI Update: Nachricht sofort ohne ID anzeigen
let mut current = (*messages).clone(); let mut current = (*messages).clone();
current.push(ChatMessage { current.push(ChatMessage {
id: None,
content: content.clone(), content: content.clone(),
is_incoming: false, is_incoming: false,
created_at: (Date::now() / 1000.0) as u64, created_at: (Date::now() / 1000.0) as u64,
@@ -117,7 +143,11 @@ pub fn chat() -> Html {
input.set_value(""); input.set_value("");
spawn_local(async move { spawn_local(async move {
let args = serde_wasm_bindgen::to_value(&ChatArgs { content, receiver_npub }).unwrap(); let args = serde_wasm_bindgen::to_value(&ChatArgs {
content,
receiver_npub,
})
.unwrap();
let _ = invoke("send_nostr_message", args).await; let _ = invoke("send_nostr_message", args).await;
}); });
} }
@@ -133,36 +163,26 @@ pub fn chat() -> Html {
<small>{if *relay_connected { " Relays aktiv" } else { " Verbinde..." }}</small> <small>{if *relay_connected { " Relays aktiv" } else { " Verbinde..." }}</small>
</div> </div>
</header> </header>
<div class="recipient-box"> <div class="recipient-box">
<input <input ref={recipient_ref} type="text" placeholder="Empfänger npub..." class="recipient-input" />
ref={recipient_ref}
type="text"
placeholder="Empfänger npub..."
class="recipient-input"
/>
</div> </div>
<div class="chat-list"> <div class="chat-list">
{ for (*messages).iter().map(|msg| html! { { for (*messages).iter().map(|msg| html! {
<div class={if msg.is_incoming { "chat-item incoming" } else { "chat-item outgoing" }}> <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"> <div class="chat-meta">
{ if msg.is_incoming { "Empfangen" } else { "Du" } } { if msg.is_incoming { "Empfangen" } else { "Du" } }
{ if msg.id.is_none() { " (sendet...)" } else { "" } }
</div> </div>
<div class="chat-content">{ &msg.content }</div> <div class="chat-content">{ &msg.content }</div>
</div> </div>
}) } }) }
<div ref={chat_bottom_ref}></div> <div ref={chat_bottom_ref}></div>
</div> </div>
<form class="chat-input-container" onsubmit={on_send}> <form class="chat-input-container" onsubmit={on_send}>
<input <input ref={input_ref} type="text" placeholder="Nachricht schreiben..." class="chat-input" autocomplete="off" />
ref={input_ref}
type="text"
placeholder="Nachricht schreiben..."
class="chat-input"
autocomplete="off"
/>
<button type="submit" class="chat-send-btn">{""}</button> <button type="submit" class="chat-send-btn">{""}</button>
</form> </form>
</div> </div>