robin/src-tauri/src/notify.rs
pyr0ball f0269c62a5 fix(build): add reqwest, fix exhaustive match, tauri deprecation, Cargo.lock
- Add reqwest 0.12 (json + stream features) — was missing from Cargo.toml
  causing chat_stream to fail to compile
- Add Android/IpadOs arms to SourceOs match in chat command
- Add tauri::Manager import to notify.rs (needed for .state())
- Replace deprecated menu_on_left_click with show_menu_on_left_click
- Remove desktopTemplate from tauri.conf.json (not in tauri-utils 2.9.2 schema)
- Commit Cargo.lock so cross-machine builds pin identical crate versions

All 42 unit tests pass on Linux Mint 22.3 (Muninn).
2026-05-20 08:32:39 -07:00

95 lines
2.6 KiB
Rust

use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex;
use tauri::{AppHandle, Emitter, Manager, Runtime};
use tauri_plugin_notification::NotificationExt;
use crate::commands::AppState;
use crate::config::NotificationLevel;
use crate::patterns::MatchedEvent;
static PENDING: Mutex<Vec<MatchedEvent>> = Mutex::new(vec![]);
// True while ChatPanel is mounted and listening for live events.
// When true, dispatch skips PENDING so events are not shown twice on re-open.
static PANEL_OPEN: AtomicBool = AtomicBool::new(false);
pub fn set_panel_open(open: bool) {
PANEL_OPEN.store(open, Ordering::Relaxed);
}
pub fn dispatch<R: Runtime>(app: &AppHandle<R>, event: MatchedEvent) {
let level = app
.state::<AppState>()
.config
.lock()
.map(|c| c.display.notification_level.clone())
.unwrap_or_default();
if !PANEL_OPEN.load(Ordering::Relaxed) {
match PENDING.lock() {
Ok(mut pending) => pending.push(event.clone()),
Err(e) => log::warn!("notify: pending queue lock poisoned — event dropped: {e}"),
}
}
match level {
NotificationLevel::Off => {}
NotificationLevel::BadgeOnly => {
crate::tray::badge_on(app);
}
NotificationLevel::BadgeAndToast => {
crate::tray::badge_on(app);
send_toast(app, &event);
}
}
let _ = app.emit("robin:event", &event);
}
fn send_toast<R: Runtime>(app: &AppHandle<R>, event: &MatchedEvent) {
let _ = app
.notification()
.builder()
.title(&event.title)
.body(&event.body)
.show();
}
pub fn take_pending() -> Vec<MatchedEvent> {
PENDING
.lock()
.map(|mut v| std::mem::take(&mut *v))
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
fn make_event(id: &str) -> MatchedEvent {
MatchedEvent {
pattern_id: id.into(),
title: "Title".into(),
body: "Body".into(),
severity: "warn".into(),
timestamp: 0,
}
}
#[test]
fn take_pending_drains_queue() {
// Ensure clean state — drain any events from other tests sharing the static
let _ = take_pending();
if let Ok(mut q) = PENDING.lock() {
q.push(make_event("a"));
q.push(make_event("b"));
}
let first = take_pending();
assert_eq!(first.len(), 2);
assert_eq!(first[0].pattern_id, "a");
let second = take_pending();
assert!(second.is_empty(), "queue must be empty after drain");
}
}