From 497b39e0aecf4087243c6f481b258274ab0c34ba Mon Sep 17 00:00:00 2001 From: spinline Date: Thu, 5 Feb 2026 20:48:40 +0300 Subject: [PATCH] fix(frontend): use signal-based toast for async contexts --- .../src/components/torrent/add_torrent.rs | 16 +++--- frontend/src/components/torrent/table.rs | 14 +++-- frontend/src/store.rs | 53 +++++++++++-------- 3 files changed, 51 insertions(+), 32 deletions(-) diff --git a/frontend/src/components/torrent/add_torrent.rs b/frontend/src/components/torrent/add_torrent.rs index 54bb278..03d6c4a 100644 --- a/frontend/src/components/torrent/add_torrent.rs +++ b/frontend/src/components/torrent/add_torrent.rs @@ -1,6 +1,7 @@ use leptos::*; use leptos::html::Dialog; -use crate::store::{toast_success, toast_error, toast_warning}; +use crate::store::{show_toast_with_signal, TorrentStore}; +use shared::NotificationLevel; #[component] @@ -8,6 +9,9 @@ pub fn AddTorrentModal( #[prop(into)] on_close: Callback<()>, ) -> impl IntoView { + let store = use_context::().expect("TorrentStore not provided"); + let notifications = store.notifications; + let dialog_ref = create_node_ref::(); let (uri, set_uri) = create_signal(String::new()); let (is_loading, set_loading) = create_signal(false); @@ -23,7 +27,7 @@ pub fn AddTorrentModal( let handle_submit = move |_| { let uri_val = uri.get(); if uri_val.is_empty() { - toast_warning("Lütfen bir Magnet URI veya URL girin"); + show_toast_with_signal(notifications, NotificationLevel::Warning, "Lütfen bir Magnet URI veya URL girin"); set_error_msg.set(Some("Please enter a Magnet URI or URL".to_string())); return; } @@ -44,7 +48,7 @@ pub fn AddTorrentModal( Ok(resp) => { if resp.ok() { logging::log!("Torrent added successfully"); - toast_success("Torrent eklendi"); + show_toast_with_signal(notifications, NotificationLevel::Success, "Torrent eklendi"); set_loading.set(false); if let Some(dialog) = dialog_ref.get() { dialog.close(); @@ -54,14 +58,14 @@ pub fn AddTorrentModal( let status = resp.status(); let text = resp.text().await.unwrap_or_default(); logging::error!("Failed to add torrent: {} - {}", status, text); - toast_error("Torrent eklenemedi"); + show_toast_with_signal(notifications, NotificationLevel::Error, "Torrent eklenemedi"); set_error_msg.set(Some(format!("Error {}: {}", status, text))); set_loading.set(false); } } Err(e) => { logging::error!("Network error: {}", e); - toast_error("Bağlantı hatası"); + show_toast_with_signal(notifications, NotificationLevel::Error, "Bağlantı hatası"); set_error_msg.set(Some(format!("Network Error: {}", e))); set_loading.set(false); } @@ -69,7 +73,7 @@ pub fn AddTorrentModal( } Err(e) => { logging::error!("Serialization error: {}", e); - toast_error("İstek hatası"); + show_toast_with_signal(notifications, NotificationLevel::Error, "İstek hatası"); set_error_msg.set(Some(format!("Request Error: {}", e))); set_loading.set(false); } diff --git a/frontend/src/components/torrent/table.rs b/frontend/src/components/torrent/table.rs index 4424f43..8e5033e 100644 --- a/frontend/src/components/torrent/table.rs +++ b/frontend/src/components/torrent/table.rs @@ -1,7 +1,8 @@ use leptos::*; use wasm_bindgen::closure::Closure; use wasm_bindgen::JsCast; -use crate::store::{get_action_messages, toast_success, toast_error}; +use crate::store::{get_action_messages, show_toast_with_signal}; +use shared::NotificationLevel; fn format_bytes(bytes: i64) -> String { const UNITS: [&str; 6] = ["B", "KB", "MB", "GB", "TB", "PB"]; @@ -186,6 +187,9 @@ pub fn TorrentTable() -> impl IntoView { let (success_msg, error_msg) = get_action_messages(&action); let success_msg = success_msg.to_string(); let error_msg = error_msg.to_string(); + + // Capture notifications signal before async (use_context unavailable in spawn_local) + let notifications = store.notifications; spawn_local(async move { let action_req = if action == "delete_with_data" { @@ -210,20 +214,20 @@ pub fn TorrentTable() -> impl IntoView { resp.status(), resp.status_text() ); - toast_error(error_msg); + show_toast_with_signal(notifications, NotificationLevel::Error, error_msg); } else { logging::log!("Action {} executed successfully", action); - toast_success(success_msg); + show_toast_with_signal(notifications, NotificationLevel::Success, success_msg); } } Err(e) => { logging::error!("Network error executing action: {}", e); - toast_error(format!("{}: Bağlantı hatası", error_msg)); + show_toast_with_signal(notifications, NotificationLevel::Error, format!("{}: Bağlantı hatası", error_msg)); } }, Err(e) => { logging::error!("Failed to serialize request: {}", e); - toast_error(error_msg); + show_toast_with_signal(notifications, NotificationLevel::Error, error_msg); } } }); diff --git a/frontend/src/store.rs b/frontend/src/store.rs index 3045f08..fef66dd 100644 --- a/frontend/src/store.rs +++ b/frontend/src/store.rs @@ -13,46 +13,57 @@ pub struct NotificationItem { // Toast Helper Functions (Clean Code: Single Responsibility) // ============================================================================ +/// Shows a toast notification using a direct signal reference. +/// Use this version inside async blocks (spawn_local) where use_context is unavailable. +/// Auto-removes after 5 seconds. +pub fn show_toast_with_signal( + notifications: RwSignal>, + level: NotificationLevel, + message: impl Into, +) { + let id = js_sys::Date::now() as u64; + let notification = SystemNotification { + level, + message: message.into(), + }; + let item = NotificationItem { id, notification }; + + notifications.update(|list| list.push(item)); + + // Auto-remove after 5 seconds + let _ = set_timeout( + move || { + notifications.update(|list| list.retain(|i| i.id != id)); + }, + std::time::Duration::from_secs(5), + ); +} + /// Shows a toast notification with the given level and message. +/// Only works within reactive scope (components, effects). For async, use show_toast_with_signal. /// Auto-removes after 5 seconds. pub fn show_toast(level: NotificationLevel, message: impl Into) { if let Some(store) = use_context::() { - let id = js_sys::Date::now() as u64; - let notification = SystemNotification { - level, - message: message.into(), - }; - let item = NotificationItem { id, notification }; - - store.notifications.update(|list| list.push(item)); - - // Auto-remove after 5 seconds - let notifications = store.notifications; - let _ = set_timeout( - move || { - notifications.update(|list| list.retain(|i| i.id != id)); - }, - std::time::Duration::from_secs(5), - ); + show_toast_with_signal(store.notifications, level, message); } } -/// Convenience function for success toasts +/// Convenience function for success toasts (reactive scope only) pub fn toast_success(message: impl Into) { show_toast(NotificationLevel::Success, message); } -/// Convenience function for error toasts +/// Convenience function for error toasts (reactive scope only) pub fn toast_error(message: impl Into) { show_toast(NotificationLevel::Error, message); } -/// Convenience function for info toasts +/// Convenience function for info toasts (reactive scope only) pub fn toast_info(message: impl Into) { show_toast(NotificationLevel::Info, message); } -/// Convenience function for warning toasts +/// Convenience function for warning toasts (reactive scope only) pub fn toast_warning(message: impl Into) { show_toast(NotificationLevel::Warning, message); }