Fixed the message loading for allready sendet messages (bug for loading self sended messages)
This commit is contained in:
Submodule src-tauri/easy-nostr updated: 9c50d8a3ba...7cebc490ca
@@ -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
|
||||||
|
let easy =
|
||||||
|
EasyNostr::new("nsec1rz87pcjnhcl9yfkyq2pn3mlptluvxdgdgw6fyhhg3zmzw2zpwn0sm2q82f")
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
// Nutze eine breite Auswahl an stabilen Relays
|
|
||||||
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?;
|
let client = get_client(&state).await?;
|
||||||
|
|
||||||
// Wir versuchen es bis zu 2 mal, falls das erste Mal 0 zurückkommt (Nostr-Latenz)
|
// easy-nostr now handles the 10s timeout and heavy filtering internally
|
||||||
let mut messages = client.get_private_messages(&clean_npub).await.unwrap_or_default();
|
let messages = client
|
||||||
|
.get_private_messages(&clean_npub)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
if messages.is_empty() {
|
// Map to frontend structure
|
||||||
sleep(Duration::from_millis(500)).await;
|
let chat_msgs: Vec<ChatMessage> = messages
|
||||||
messages = client.get_private_messages(&clean_npub).await.unwrap_or_default();
|
.into_iter()
|
||||||
}
|
.map(|m| ChatMessage {
|
||||||
|
id: m.id.to_string(),
|
||||||
let mut chat_msgs: Vec<ChatMessage> = messages.into_iter().map(|m| ChatMessage {
|
|
||||||
content: m.content,
|
content: m.content,
|
||||||
is_incoming: m.is_incoming,
|
is_incoming: m.is_incoming,
|
||||||
created_at: m.created_at.as_secs(),
|
created_at: m.created_at.as_secs(), // Convert Timestamp to u64
|
||||||
}).collect();
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
chat_msgs.sort_by_key(|m| m.created_at);
|
// Sorting is already done in easy-nostr, but we ensure it here too
|
||||||
println!("[BACKEND] {} Nachrichten für {} geladen.", chat_msgs.len(), clean_npub);
|
|
||||||
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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,57 +23,80 @@ 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_clean
|
receiver_npub: npub,
|
||||||
}).unwrap();
|
})
|
||||||
|
.unwrap();
|
||||||
let fetched: JsValue = invoke("get_nostr_messages", args).await;
|
let fetched: JsValue = invoke("get_nostr_messages", args).await;
|
||||||
|
|
||||||
if let Ok(mut new_msgs) = serde_wasm_bindgen::from_value::<Vec<ChatMessage>>(fetched) {
|
if let Ok(relay_msgs) =
|
||||||
new_msgs.sort_by_key(|m| m.created_at);
|
serde_wasm_bindgen::from_value::<Vec<ChatMessage>>(fetched)
|
||||||
|
{
|
||||||
// WICHTIG FÜR NEUSTART:
|
if !relay_msgs.is_empty() {
|
||||||
// Wenn lokal 0, aber Relay > 0 -> Sofort laden
|
|
||||||
let current_len = (*messages).len();
|
|
||||||
if current_len == 0 && !new_msgs.is_empty() {
|
|
||||||
messages.set(new_msgs);
|
|
||||||
relay_connected.set(true);
|
|
||||||
} else if new_msgs.len() > current_len {
|
|
||||||
messages.set(new_msgs);
|
|
||||||
relay_connected.set(true);
|
|
||||||
} else if !new_msgs.is_empty() {
|
|
||||||
relay_connected.set(true);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -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();
|
||||||
@@ -107,8 +131,10 @@ pub fn chat() -> Html {
|
|||||||
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>
|
||||||
|
|||||||
Reference in New Issue
Block a user