new hashtag filter option

This commit is contained in:
2026-01-31 13:06:47 +01:00
parent e355c1014b
commit 122bac702c
4 changed files with 186 additions and 31 deletions

Submodule src-tauri/easy-nostr updated: 7cebc490ca...1becf76264

View File

@@ -11,9 +11,9 @@ pub struct LocalPost {
}
#[tauri::command]
pub async fn fetch_nostr_posts() -> Result<Vec<LocalPost>, String> {
pub async fn fetch_nostr_posts(hashtags: Vec<String>) -> Result<Vec<LocalPost>, String> {
println!("Fetching Nostr posts for hashtags: {:?}", hashtags);
// 1. Temporären Einweg-Schlüssel generieren
// Das erzeugt ein Schlüsselpaar im RAM, das nach dem Funktionsaufruf verschwindet.
let random_keys = Keys::generate();
let temp_nsec = random_keys
.secret_key()
@@ -34,8 +34,14 @@ pub async fn fetch_nostr_posts() -> Result<Vec<LocalPost>, String> {
.await
.map_err(|e| e.to_string())?;
// 4. Posts von der Library holen
let raw_posts = easy.get_random_posts().await.map_err(|e| e.to_string())?;
// 4. Posts von der Library holen - Entweder per Hashtag oder Random
let raw_posts = if hashtags.is_empty() {
easy.get_random_posts().await.map_err(|e| e.to_string())?
} else {
easy.get_posts_by_hashtags(hashtags)
.await
.map_err(|e| e.to_string())?
};
// 5. Mappen: Library-Typ -> Unser serialisierbarer Typ
let mapped_posts = raw_posts

View File

@@ -7,8 +7,8 @@ use yew::prelude::*;
// Tauri 'invoke' Funktion deklarieren
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"])]
async fn invoke(cmd: &str, args: JsValue) -> JsValue;
#[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"], catch)]
async fn invoke(cmd: &str, args: JsValue) -> Result<JsValue, JsValue>;
}
// Wir definieren Post lokal, damit Yew weiß, wie die JSON-Daten vom Backend aussehen
@@ -24,13 +24,17 @@ pub struct Post {
pub fn home() -> Html {
let posts = use_state(|| None::<Vec<Post>>);
let error = use_state(|| false);
let hashtag_input = use_state(|| String::new());
let active_tags = use_state(Vec::<String>::new);
let load_posts = {
let posts = posts.clone();
let error = error.clone();
let active_tags = active_tags.clone();
Callback::from(move |_e: MouseEvent| {
let posts = posts.clone();
let error = error.clone();
let tags = (*active_tags).clone();
// UI zurücksetzen & Scrollen
posts.set(None);
@@ -43,15 +47,26 @@ pub fn home() -> Html {
}
spawn_local(async move {
// Daten vom Rust-Backend anfordern
let res = invoke("fetch_nostr_posts", JsValue::NULL).await;
#[derive(Serialize)]
struct HashtagArgs {
hashtags: Vec<String>,
}
let args = serde_wasm_bindgen::to_value(&HashtagArgs { hashtags: tags }).unwrap();
// Daten vom Rust-Backend anfordern
match invoke("fetch_nostr_posts", args).await {
Ok(res) => {
// JSON-Resultat in Vec<Post> umwandeln
if let Ok(new_posts) = serde_wasm_bindgen::from_value::<Vec<Post>>(res) {
posts.set(Some(new_posts));
} else {
error.set(true);
}
}
Err(_) => {
error.set(true);
}
}
});
})
};
@@ -68,11 +83,74 @@ pub fn home() -> Html {
html! {
<div class="feed-container">
<div class="feed-header">
<div class="header-main">
<h1 class="feed-title">{"✨ Entdecken"}</h1>
<button class="reload-btn" onclick={load_posts.clone()}>
{"🔄"}
</button>
</div>
</div>
<div class="hashtag-section">
<div class="hashtag-input-row">
<input
type="text"
class="hashtag-input"
placeholder="Tags hinzufügen (z.B. bitcoin, rust)..."
value={(*hashtag_input).clone()}
oninput={
let hashtag_input = hashtag_input.clone();
move |e: InputEvent| {
let target: web_sys::HtmlInputElement = e.target_unchecked_into();
hashtag_input.set(target.value());
}
}
onkeydown={
let hashtag_input = hashtag_input.clone();
let active_tags = active_tags.clone();
let load_posts = load_posts.clone();
move |e: KeyboardEvent| {
if e.key() == "Enter" {
let val = (*hashtag_input).trim().to_string();
if !val.is_empty() {
let mut tags = (*active_tags).clone();
// Mehrere Tags per Komma trennen
for t in val.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()) {
if !tags.contains(&t.to_string()) {
tags.push(t.to_string());
}
}
active_tags.set(tags);
hashtag_input.set(String::new());
load_posts.emit(MouseEvent::new("click").unwrap());
}
}
}
}
/>
</div>
if !active_tags.is_empty() {
<div class="tag-chips">
{ for active_tags.iter().map(|t| {
let t_clone = t.clone();
let active_tags = active_tags.clone();
let load_posts = load_posts.clone();
let remove = move |_| {
let mut tags = (*active_tags).clone();
tags.retain(|x| x != &t_clone);
active_tags.set(tags);
load_posts.emit(MouseEvent::new("click").unwrap());
};
html! {
<span class="tag-chip" onclick={remove}>
{ format!("#{}", t) }
<span class="tag-remove">{"×"}</span>
</span>
}
}) }
</div>
}
</div>
{
if *error {

View File

@@ -10,13 +10,15 @@
--text-primary: #e2e2e7;
--text-secondary: #a0a0b0;
--accent-color: #4a90e2;
--nav-bg: rgba(30, 30, 46, 0.95); /* Fallback for blur */
--nav-bg: rgba(30, 30, 46, 0.95);
/* Fallback for blur */
--danger: #ef4444;
}
* {
box-sizing: border-box;
-webkit-tap-highlight-color: transparent; /* UX-Finishing: No blue flash */
-webkit-tap-highlight-color: transparent;
/* UX-Finishing: No blue flash */
}
/* --- Global & Layout --- */
@@ -35,7 +37,8 @@ body {
display: flex;
justify-content: center;
min-height: 100vh;
overflow-x: hidden; /* Performance: Prevent horizontal scroll */
overflow-x: hidden;
/* Performance: Prevent horizontal scroll */
-webkit-tap-highlight-color: transparent;
}
@@ -48,8 +51,7 @@ html {
width: 100%;
max-width: 600px;
/* Safe Areas: Top inset and mobile-friendly padding */
padding: calc(20px + env(safe-area-inset-top)) 16px
calc(120px + env(safe-area-inset-bottom)) 16px;
padding: calc(20px + env(safe-area-inset-top)) 16px calc(120px + env(safe-area-inset-bottom)) 16px;
}
@media (min-width: 768px) {
@@ -93,7 +95,8 @@ html {
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
/* Touch-Ergonomie: Min 44px effective height */
padding: 10px 12px;
user-select: none; /* UX-Finishing */
user-select: none;
/* UX-Finishing */
display: flex;
align-items: center;
justify-content: center;
@@ -358,10 +361,12 @@ html {
/* --- Animations --- */
@keyframes float {
0%,
100% {
transform: translateY(0px);
}
50% {
transform: translateY(-15px);
}
@@ -372,6 +377,7 @@ html {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
@@ -381,11 +387,9 @@ html {
/* --- Utilities --- */
.reload-btn-large {
cursor: pointer;
background: linear-gradient(
135deg,
background: linear-gradient(135deg,
rgba(74, 144, 226, 0.15),
rgba(74, 144, 226, 0.05)
);
rgba(74, 144, 226, 0.05));
color: var(--accent-color);
padding: 16px;
border-radius: 16px;
@@ -418,9 +422,11 @@ html {
0% {
box-shadow: 0 0 0 0 rgba(74, 144, 226, 0.4);
}
70% {
box-shadow: 0 0 0 10px rgba(74, 144, 226, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(74, 144, 226, 0);
}
@@ -513,9 +519,74 @@ html {
opacity: 0.8;
}
/* --- Hashtag Section --- */
.hashtag-section {
margin-bottom: 24px;
}
.hashtag-input-row {
display: flex;
gap: 10px;
}
.hashtag-input {
flex: 1;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 12px 16px;
color: white;
font-size: 0.95rem;
outline: none;
transition: all 0.2s;
}
.hashtag-input:focus {
border-color: var(--accent-color);
background: rgba(255, 255, 255, 0.08);
}
.tag-chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
}
.tag-chip {
background: rgba(74, 144, 226, 0.15);
color: var(--accent-color);
border: 1px solid rgba(74, 144, 226, 0.3);
padding: 4px 12px;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.2s;
}
.tag-chip:hover {
background: rgba(74, 144, 226, 0.25);
transform: scale(1.05);
}
.tag-remove {
font-size: 1.1rem;
line-height: 1;
opacity: 0.6;
}
.tag-chip:hover .tag-remove {
opacity: 1;
}
/* --- Layout Adjustment für Provider Switch --- */
.header-main {
display: flex;
align-items: center;
gap: 12px;
justify-content: space-between;
width: 100%;
}