diff --git a/Cargo.toml b/Cargo.toml index 042a8e1..101b805 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ js-sys = "0.3" serde = { version = "1", features = ["derive"] } serde-wasm-bindgen = "0.6" console_error_panic_hook = "0.1.7" +serde_json = "1.0.148" [workspace] members = ["src-tauri"] diff --git a/src-tauri/src/chat.rs b/src-tauri/src/chat.rs new file mode 100644 index 0000000..404d01b --- /dev/null +++ b/src-tauri/src/chat.rs @@ -0,0 +1,64 @@ +use easy_nostr::EasyNostr; +use tauri::State; +use tokio::sync::Mutex; + +// State-Struktur, die in lib.rs mit .manage() registriert wird +pub struct NostrState(pub Mutex>); + +#[tauri::command] +pub async fn send_nostr_message( + content: String, + 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", + ]) + .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()) + } +} diff --git a/src-tauri/src/home.rs b/src-tauri/src/home.rs index 48c85d8..a804da3 100644 --- a/src-tauri/src/home.rs +++ b/src-tauri/src/home.rs @@ -12,7 +12,7 @@ pub struct LocalPost { #[tauri::command] pub async fn fetch_nostr_posts() -> Result, String> { // 1. Verbindung aufbauen - let easy = EasyNostr::new("nsec1...") + let easy = EasyNostr::new("nsec1fkhszd5sv8yp6g966qdh5kuph25g4nn9pa2k5rwpuglt6rde8u8qwr3r87") .await .map_err(|e| e.to_string())?; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 1baa150..b38ea89 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,10 +1,18 @@ +mod chat; mod home; +use tokio::sync::Mutex; // Wichtig für den State + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) - .invoke_handler(tauri::generate_handler![home::fetch_nostr_posts]) + // DIESE ZEILE HINZUGEFÜGT: Registriert den State für alle Commands + .manage(chat::NostrState(Mutex::new(None))) + .invoke_handler(tauri::generate_handler![ + home::fetch_nostr_posts, + chat::send_nostr_message + ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src/pages/chat.rs b/src/pages/chat.rs index 0799951..d79d89b 100644 --- a/src/pages/chat.rs +++ b/src/pages/chat.rs @@ -1,10 +1,106 @@ +use serde::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; +} + +// DAS HIER IST DIE WICHTIGE ÄNDERUNG: +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct ChatArgs { + content: String, + receiver_npub: String, // Wird jetzt als "receiverNpub" gesendet +} + #[function_component(Chat)] pub fn chat() -> Html { + let messages = use_state(|| vec![]); + let input_ref = use_node_ref(); + let recipient_ref = use_node_ref(); + let chat_bottom_ref = use_node_ref(); + + // Autoscroll + { + let chat_bottom_ref = chat_bottom_ref.clone(); + let messages = messages.clone(); + use_effect_with(messages, 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 + let mut current = (*messages).clone(); + current.push(content.clone()); + 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; + }); + } + }) + }; + html! { -
-

{"Chat Page"}

+
+
+

{"💬 Chat"}

+
+ +
+ +
+ +
+ { for (*messages).iter().map(|msg| html! { +
+
{"Du • Jetzt"}
+
{ msg }
+
+ }) } +
+
+ +
+ + +
} } diff --git a/styles.css b/styles.css index cf35309..defd248 100644 --- a/styles.css +++ b/styles.css @@ -1,4 +1,4 @@ -/* --- Basis & Layout --- */ +/* --- Global & Layout --- */ body { background-color: #0f0f13; color: #e2e2e7; @@ -7,119 +7,22 @@ body { -apple-system, sans-serif; margin: 0; - padding: 0; line-height: 1.5; display: flex; justify-content: center; } +html { + scroll-behavior: smooth; +} + .feed-container { width: 100%; max-width: 600px; - margin: 0 auto; - /* 150px unten, damit der Content nicht hinter der Navbar verschwindet */ - padding: 40px 20px 150px 20px; + padding: 40px 20px 150px 20px; /* 150px unten Platz für das Dock */ } -/* --- Header & Titel --- */ -.feed-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 30px; -} - -.feed-title { - font-size: 1.8rem; - font-weight: 800; - letter-spacing: -0.02em; - margin: 0; -} - -/* --- Post Karten --- */ -.posts-list { - display: flex; - flex-direction: column; - gap: 16px; -} - -.post-card { - background: rgba(255, 255, 255, 0.03); - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 16px; - padding: 20px; - transition: all 0.2s ease; -} - -.post-card:hover { - background: rgba(255, 255, 255, 0.05); - border-color: rgba(255, 255, 255, 0.15); -} - -.post-header { - display: flex; - justify-content: space-between; - margin-bottom: 12px; - font-size: 0.85rem; -} - -.post-author { - color: #4a90e2; - font-weight: 700; -} - -.post-date { - color: #63636e; -} - -.post-content { - word-wrap: break-word; - white-space: pre-wrap; - font-size: 1.05rem; -} - -/* --- Buttons (Reload) --- */ -.reload-btn { - background: rgba(255, 255, 255, 0.05); - border: 1px solid rgba(255, 255, 255, 0.1); - color: #e2e2e7; - font-size: 1.2rem; - padding: 8px 12px; - border-radius: 12px; - cursor: pointer; - transition: all 0.2s ease; -} - -.reload-btn:hover { - background: rgba(255, 255, 255, 0.1); - transform: rotate(15deg) scale(1.1); -} - -.footer-action { - display: flex; - justify-content: center; - padding: 40px 0; -} - -.reload-btn-large { - background: rgba(74, 144, 226, 0.1); - color: #4a90e2; - border: 1px solid rgba(74, 144, 226, 0.3); - padding: 14px 28px; - border-radius: 16px; - font-weight: 600; - cursor: pointer; - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); - width: 100%; -} - -.reload-btn-large:hover { - background: rgba(74, 144, 226, 0.2); - box-shadow: 0 5px 15px rgba(74, 144, 226, 0.2); - transform: translateY(-2px); -} - -/* --- Navbar Dock (Fixed) --- */ +/* --- Navigation Dock --- */ .navbar-dock { position: fixed; bottom: 30px; @@ -133,7 +36,6 @@ body { border: 1px solid rgba(255, 255, 255, 0.1); display: flex; gap: 30px; - align-items: center; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3); z-index: 1000; } @@ -142,24 +44,178 @@ body { color: #a0a0b0; text-decoration: none; font-weight: 600; - font-size: 1.2rem; - display: flex; - flex-direction: column; - align-items: center; transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); } .nav-link:hover { color: #ffffff; - transform: translateY(-5px) scale(1.1); + transform: translateY(-3px); } .nav-link.active { color: #4a90e2; } +/* --- Content & Cards --- */ +.feed-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; +} + +.post-card { + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.08); + 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 { + color: #4a90e2; + font-weight: 700; + font-size: 0.85rem; +} + +.post-content { + white-space: pre-wrap; + word-wrap: break-word; + margin-top: 10px; +} + +/* --- 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; +} + +.reload-btn-large:hover { + background: rgba(74, 144, 226, 0.2); + transform: translateY(-2px); +} + .status-msg { text-align: center; color: #63636e; - margin-top: 60px; + margin-top: 40px; +} + +/* --- Chat Layout --- */ +.chat-wrapper { + display: flex; + flex-direction: column; + min-height: 70vh; +} + +.chat-list { + display: flex; + flex-direction: column; + gap: 24px; + margin-bottom: 100px; +} + +.chat-item { + border-left: 2px solid rgba(74, 144, 226, 0.3); + padding-left: 16px; + transition: border-color 0.3s ease; +} + +.chat-item:hover { + border-left-color: #4a90e2; +} + +.chat-meta { + font-size: 0.75rem; + color: #63636e; + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 4px; +} + +.chat-content { + font-size: 1.1rem; + color: #e2e2e7; +} + +/* --- Floating Input Field --- */ +.chat-input-container { + position: fixed; + bottom: 110px; /* Platziert über dem Navbar-Dock */ + left: 50%; + transform: translateX(-50%); + width: 100%; + max-width: 500px; + display: flex; + gap: 10px; + background: rgba(15, 15, 19, 0.8); + backdrop-filter: blur(10px); + padding: 10px; + border-radius: 20px; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.chat-input { + flex: 1; + background: transparent; + border: none; + color: white; + padding: 10px 15px; + font-size: 1rem; + outline: none; +} + +.chat-send-btn { + background: #4a90e2; + color: white; + border: none; + width: 40px; + height: 40px; + border-radius: 12px; + 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-box { + margin-bottom: 20px; + 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; + width: 100%; + padding: 10px 0; + outline: none; }