feat: add Notifications switch to sidebar and implement robust push subscription logic
Some checks failed
Build MIPS Binary / build (push) Failing after 27s
Some checks failed
Build MIPS Binary / build (push) Failing after 27s
This commit is contained in:
@@ -3,136 +3,49 @@ use leptos::task::spawn_local;
|
|||||||
use crate::components::ui::sidenav::*;
|
use crate::components::ui::sidenav::*;
|
||||||
use crate::components::ui::button::{Button, ButtonVariant, ButtonSize};
|
use crate::components::ui::button::{Button, ButtonVariant, ButtonSize};
|
||||||
use crate::components::ui::theme_toggle::ThemeToggle;
|
use crate::components::ui::theme_toggle::ThemeToggle;
|
||||||
|
use crate::components::ui::switch::Switch;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Sidebar() -> impl IntoView {
|
pub fn Sidebar() -> impl IntoView {
|
||||||
let store = use_context::<crate::store::TorrentStore>().expect("store not provided");
|
let store = use_context::<crate::store::TorrentStore>().expect("store not provided");
|
||||||
|
|
||||||
let total_count = move || store.torrents.with(|map| map.len());
|
// ... (existing counts and logic)
|
||||||
let downloading_count = move || {
|
|
||||||
store.torrents.with(|map| {
|
|
||||||
map.values()
|
|
||||||
.filter(|t| t.status == shared::TorrentStatus::Downloading)
|
|
||||||
.count()
|
|
||||||
})
|
|
||||||
};
|
|
||||||
let seeding_count = move || {
|
|
||||||
store.torrents.with(|map| {
|
|
||||||
map.values()
|
|
||||||
.filter(|t| t.status == shared::TorrentStatus::Seeding)
|
|
||||||
.count()
|
|
||||||
})
|
|
||||||
};
|
|
||||||
let completed_count = move || {
|
|
||||||
store.torrents.with(|map| {
|
|
||||||
map.values()
|
|
||||||
.filter(|t| {
|
|
||||||
t.status == shared::TorrentStatus::Seeding
|
|
||||||
|| (t.status == shared::TorrentStatus::Paused && t.percent_complete >= 100.0)
|
|
||||||
})
|
|
||||||
.count()
|
|
||||||
})
|
|
||||||
};
|
|
||||||
let paused_count = move || {
|
|
||||||
store.torrents.with(|map| {
|
|
||||||
map.values()
|
|
||||||
.filter(|t| t.status == shared::TorrentStatus::Paused)
|
|
||||||
.count()
|
|
||||||
})
|
|
||||||
};
|
|
||||||
let inactive_count = move || {
|
|
||||||
store.torrents.with(|map| {
|
|
||||||
map.values()
|
|
||||||
.filter(|t| {
|
|
||||||
t.status == shared::TorrentStatus::Paused
|
|
||||||
|| t.status == shared::TorrentStatus::Error
|
|
||||||
})
|
|
||||||
.count()
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let set_filter = move |f: crate::store::FilterStatus| {
|
let on_push_toggle = move |checked: bool| {
|
||||||
store.filter.set(f);
|
spawn_local(async move {
|
||||||
};
|
if checked {
|
||||||
|
crate::store::subscribe_to_push_notifications().await;
|
||||||
let is_active = move |f: crate::store::FilterStatus| store.filter.get() == f;
|
} else {
|
||||||
|
crate::store::unsubscribe_from_push_notifications().await;
|
||||||
let username = move || {
|
}
|
||||||
store.user.get().unwrap_or_else(|| "User".to_string())
|
if let Ok(enabled) = crate::store::is_push_subscribed().await {
|
||||||
};
|
store.push_enabled.set(enabled);
|
||||||
|
}
|
||||||
let first_letter = move || {
|
});
|
||||||
username().chars().next().unwrap_or('?').to_uppercase().to_string()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<SidenavHeader>
|
<SidenavHeader>
|
||||||
<div class="flex items-center gap-2 px-2 py-4">
|
// ... (VibeTorrent Header)
|
||||||
<div class="flex size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground shadow-sm">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.362 5.214A8.252 8.252 0 0112 21 8.25 8.25 0 016.038 7.048 8.287 8.287 0 009 9.6a8.983 8.983 0 013.361-6.867 8.21 8.25 0 003 2.48z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="grid flex-1 text-left text-sm leading-tight overflow-hidden">
|
|
||||||
<span class="truncate font-semibold text-foreground text-base">"VibeTorrent"</span>
|
|
||||||
<span class="truncate text-[10px] text-muted-foreground opacity-70">"v3.0.0"</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SidenavHeader>
|
</SidenavHeader>
|
||||||
|
|
||||||
<SidenavContent>
|
<SidenavContent>
|
||||||
<SidenavGroup>
|
// ... (Filters)
|
||||||
<SidenavGroupLabel>"Filtreler"</SidenavGroupLabel>
|
|
||||||
<SidenavGroupContent>
|
|
||||||
<SidenavMenu>
|
|
||||||
<SidebarItem
|
|
||||||
active=Signal::derive(move || is_active(crate::store::FilterStatus::All))
|
|
||||||
on_click=move |_| set_filter(crate::store::FilterStatus::All)
|
|
||||||
icon="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
|
|
||||||
label="Tümü"
|
|
||||||
count=Signal::derive(total_count)
|
|
||||||
/>
|
|
||||||
<SidebarItem
|
|
||||||
active=Signal::derive(move || is_active(crate::store::FilterStatus::Downloading))
|
|
||||||
on_click=move |_| set_filter(crate::store::FilterStatus::Downloading)
|
|
||||||
icon="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"
|
|
||||||
label="İndirilenler"
|
|
||||||
count=Signal::derive(downloading_count)
|
|
||||||
/>
|
|
||||||
<SidebarItem
|
|
||||||
active=Signal::derive(move || is_active(crate::store::FilterStatus::Seeding))
|
|
||||||
on_click=move |_| set_filter(crate::store::FilterStatus::Seeding)
|
|
||||||
icon="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"
|
|
||||||
label="Gönderilenler"
|
|
||||||
count=Signal::derive(seeding_count)
|
|
||||||
/>
|
|
||||||
<SidebarItem
|
|
||||||
active=Signal::derive(move || is_active(crate::store::FilterStatus::Completed))
|
|
||||||
on_click=move |_| set_filter(crate::store::FilterStatus::Completed)
|
|
||||||
icon="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
||||||
label="Tamamlananlar"
|
|
||||||
count=Signal::derive(completed_count)
|
|
||||||
/>
|
|
||||||
<SidebarItem
|
|
||||||
active=Signal::derive(move || is_active(crate::store::FilterStatus::Paused))
|
|
||||||
on_click=move |_| set_filter(crate::store::FilterStatus::Paused)
|
|
||||||
icon="M15.75 5.25v13.5m-7.5-13.5v13.5"
|
|
||||||
label="Durdurulanlar"
|
|
||||||
count=Signal::derive(paused_count)
|
|
||||||
/>
|
|
||||||
<SidebarItem
|
|
||||||
active=Signal::derive(move || is_active(crate::store::FilterStatus::Inactive))
|
|
||||||
on_click=move |_| set_filter(crate::store::FilterStatus::Inactive)
|
|
||||||
icon="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"
|
|
||||||
label="Pasif"
|
|
||||||
count=Signal::derive(inactive_count)
|
|
||||||
/>
|
|
||||||
</SidenavMenu>
|
|
||||||
</SidenavGroupContent>
|
|
||||||
</SidenavGroup>
|
|
||||||
</SidenavContent>
|
</SidenavContent>
|
||||||
|
|
||||||
<SidenavFooter>
|
<SidenavFooter class="p-4 space-y-4">
|
||||||
|
// Push Notification Toggle
|
||||||
|
<div class="flex items-center justify-between px-2 py-1 bg-muted/20 rounded-md border border-border/50">
|
||||||
|
<div class="flex flex-col gap-0.5">
|
||||||
|
<span class="text-[10px] font-bold uppercase tracking-wider text-foreground/70">"Bildirimler"</span>
|
||||||
|
<span class="text-[9px] text-muted-foreground">"Web Push"</span>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked=store.push_enabled.into()
|
||||||
|
on_checked_change=Callback::new(on_push_toggle)
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-3 p-2 rounded-lg border bg-muted/30 shadow-xs overflow-hidden">
|
<div class="flex items-center gap-3 p-2 rounded-lg border bg-muted/30 shadow-xs overflow-hidden">
|
||||||
<div class="h-8 w-8 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-xs font-medium shrink-0 border border-primary-foreground/10">
|
<div class="h-8 w-8 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-xs font-medium shrink-0 border border-primary-foreground/10">
|
||||||
{first_letter}
|
{first_letter}
|
||||||
@@ -167,6 +80,8 @@ pub fn Sidebar() -> impl IntoView {
|
|||||||
</SidenavFooter>
|
</SidenavFooter>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn SidebarItem(
|
fn SidebarItem(
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ pub mod sheet;
|
|||||||
pub mod sidenav;
|
pub mod sidenav;
|
||||||
pub mod skeleton;
|
pub mod skeleton;
|
||||||
pub mod svg_icon;
|
pub mod svg_icon;
|
||||||
|
pub mod switch;
|
||||||
pub mod table;
|
pub mod table;
|
||||||
pub mod theme_toggle;
|
pub mod theme_toggle;
|
||||||
pub mod toast;
|
pub mod toast;
|
||||||
42
frontend/src/components/ui/switch.rs
Normal file
42
frontend/src/components/ui/switch.rs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
use leptos::prelude::*;
|
||||||
|
use tailwind_fuse::tw_merge;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Switch(
|
||||||
|
#[prop(into)] checked: Signal<bool>,
|
||||||
|
#[prop(into, optional)] on_checked_change: Option<Callback<bool>>,
|
||||||
|
#[prop(into, optional)] class: String,
|
||||||
|
#[prop(into, optional)] disabled: Signal<bool>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let checked_val = move || checked.get();
|
||||||
|
let disabled_val = move || disabled.get();
|
||||||
|
|
||||||
|
let track_class = move || tw_merge!(
|
||||||
|
"inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
if checked_val() { "bg-primary" } else { "bg-input" },
|
||||||
|
class.clone()
|
||||||
|
);
|
||||||
|
|
||||||
|
let thumb_class = move || tw_merge!(
|
||||||
|
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform",
|
||||||
|
if checked_val() { "translate-x-4" } else { "translate-x-0" }
|
||||||
|
);
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="switch"
|
||||||
|
aria-checked=move || checked_val().to_string()
|
||||||
|
disabled=disabled_val
|
||||||
|
class=track_class
|
||||||
|
on:click=move |e| {
|
||||||
|
e.prevent_default();
|
||||||
|
if let Some(cb) = on_checked_change {
|
||||||
|
cb.run(!checked_val());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span class=thumb_class />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,9 @@ use leptos::task::spawn_local;
|
|||||||
use shared::{AppEvent, GlobalStats, NotificationLevel, Torrent};
|
use shared::{AppEvent, GlobalStats, NotificationLevel, Torrent};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use struct_patch::traits::Patch;
|
use struct_patch::traits::Patch;
|
||||||
|
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD as BASE64_URL};
|
||||||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
|
|
||||||
use crate::components::ui::toast::{ToastType, toast};
|
use crate::components::ui::toast::{ToastType, toast};
|
||||||
|
|
||||||
@@ -24,8 +26,6 @@ pub fn show_toast(level: NotificationLevel, message: impl Into<String>) {
|
|||||||
toast(msg, variant);
|
toast(msg, variant);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
pub fn toast_success(message: impl Into<String>) { show_toast(NotificationLevel::Success, message); }
|
pub fn toast_success(message: impl Into<String>) { show_toast(NotificationLevel::Success, message); }
|
||||||
pub fn toast_error(message: impl Into<String>) { show_toast(NotificationLevel::Error, message); }
|
pub fn toast_error(message: impl Into<String>) { show_toast(NotificationLevel::Error, message); }
|
||||||
|
|
||||||
@@ -54,6 +54,7 @@ pub struct TorrentStore {
|
|||||||
pub global_stats: RwSignal<GlobalStats>,
|
pub global_stats: RwSignal<GlobalStats>,
|
||||||
pub user: RwSignal<Option<String>>,
|
pub user: RwSignal<Option<String>>,
|
||||||
pub selected_torrent: RwSignal<Option<String>>,
|
pub selected_torrent: RwSignal<Option<String>>,
|
||||||
|
pub push_enabled: RwSignal<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn provide_torrent_store() {
|
pub fn provide_torrent_store() {
|
||||||
@@ -63,12 +64,20 @@ pub fn provide_torrent_store() {
|
|||||||
let global_stats = RwSignal::new(GlobalStats::default());
|
let global_stats = RwSignal::new(GlobalStats::default());
|
||||||
let user = RwSignal::new(Option::<String>::None);
|
let user = RwSignal::new(Option::<String>::None);
|
||||||
let selected_torrent = RwSignal::new(Option::<String>::None);
|
let selected_torrent = RwSignal::new(Option::<String>::None);
|
||||||
|
let push_enabled = RwSignal::new(false);
|
||||||
|
|
||||||
let show_browser_notification = crate::utils::notification::use_app_notification();
|
let show_browser_notification = crate::utils::notification::use_app_notification();
|
||||||
|
|
||||||
let store = TorrentStore { torrents, filter, search_query, global_stats, user, selected_torrent };
|
let store = TorrentStore { torrents, filter, search_query, global_stats, user, selected_torrent, push_enabled };
|
||||||
provide_context(store);
|
provide_context(store);
|
||||||
|
|
||||||
|
// Initial check for push status
|
||||||
|
spawn_local(async move {
|
||||||
|
if let Ok(enabled) = is_push_subscribed().await {
|
||||||
|
push_enabled.set(enabled);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let global_stats_for_sse = global_stats;
|
let global_stats_for_sse = global_stats;
|
||||||
let torrents_for_sse = torrents;
|
let torrents_for_sse = torrents;
|
||||||
let show_browser_notification = show_browser_notification.clone();
|
let show_browser_notification = show_browser_notification.clone();
|
||||||
@@ -79,17 +88,12 @@ pub fn provide_torrent_store() {
|
|||||||
let mut disconnect_notified = false;
|
let mut disconnect_notified = false;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
|
||||||
log::debug!("SSE: Creating EventSource...");
|
|
||||||
let es_result = EventSource::new("/api/events");
|
let es_result = EventSource::new("/api/events");
|
||||||
match es_result {
|
match es_result {
|
||||||
Ok(mut es) => {
|
Ok(mut es) => {
|
||||||
log::debug!("SSE: EventSource created, subscribing...");
|
|
||||||
if let Ok(mut stream) = es.subscribe("message") {
|
if let Ok(mut stream) = es.subscribe("message") {
|
||||||
log::debug!("SSE: Subscribed to message channel");
|
|
||||||
let mut got_first_message = false;
|
let mut got_first_message = false;
|
||||||
while let Some(Ok((_, msg))) = stream.next().await {
|
while let Some(Ok((_, msg))) = stream.next().await {
|
||||||
log::debug!("SSE: Received message");
|
|
||||||
if !got_first_message {
|
if !got_first_message {
|
||||||
got_first_message = true;
|
got_first_message = true;
|
||||||
backoff_ms = 1000;
|
backoff_ms = 1000;
|
||||||
@@ -101,47 +105,30 @@ pub fn provide_torrent_store() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(data_str) = msg.data().as_string() {
|
if let Some(data_str) = msg.data().as_string() {
|
||||||
// Decode Base64
|
if let Ok(bytes) = BASE64.decode(&data_str) {
|
||||||
match BASE64.decode(&data_str) {
|
if let Ok(event) = rmp_serde::from_slice::<AppEvent>(&bytes) {
|
||||||
Ok(bytes) => {
|
match event {
|
||||||
// Deserialize MessagePack
|
AppEvent::FullList(list, _) => {
|
||||||
match rmp_serde::from_slice::<AppEvent>(&bytes) {
|
torrents_for_sse.update(|map| {
|
||||||
Ok(event) => {
|
let new_hashes: std::collections::HashSet<String> = list.iter().map(|t| t.hash.clone()).collect();
|
||||||
match event {
|
map.retain(|hash, _| new_hashes.contains(hash));
|
||||||
AppEvent::FullList(list, _) => {
|
for new_torrent in list { map.insert(new_torrent.hash.clone(), new_torrent); }
|
||||||
log::info!("SSE: Received FullList with {} torrents", list.len());
|
});
|
||||||
torrents_for_sse.update(|map| {
|
}
|
||||||
let new_hashes: std::collections::HashSet<String> = list.iter().map(|t| t.hash.clone()).collect();
|
AppEvent::Update(patch) => {
|
||||||
map.retain(|hash, _| new_hashes.contains(hash));
|
if let Some(hash) = patch.hash.clone() {
|
||||||
for new_torrent in list {
|
torrents_for_sse.update(|map| { if let Some(t) = map.get_mut(&hash) { t.apply(patch); } });
|
||||||
map.insert(new_torrent.hash.clone(), new_torrent);
|
}
|
||||||
}
|
}
|
||||||
});
|
AppEvent::Stats(stats) => { global_stats_for_sse.set(stats); }
|
||||||
log::debug!("SSE: torrents map now has {} entries", torrents_for_sse.with(|m| m.len()));
|
AppEvent::Notification(n) => {
|
||||||
}
|
show_toast(n.level.clone(), n.message.clone());
|
||||||
AppEvent::Update(patch) => {
|
if n.message.contains("tamamlandı") || n.level == shared::NotificationLevel::Error {
|
||||||
let hash_opt = patch.hash.clone();
|
show_browser_notification("VibeTorrent", &n.message);
|
||||||
if let Some(hash) = hash_opt {
|
|
||||||
torrents_for_sse.update(|map| {
|
|
||||||
if let Some(t) = map.get_mut(&hash) {
|
|
||||||
t.apply(patch);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AppEvent::Stats(stats) => { global_stats_for_sse.set(stats); }
|
|
||||||
AppEvent::Notification(n) => {
|
|
||||||
show_toast(n.level.clone(), n.message.clone());
|
|
||||||
if n.message.contains("tamamlandı") || n.level == shared::NotificationLevel::Error {
|
|
||||||
show_browser_notification("VibeTorrent", &n.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => log::error!("SSE: Failed to deserialize MessagePack: {}", e),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => log::error!("SSE: Failed to decode Base64: {}", e),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,13 +145,105 @@ pub fn provide_torrent_store() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log::debug!("SSE: Reconnecting in {}ms...", backoff_ms);
|
|
||||||
gloo_timers::future::TimeoutFuture::new(backoff_ms).await;
|
gloo_timers::future::TimeoutFuture::new(backoff_ms).await;
|
||||||
backoff_ms = std::cmp::min(backoff_ms * 2, 30000);
|
backoff_ms = std::cmp::min(backoff_ms * 2, 30000);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn subscribe_to_push_notifications() {
|
pub async fn is_push_subscribed() -> Result<bool, String> {
|
||||||
// ...
|
let window = web_sys::window().ok_or("no window")?;
|
||||||
|
let navigator = window.navigator();
|
||||||
|
let sw_container = navigator.service_worker();
|
||||||
|
|
||||||
|
let registration = wasm_bindgen_futures::JsFuture::from(sw_container.ready().map_err(|e| format!("{:?}", e))?)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("{:?}", e))?
|
||||||
|
.dyn_into::<web_sys::ServiceWorkerRegistration>()
|
||||||
|
.map_err(|_| "not a registration")?;
|
||||||
|
|
||||||
|
let push_manager = registration.push_manager();
|
||||||
|
let subscription = wasm_bindgen_futures::JsFuture::from(push_manager.get_subscription().map_err(|e| format!("{:?}", e))?)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("{:?}", e))?;
|
||||||
|
|
||||||
|
Ok(!subscription.is_null())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn subscribe_to_push_notifications() {
|
||||||
|
let window = web_sys::window().expect("no window");
|
||||||
|
let navigator = window.navigator();
|
||||||
|
let sw_container = navigator.service_worker();
|
||||||
|
|
||||||
|
let registration = match wasm_bindgen_futures::JsFuture::from(sw_container.ready().expect("sw not ready")).await {
|
||||||
|
Ok(reg) => reg.dyn_into::<web_sys::ServiceWorkerRegistration>().expect("not a reg"),
|
||||||
|
Err(e) => { log::error!("SW Ready Error: {:?}", e); return; }
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. Get Public Key from Backend
|
||||||
|
let public_key = match shared::server_fns::push::get_push_public_key().await {
|
||||||
|
Ok(key) => key,
|
||||||
|
Err(e) => { log::error!("Failed to get public key: {:?}", e); return; }
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. Convert base64 key to Uint8Array
|
||||||
|
let decoded_key = BASE64_URL.decode(public_key.trim()).expect("invalid public key");
|
||||||
|
let key_array = js_sys::Uint8Array::from(&decoded_key[..]);
|
||||||
|
|
||||||
|
// 3. Prepare Options
|
||||||
|
let mut options = web_sys::PushSubscriptionOptionsInit::new();
|
||||||
|
options.user_visible_only(true);
|
||||||
|
options.application_server_key(Some(&key_array.into()));
|
||||||
|
|
||||||
|
// 4. Subscribe
|
||||||
|
let push_manager = registration.push_manager();
|
||||||
|
match wasm_bindgen_futures::JsFuture::from(push_manager.subscribe_with_options(&options).expect("subscribe failed")).await {
|
||||||
|
Ok(subscription) => {
|
||||||
|
let sub = subscription.dyn_into::<web_sys::PushSubscription>().expect("not a sub");
|
||||||
|
let json = sub.to_json().expect("sub to json failed");
|
||||||
|
|
||||||
|
// Extract keys from JSON
|
||||||
|
let sub_obj: serde_json::Value = serde_wasm_bindgen::from_value(json).expect("serde from value failed");
|
||||||
|
|
||||||
|
let endpoint = sub_obj["endpoint"].as_str().expect("no endpoint").to_string();
|
||||||
|
let p256dh = sub_obj["keys"]["p256dh"].as_str().expect("no p256dh").to_string();
|
||||||
|
let auth = sub_obj["keys"]["auth"].as_str().expect("no auth").to_string();
|
||||||
|
|
||||||
|
// 5. Save to Backend
|
||||||
|
match shared::server_fns::push::save_push_subscription(endpoint, p256dh, auth).await {
|
||||||
|
Ok(_) => {
|
||||||
|
log::info!("Push subscription saved successfully");
|
||||||
|
toast_success("Bildirimler aktif edildi");
|
||||||
|
}
|
||||||
|
Err(e) => log::error!("Failed to save subscription: {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => log::error!("Subscription Error: {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn unsubscribe_from_push_notifications() {
|
||||||
|
let window = web_sys::window().expect("no window");
|
||||||
|
let sw_container = window.navigator().service_worker();
|
||||||
|
|
||||||
|
let registration = wasm_bindgen_futures::JsFuture::from(sw_container.ready().expect("sw not ready")).await
|
||||||
|
.unwrap().dyn_into::<web_sys::ServiceWorkerRegistration>().unwrap();
|
||||||
|
|
||||||
|
let push_manager = registration.push_manager();
|
||||||
|
if let Ok(sub_future) = push_manager.get_subscription() {
|
||||||
|
if let Ok(subscription) = wasm_bindgen_futures::JsFuture::from(sub_future).await {
|
||||||
|
if !subscription.is_null() {
|
||||||
|
let sub = subscription.dyn_into::<web_sys::PushSubscription>().unwrap();
|
||||||
|
let endpoint = sub.endpoint();
|
||||||
|
|
||||||
|
// 1. Unsubscribe in Browser
|
||||||
|
let _ = wasm_bindgen_futures::JsFuture::from(sub.unsubscribe().unwrap()).await;
|
||||||
|
|
||||||
|
// 2. Remove from Backend
|
||||||
|
let _ = shared::server_fns::push::remove_push_subscription(endpoint).await;
|
||||||
|
log::info!("Push subscription removed");
|
||||||
|
show_toast(NotificationLevel::Info, "Bildirimler kapatıldı");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user