//! Keyword alert settings, native side, for every app that sets [`crate::app::AppConfig::alerts`]: //! the shell-only `alerts_get` / `alerts_set` ([`crate::commands::ALERTS`]) over the owner's //! [`AlertSettings`] (saved in app data, validated by [`crate::alerts`]). Site tabs gain no command //! here: native code hands the page runtime the keywords to watch ([`script`]) at every page load //! and whenever they change. What a match looks like and the notification are the app's. use crate::alerts::AlertSettings; use crate::app::config; use crate::host::{eval_in_site_tabs, read_json, shell_only, write_json}; use crate::tabs::deliver_script; use std::sync::{Mutex, MutexGuard}; use tauri::{AppHandle, Manager, Webview}; pub use crate::alerts::AlertsConfig; struct AlertState(Mutex); /// Loads the saved settings. Does nothing unless the app set [`crate::app::AppConfig::alerts`]. pub(crate) fn init(app: &AppHandle) { let Some(alerts) = config(app).alerts else { return }; let settings = read_json::(app, alerts.file).and_then(|settings| settings.validated().ok()).unwrap_or_default(); app.manage(AlertState(Mutex::new(settings))); } fn state(app: &AppHandle) -> Result, String> { app.try_state::() .ok_or("DATABASE: alert state unavailable; restart safely")? .inner() .0 .lock() .map_err(|_| "DATABASE: alert state unavailable; restart safely".into()) } /// The owner's alert settings. pub fn settings(app: &AppHandle) -> Result { Ok(state(app)?.clone()) } /// The script that hands the page runtime the keywords to watch (empty = alerts off). pub fn script(app: &AppHandle) -> String { let (Some(alerts), Ok(settings)) = (config(app).alerts, settings(app)) else { return String::new() }; script_for(app, alerts, &settings) } fn script_for(app: &AppHandle, alerts: &AlertsConfig, settings: &AlertSettings) -> String { let keywords = serde_json::to_string(settings.active_keywords()).unwrap_or_else(|_| "[]".into()); deliver_script(config(app).site.runtime_global, alerts.member, "set", alerts.queue, &keywords) } /// Validates, saves and keeps the settings, and does not touch the tabs: for the app's own /// writers (a backup import), which then evaluate [`script`] themselves. pub fn store(app: &AppHandle, settings: AlertSettings) -> Result { let alerts = config(app).alerts.ok_or_else(|| "UNAVAILABLE: this app has no alerts".to_owned())?; let settings = settings.validated()?; // The lock is held across the write so concurrent saves cannot leave disk and memory disagreeing. let mut current = state(app)?; write_json(app, alerts.file, &settings)?; *current = settings.clone(); Ok(settings) } #[tauri::command] pub(crate) async fn alerts_get(webview: Webview, app: AppHandle) -> Result { shell_only(&webview)?; settings(&app) } #[tauri::command] pub(crate) async fn alerts_set(webview: Webview, app: AppHandle, settings: AlertSettings) -> Result { shell_only(&webview)?; let settings = store(&app, settings)?; eval_in_site_tabs(&app, config(&app).site, &script(&app)); Ok(settings) } pub(crate) fn handler() -> impl Fn(tauri::ipc::Invoke) -> bool + Send + Sync + 'static { tauri::generate_handler![alerts_get, alerts_set] }