diff --git a/Cargo.toml b/Cargo.toml index 101b805..f08e0ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ serde = { version = "1", features = ["derive"] } serde-wasm-bindgen = "0.6" console_error_panic_hook = "0.1.7" serde_json = "1.0.148" +gloo-timers = "0.3.0" [workspace] members = ["src-tauri"] diff --git a/src-tauri/src/chat.rs b/src-tauri/src/chat.rs index 404d01b..fbb2f3c 100644 --- a/src-tauri/src/chat.rs +++ b/src-tauri/src/chat.rs @@ -1,9 +1,75 @@ use easy_nostr::EasyNostr; +use serde::Serialize; use tauri::State; use tokio::sync::Mutex; +use std::sync::Arc; +use tokio::time::{sleep, Duration}; -// State-Struktur, die in lib.rs mit .manage() registriert wird -pub struct NostrState(pub Mutex>); +pub struct NostrState(pub Mutex>>); + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChatMessage { + pub content: String, + pub is_incoming: bool, + pub created_at: u64, +} + +async fn get_client(state: &State<'_, NostrState>) -> Result, String> { + let mut guard = state.0.lock().await; + if guard.is_none() { + println!("[BACKEND] Verbinde zu Nostr Relays..."); + let easy = EasyNostr::new("nsec1rz87pcjnhcl9yfkyq2pn3mlptluvxdgdgw6fyhhg3zmzw2zpwn0sm2q82f") + .await + .map_err(|e| e.to_string())?; + + // Nutze eine breite Auswahl an stabilen Relays + easy.add_relays(vec![ + "wss://relay.damus.io", + "wss://nos.lol", + "wss://relay.snort.social", + "wss://relay.malxte.de", + "wss://relay.0xchat.com", + "wss://nostr.wine" + ]).await.map_err(|e| e.to_string())?; + + *guard = Some(Arc::new(easy)); + + // WICHTIG: Erhöhte Wartezeit beim Kaltstart, damit Relays DMs senden können + println!("[BACKEND] Warte auf Relay-Handshake..."); + sleep(Duration::from_millis(2000)).await; + } + Ok(guard.as_ref().unwrap().clone()) +} + +#[tauri::command] +pub async fn get_nostr_messages( + receiver_npub: String, + state: State<'_, NostrState>, +) -> Result, String> { + let clean_npub = receiver_npub.trim().trim_matches('#').to_string(); + 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 = messages.into_iter().map(|m| ChatMessage { + 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); + println!("[BACKEND] {} Nachrichten für {} geladen.", chat_msgs.len(), clean_npub); + Ok(chat_msgs) +} #[tauri::command] pub async fn send_nostr_message( @@ -11,54 +77,10 @@ pub async fn send_nostr_message( receiver_npub: String, state: State<'_, NostrState>, ) -> Result { - let mut guard = state.0.lock().await; - - // Lazy Initialization: Erstelle den Client nur, wenn er noch nicht existiert - if guard.is_none() { - println!("[NOSTR] Initialisiere neuen Client..."); - let easy = - EasyNostr::new("nsec1rz87pcjnhcl9yfkyq2pn3mlptluvxdgdgw6fyhhg3zmzw2zpwn0sm2q82f") - .await - .map_err(|e| { - println!("[NOSTR] Fehler bei Key-Initialisierung: {}", e); - e.to_string() - })?; - - println!("[NOSTR] Verbinde zu Relays..."); - easy.add_relays(vec![ - //"wss://relay.damus.io", - //"wss://nos.lol", - //"wss://relay.snort.social", - "wss://relay.malxte.de", - ]) + let clean_npub = receiver_npub.trim().trim_matches('#').to_string(); + let client = get_client(&state).await?; + let event_id = client.send_private_message(&clean_npub, &content) .await - .map_err(|e| { - println!("[NOSTR] Relay-Fehler: {}", e); - e.to_string() - })?; - - *guard = Some(easy); - println!("[NOSTR] Client bereit."); - - // Kleine Pause für den Verbindungsaufbau der Relays - tokio::time::sleep(std::time::Duration::from_millis(500)).await; - } - - if let Some(easy) = guard.as_ref() { - println!("[NOSTR] Sende Private Message an: {}", receiver_npub); - - match easy.send_private_message(&receiver_npub, &content).await { - Ok(event_id) => { - let id_str = event_id.to_string(); - println!("[NOSTR] ERFOLG! ID: {}", id_str); - Ok(id_str) - } - Err(e) => { - println!("[NOSTR] VERSAND FEHLGESCHLAGEN: {:?}", e); - Err(format!("Versand-Fehler: {}", e)) - } - } - } else { - Err("Client konnte nicht initialisiert werden".to_string()) - } + .map_err(|e| e.to_string())?; + Ok(event_id.to_string()) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b38ea89..8cc8974 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -11,7 +11,8 @@ pub fn run() { .manage(chat::NostrState(Mutex::new(None))) .invoke_handler(tauri::generate_handler![ home::fetch_nostr_posts, - chat::send_nostr_message + chat::send_nostr_message, + chat::get_nostr_messages ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/pages/chat.rs b/src/pages/chat.rs index d79d89b..c816423 100644 --- a/src/pages/chat.rs +++ b/src/pages/chat.rs @@ -1,8 +1,10 @@ -use serde::Serialize; +use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::spawn_local; use web_sys::HtmlInputElement; use yew::prelude::*; +use gloo_timers::callback::Interval; +use js_sys::Date; #[wasm_bindgen] extern "C" { @@ -10,26 +12,81 @@ extern "C" { async fn invoke(cmd: &str, args: JsValue) -> JsValue; } -// DAS HIER IST DIE WICHTIGE ÄNDERUNG: +#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ChatMessage { + pub content: String, + pub is_incoming: bool, + pub created_at: u64, +} + #[derive(Serialize)] #[serde(rename_all = "camelCase")] -struct ChatArgs { - content: String, - receiver_npub: String, // Wird jetzt als "receiverNpub" gesendet -} +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![]); - let input_ref = use_node_ref(); + let messages = use_state(|| Vec::::new()); + let relay_connected = use_state(|| false); // Status für den Punkt let recipient_ref = use_node_ref(); + let input_ref = use_node_ref(); let chat_bottom_ref = use_node_ref(); + // Polling Logik & Status Check + { + 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(); + let npub_clean = npub.trim().to_string(); + + spawn_local(async move { + if !npub_clean.is_empty() { + let args = serde_wasm_bindgen::to_value(&GetArgs { + receiver_npub: npub_clean + }).unwrap(); + + let fetched: JsValue = invoke("get_nostr_messages", args).await; + + if let Ok(mut new_msgs) = serde_wasm_bindgen::from_value::>(fetched) { + new_msgs.sort_by_key(|m| m.created_at); + + // WICHTIG FÜR NEUSTART: + // 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); + } + } + } + }); + } + }); + move || drop(interval) + }); + } + // Autoscroll { let chat_bottom_ref = chat_bottom_ref.clone(); - let messages = messages.clone(); - use_effect_with(messages, move |_| { + let messages_len = messages.len(); + use_effect_with(messages_len, move |_| { if let Some(element) = chat_bottom_ref.cast::() { element.scroll_into_view(); } @@ -41,32 +98,27 @@ pub fn chat() -> Html { 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 let mut current = (*messages).clone(); - current.push(content.clone()); + current.push(ChatMessage { + 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(); - - // Der Aufruf wird jetzt funktionieren, da die Keys matchen - invoke("send_nostr_message", args).await; + let args = serde_wasm_bindgen::to_value(&ChatArgs { content, receiver_npub }).unwrap(); + let _ = invoke("send_nostr_message", args).await; }); } }) @@ -75,30 +127,42 @@ pub fn chat() -> Html { html! {
-

{"💬 Chat"}

+

{"💬 Nostr Chat"}

+
+ + {if *relay_connected { " Relays aktiv" } else { " Verbinde..." }} +
-
{ for (*messages).iter().map(|msg| html! { -
-
{"Du • Jetzt"}
-
{ msg }
+
+
+ { if msg.is_incoming { "Empfangen" } else { "Du" } } +
+
{ &msg.content }
}) }
- +
diff --git a/styles.css b/styles.css index defd248..1c9b7f9 100644 --- a/styles.css +++ b/styles.css @@ -2,10 +2,7 @@ body { background-color: #0f0f13; color: #e2e2e7; - font-family: - "Inter", - -apple-system, - sans-serif; + font-family: "Inter", -apple-system, sans-serif; margin: 0; line-height: 1.5; display: flex; @@ -19,7 +16,7 @@ html { .feed-container { width: 100%; max-width: 600px; - padding: 40px 20px 150px 20px; /* 150px unten Platz für das Dock */ + padding: 40px 20px 150px 20px; } /* --- Navigation Dock --- */ @@ -70,11 +67,6 @@ html { border-radius: 16px; padding: 20px; margin-bottom: 16px; - transition: background 0.2s ease; -} - -.post-card:hover { - background: rgba(255, 255, 255, 0.05); } .post-author { @@ -90,26 +82,16 @@ html { } /* --- Interaction Elements --- */ -.reload-btn, .reload-btn-large { cursor: pointer; - border: 1px solid rgba(74, 144, 226, 0.3); - transition: all 0.2s ease; -} - -.reload-btn { - background: rgba(255, 255, 255, 0.05); - border-radius: 12px; - padding: 8px 12px; -} - -.reload-btn-large { background: rgba(74, 144, 226, 0.1); color: #4a90e2; padding: 14px; border-radius: 16px; width: 100%; font-weight: 600; + border: 1px solid rgba(74, 144, 226, 0.3); + transition: all 0.2s ease; } .reload-btn-large:hover { @@ -117,13 +99,7 @@ html { transform: translateY(-2px); } -.status-msg { - text-align: center; - color: #63636e; - margin-top: 40px; -} - -/* --- Chat Layout --- */ +/* --- Chat Layout (Neu & Verbessert) --- */ .chat-wrapper { display: flex; flex-direction: column; @@ -133,48 +109,69 @@ html { .chat-list { display: flex; flex-direction: column; - gap: 24px; - margin-bottom: 100px; + gap: 12px; + margin-bottom: 180px; /* Platz für Input-Container */ + padding: 10px 0; } .chat-item { - border-left: 2px solid rgba(74, 144, 226, 0.3); - padding-left: 16px; - transition: border-color 0.3s ease; + max-width: 85%; + padding: 12px 16px; + border-radius: 18px; + display: flex; + flex-direction: column; } -.chat-item:hover { - border-left-color: #4a90e2; +/* Ausrichtung der Bubbles */ +.chat-item.outgoing { + align-self: flex-end; + background: rgba(74, 144, 226, 0.2); + border: 1px solid rgba(74, 144, 226, 0.3); + border-bottom-right-radius: 4px; +} + +.chat-item.incoming { + align-self: flex-start; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-bottom-left-radius: 4px; } .chat-meta { - font-size: 0.75rem; + font-size: 0.65rem; color: #63636e; text-transform: uppercase; - letter-spacing: 0.05em; margin-bottom: 4px; + font-weight: 700; +} + +.chat-item.outgoing .chat-meta { + color: #4a90e2; + text-align: right; } .chat-content { - font-size: 1.1rem; + font-size: 1rem; color: #e2e2e7; + word-wrap: break-word; } -/* --- Floating Input Field --- */ +/* --- Floating Chat Input --- */ .chat-input-container { position: fixed; - bottom: 110px; /* Platziert über dem Navbar-Dock */ + bottom: 110px; left: 50%; transform: translateX(-50%); - width: 100%; + width: 90%; max-width: 500px; display: flex; gap: 10px; - background: rgba(15, 15, 19, 0.8); - backdrop-filter: blur(10px); - padding: 10px; - border-radius: 20px; + background: rgba(20, 20, 25, 0.9); + backdrop-filter: blur(15px); + padding: 12px; + border-radius: 24px; border: 1px solid rgba(255, 255, 255, 0.1); + z-index: 900; } .chat-input { @@ -182,8 +179,7 @@ html { background: transparent; border: none; color: white; - padding: 10px 15px; - font-size: 1rem; + padding: 0 10px; outline: none; } @@ -193,29 +189,57 @@ html { border: none; width: 40px; height: 40px; - border-radius: 12px; + border-radius: 50%; cursor: pointer; font-weight: bold; - font-size: 1.2rem; - transition: transform 0.2s ease; -} - -.chat-send-btn:hover { - transform: scale(1.05); - background: #357abd; } +/* --- Recipient UI --- */ .recipient-box { - margin-bottom: 20px; + position: sticky; + top: 0; + background: #0f0f13; + z-index: 100; + padding-bottom: 10px; border-bottom: 1px solid rgba(255, 255, 255, 0.05); } .recipient-input { - background: transparent; - border: none; - color: #4a90e2; /* Blau markiert als Ziel */ - font-size: 0.9rem; + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 12px; + color: #4a90e2; + font-family: monospace; + font-size: 0.85rem; width: 100%; - padding: 10px 0; + padding: 10px 15px; outline: none; + box-sizing: border-box; +} + +.relay-status { + display: flex; + align-items: center; + gap: 8px; + background: rgba(255, 255, 255, 0.05); + padding: 4px 12px; + border-radius: 20px; + font-size: 0.7rem; +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + display: inline-block; +} + +.status-dot.online { + background-color: #4ade80; /* Grün */ + box-shadow: 0 0 8px #4ade80; +} + +.status-dot.offline { + background-color: #facc15; /* Gelb/Orange für "Connecting" */ + box-shadow: 0 0 8px #facc15; }