New Greeting Tab + New File Structure

This commit is contained in:
2026-01-22 12:57:35 +01:00
parent 881363e703
commit ba72e8118f
14 changed files with 503 additions and 445 deletions

View File

@@ -1,5 +1,5 @@
use crate::navbar::Navbar;
use crate::pages::{chat::Chat, home::Home, news::News};
use crate::pages::{greet::Greet, home::Home, news::News};
use yew::prelude::*;
use yew_router::prelude::*;
@@ -9,8 +9,8 @@ pub enum Route {
Home,
#[at("/news")]
News,
#[at("/chat")]
Chat,
#[at("/greet")]
Greet,
#[not_found]
#[at("/404")]
NotFound,
@@ -20,7 +20,7 @@ fn switch(routes: Route) -> Html {
match routes {
Route::Home => html! { <Home /> },
Route::News => html! { <News /> },
Route::Chat => html! { <Chat /> },
Route::Greet => html! { <Greet /> },
Route::NotFound => html! { <h1 class="status-msg">{ "404 - Not Found" }</h1> },
}
}

View File

@@ -23,10 +23,10 @@ pub fn navbar() -> Html {
</Link<Route>>
<Link<Route>
to={Route::Chat}
classes={classes!("nav-link", if route == Route::Chat { "active" } else { "" })}
to={Route::Greet}
classes={classes!("nav-link", if route == Route::Greet { "active" } else { "" })}
>
{ "Chat" }
{ "Greet" }
</Link<Route>>
</nav>
}

View File

@@ -1,190 +0,0 @@
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>
}
}

38
src/pages/greet.rs Normal file
View File

@@ -0,0 +1,38 @@
use js_sys::Math;
use ron::de::from_str;
use yew::prelude::*; // Nutzt die Browser-API für Zufall
#[function_component(Greet)]
pub fn greet() -> Html {
let message = use_state(|| "".to_string());
// Inkludiert die RON-Daten direkt in die .wasm Datei
let greetings_data = include_str!("../../assets/data/greetings.ron");
// Wir parsen die Daten einmalig beim Rendern der Komponente
let messages: Vec<String> = from_str(greetings_data)
.unwrap_or_else(|_| vec!["Fehler beim Laden der Sprüche...".to_string()]);
let onclick = {
let message = message.clone();
let messages = messages.clone();
Callback::from(move |_| {
if !messages.is_empty() {
// Sicherer Zufall via JavaScript Math API (funktioniert immer in WASM)
let idx = (Math::random() * messages.len() as f64).floor() as usize;
if let Some(new_msg) = messages.get(idx) {
message.set(new_msg.clone());
}
}
})
};
html! {
<div class="robot-container" {onclick}>
<div class="robot-icon">{"🤖"}</div>
if !(*message).is_empty() {
<p class="bubble">{ (*message).clone() }</p>
}
</div>
}
}

View File

@@ -1,3 +1,3 @@
pub mod chat;
pub mod greet;
pub mod home;
pub mod news;