Skip to content

Auto dismiss notification when main window focused#1488

Merged
yujonglee merged 9 commits intomainfrom
macos-notif-dismiss
Sep 22, 2025
Merged

Auto dismiss notification when main window focused#1488
yujonglee merged 9 commits intomainfrom
macos-notif-dismiss

Conversation

@yujonglee
Copy link
Contributor

No description provided.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 22, 2025

📝 Walkthrough

Walkthrough

Adds macOS notification lifecycle callbacks (confirm/dismiss) with FFI bridges and a dismiss-all API; wires handlers into the Tauri notification plugin and starts/stops analytics tasks; modifies listener Joiner to synthesize zero-filled pairs and warn on queue overflow; stores a silence-stream sender in SourceState; audio test now writes buffered samples to a WAV file; adds tracing/logging.

Changes

Cohort / File(s) Summary
macOS notification FFI + Swift changes
crates/notification-macos/src/lib.rs, crates/notification-macos/swift-lib/src/lib.swift
Add global handler registration, extern "C" callbacks rust_on_notification_confirm / rust_on_notification_dismiss, dismiss_all() Rust API; Swift calls Rust callbacks on user confirm/dismiss, adds dismissWithUserAction(), exposes _dismissAllNotifications() and C interop symbols.
Notification public API and examples
crates/notification/src/lib.rs, crates/notification-macos/examples/test_notification.rs
Add NotificationMutation enum, clear() and setup handler helpers (setup_notification_confirm_handler, setup_notification_dismiss_handler); example registers handlers. Test now serializes buffered samples to out.wav (hound) — non-functional change to audio averaging logic unchanged.
Tauri plugin integration & analytics
plugins/notification/src/ext.rs, plugins/notification/src/lib.rs, apps/desktop/src-tauri/src/ext.rs
Add clear_notifications, start_notification_analytics(user_id) and stop_notification_analytics APIs; plugin State adds analytics_task field; init registers clearing on main window focus; desktop setup calls start_notification_analytics. Analytics task registers notification handlers and emits analytics events.
Listener actor pairing, silence stream, diagnostics
plugins/listener/src/actors/processor.rs, plugins/listener/src/actors/source.rs
Joiner::pop_pair now handles four cases: both empty, one empty (synthesizes zero-filled counterpart), both present; logs mic/spk sums; warns and trims when queues exceed length 10. SourceState gains _silence_stream_tx: Option<std::sync::mpsc::Sender<()>>; added tracing.info for mic_device and use_mixed.
Audio mixed (macOS) test output
crates/audio/src/mixed/macos.rs
Minor comment removals and test change to write buffered audio samples to out.wav using hound; averaging logic unchanged.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant App
  participant NotificationAPI as notification::*
  participant MacOSFFI as notification-macos (Rust)
  participant SwiftBridge as Swift lib
  participant Handlers as Registered Handlers

  rect rgb(240,248,255)
    App->>NotificationAPI: show(...)
    NotificationAPI->>MacOSFFI: show(...)
    MacOSFFI->>SwiftBridge: display notification
  end

  rect rgb(245,255,240)
    SwiftBridge-->>MacOSFFI: rust_on_notification_confirm(id)
    MacOSFFI->>Handlers: invoke confirm handler(id)
  end

  rect rgb(255,248,240)
    SwiftBridge-->>MacOSFFI: rust_on_notification_dismiss(id)
    MacOSFFI->>Handlers: invoke dismiss handler(id)
  end

  note over App,SwiftBridge: Dismiss-all path
  App->>NotificationAPI: clear()
  NotificationAPI->>MacOSFFI: dismiss_all()
  MacOSFFI->>SwiftBridge: _dismissAllNotifications()
Loading
sequenceDiagram
  autonumber
  participant MicQ as Mic Queue
  participant SpkQ as Spk Queue
  participant Joiner
  participant Processor
  participant Downstream as SessionEvent Consumer

  MicQ-->>Joiner: push(mic chunk)
  SpkQ-->>Joiner: push(spk chunk)

  alt both empty
    Joiner-->>Processor: None
  else one empty
    Joiner->>Joiner: synthesize zero-filled counterpart
    Joiner-->>Processor: (present, zeros)
  else both present
    Joiner-->>Processor: (mic, spk)
  end

  Processor->>Processor: log mic_sum, spk_sum
  Processor-->>Downstream: emit SessionEvent(mic, spk)

  opt overflow
    Joiner->>Joiner: warn and trim queue front (>10)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.43% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Description Check ❓ Inconclusive The pull request has no author-provided description, so there is insufficient context in the PR body to confirm intent, scope, and rationale; the lack of a description makes the high-level intent and potential side effects inconclusive. Please add a short PR description summarizing the objective (auto-dismiss behavior), the key modules changed (macOS Swift/FFI and notification crates, plugin analytics, and the RunEvent change), and any runtime or API effects (new handlers, clear()/dismiss_all(), and analytics task) so reviewers can quickly assess scope and impact.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The PR title "Auto dismiss notification when main window focused" is concise and accurately describes the primary behavioral change in the changeset: notifications are dismissed when the main window gains focus (see the added RunEvent::WindowEvent that calls app.clear_notifications()). It is specific, readable, and directly related to the main change.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch macos-notif-dismiss

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (6)
plugins/listener/src/actors/processor.rs (3)

132-135: Use a real amplitude metric (RMS) and lower log level to avoid noisy logs.

Sum trends to ~0 for typical audio and logs at 10 Hz are chatty at info.
[sort: keep existing error handling per guidelines]

Apply this diff:

-            let mic_sum = mic_data.iter().sum::<f32>();
-            let spk_sum = spk_data.iter().sum::<f32>();
-            tracing::info!("mic_sum: {} spk_sum: {}", mic_sum, spk_sum);
+            let mic_rms = (mic_data.iter().map(|x| x * x).sum::<f32>() / mic_data.len() as f32).sqrt();
+            let spk_rms = (spk_data.iter().map(|x| x * x).sum::<f32>() / spk_data.len() as f32).sqrt();
+            tracing::debug!("mic_rms: {:.4} spk_rms: {:.4}", mic_rms, spk_rms);

161-163: Demote overflow log level or it will spam under sustained load.

At capacity, this warns every push; the queue is capped anyway.

Apply this diff:

-            tracing::warn!("mic_queue_overflow");
+            tracing::debug!("mic_queue_overflow");

169-171: Same for speaker queue overflow logging.

Apply this diff:

-            tracing::warn!("spk_queue_overflow");
+            tracing::debug!("spk_queue_overflow");
crates/audio/src/mixed/macos.rs (1)

319-337: Consider removing or conditionalizing test WAV output.

The test now writes audio output to ./out.wav in the current directory. This could cause issues in CI environments or when running tests in read-only directories.

Consider wrapping this in a conditional check or writing to a temporary directory:

-        {
+        if std::env::var("SAVE_TEST_AUDIO").is_ok() {
             let sample_rate = stream.sample_rate();
 
-            let mut writer = hound::WavWriter::create(
-                "./out.wav",
+            let output_path = std::env::temp_dir().join("test_audio_out.wav");
+            let mut writer = hound::WavWriter::create(
+                output_path,
                 hound::WavSpec {
                     channels: 1,
                     sample_rate,
                     bits_per_sample: 32,
                     sample_format: hound::SampleFormat::Float,
                 },
             )
             .unwrap();
 
             for sample in buffer {
                 writer.write_sample(sample).unwrap();
             }
             writer.finalize().unwrap();
         }
crates/notification-macos/src/lib.rs (1)

35-59: Verify null-pointer safety in FFI callbacks (optional).

Swift declares rustOnNotificationConfirm and rustOnNotificationDismiss with UnsafePointer (non-optional) in crates/notification-macos/swift-lib/src/lib.swift, so nulls are unlikely; still add defensive null checks in crates/notification-macos/src/lib.rs before calling CStr::from_ptr:

 pub extern "C" fn rust_on_notification_confirm(id_ptr: *const c_char) {
+    if id_ptr.is_null() { return; }
     if let Some(cb) = CONFIRM_CB.lock().unwrap().as_ref() {
         let id = unsafe { CStr::from_ptr(id_ptr) }
             .to_str()
             .unwrap()
             .to_string();
         cb(id);
     }
 }

 pub extern "C" fn rust_on_notification_dismiss(id_ptr: *const c_char, reason_ptr: *const c_char) {
+    if id_ptr.is_null() || reason_ptr.is_null() { return; }
     if let Some(cb) = DISMISS_CB.lock().unwrap().as_ref() {
         let id = unsafe { CStr::from_ptr(id_ptr) }
             .to_str()
             .unwrap()
             .to_string();
         let reason = unsafe { CStr::from_ptr(reason_ptr) }
             .to_str()
             .unwrap()
             .to_string();
         cb(id, reason);
     }
 }
crates/notification-macos/swift-lib/src/lib.swift (1)

363-363: Consider more specific dismissal reasons for better analytics.

While "programmatic" is functional, consider using more specific reasons:

  • Line 363: Could use "api_dismiss" or "single_dismiss"
  • Line 368: Could use "api_dismiss_all" or "clear_all"
  • Line 390: "limit" is already specific and good

This would provide better insights into how the programmatic dismissals are triggered.

Also applies to: 368-368, 390-390

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4c3528f and 1231568.

📒 Files selected for processing (10)
  • crates/audio/src/mixed/macos.rs (2 hunks)
  • crates/notification-macos/examples/test_notification.rs (1 hunks)
  • crates/notification-macos/src/lib.rs (2 hunks)
  • crates/notification-macos/swift-lib/src/lib.swift (8 hunks)
  • crates/notification/src/lib.rs (1 hunks)
  • plugins/listener/src/actors/processor.rs (2 hunks)
  • plugins/listener/src/actors/source.rs (2 hunks)
  • plugins/notification/src/ext.rs (2 hunks)
  • plugins/notification/src/handler.rs (2 hunks)
  • plugins/notification/src/lib.rs (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{js,ts,tsx,rs}

⚙️ CodeRabbit configuration file

**/*.{js,ts,tsx,rs}: 1. Do not add any error handling. Keep the existing one.
2. No unused imports, variables, or functions.
3. For comments, keep it minimal. It should be about "Why", not "What".

Files:

  • plugins/notification/src/handler.rs
  • crates/notification/src/lib.rs
  • plugins/listener/src/actors/processor.rs
  • plugins/notification/src/lib.rs
  • plugins/notification/src/ext.rs
  • crates/notification-macos/examples/test_notification.rs
  • plugins/listener/src/actors/source.rs
  • crates/audio/src/mixed/macos.rs
  • crates/notification-macos/src/lib.rs
🧬 Code graph analysis (8)
plugins/notification/src/handler.rs (1)
crates/notification/src/lib.rs (2)
  • clear (44-46)
  • clear (52-52)
crates/notification/src/lib.rs (2)
crates/notification-macos/src/lib.rs (2)
  • dismiss_all (76-80)
  • show (61-74)
crates/notification-macos/swift-lib/src/lib.swift (1)
  • show (346-357)
plugins/notification/src/lib.rs (1)
plugins/windows/src/ext.rs (1)
  • from_str (49-68)
plugins/notification/src/ext.rs (1)
crates/notification/src/lib.rs (2)
  • clear (44-46)
  • clear (52-52)
crates/notification-macos/swift-lib/src/lib.swift (1)
crates/notification-macos/examples/test_notification.rs (1)
  • main (34-71)
crates/notification-macos/examples/test_notification.rs (1)
crates/notification-macos/src/lib.rs (2)
  • setup_notification_confirm_handler (28-33)
  • setup_notification_dismiss_handler (21-26)
plugins/listener/src/actors/source.rs (1)
crates/audio/src/lib.rs (2)
  • silence (48-63)
  • get_default_mic_name (82-90)
crates/audio/src/mixed/macos.rs (1)
crates/audio/src/mixed/mod.rs (2)
  • sample_rate (54-63)
  • stream (28-31)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: ci (macos, macos-14)
  • GitHub Check: ci (windows, windows-latest)
🔇 Additional comments (24)
plugins/listener/src/actors/processor.rs (1)

87-95: AGC used in Mixed path looks swapped — confirm intent.

You process Mixed with agc_m but then route it as speaker (last_spk, push_spk). Likely should be agc_s.

Apply this diff if that’s the case:

-                st.agc_m.process(&mut c.data);
+                st.agc_s.process(&mut c.data);
plugins/notification/src/lib.rs (2)

1-1: LGTM: Necessary trait import

Bringing FromStr into scope enables the UFCS call HyprWindow::from_str(...). Mutex is used by SharedState. No unused imports here.


125-126: Confirm unwrap safety across platforms

Assuming clear_notifications() is always available and returns Ok(()) (no‑op on non‑macOS), unwrap() is safe. Please confirm this invariant.

plugins/notification/src/handler.rs (2)

12-12: LGTM! Clear notification trigger added.

The new Clear variant provides a clean way to dismiss all notifications through the existing trigger system.


64-66: LGTM! Clear handling properly integrated.

The handler correctly dispatches to hypr_notification::clear() without any precondition checks, which is appropriate for an explicit clear action.

plugins/notification/src/ext.rs (2)

10-10: LGTM! API extension for clearing notifications.

The new clear_notifications method follows the existing trait pattern and provides consistent error handling.


51-55: LGTM! Simple delegation to platform-specific implementation.

The implementation correctly delegates to hypr_notification::clear() with proper tracing instrumentation.

crates/audio/src/mixed/macos.rs (1)

108-108: LGTM! Comment removal improves code clarity.

Removing the redundant comment makes the aggregate device configuration more concise.

plugins/listener/src/actors/source.rs (3)

76-76: Good addition of silence stream management.

Moving the silence stream creation into pre_start and storing the sender in state ensures proper lifecycle management.


80-80: LGTM! Helpful debug instrumentation added.

The tracing statements for mic_device and use_mixed will aid in debugging audio routing issues.

Also applies to: 180-180


155-155: Proper cleanup in post_stop.

Good practice to explicitly drop the silence stream sender during shutdown.

crates/notification/src/lib.rs (2)

43-46: LGTM! Platform-specific clear implementation.

The macOS implementation correctly delegates to hypr_notification_macos::dismiss_all().


51-52: LGTM! No-op for non-macOS platforms.

The empty implementation is appropriate for platforms that don't support notification management.

crates/notification-macos/examples/test_notification.rs (1)

50-55: LGTM! Test handlers demonstrate lifecycle callbacks.

The example properly demonstrates how to use the new notification lifecycle handlers for both confirmation and dismissal events.

crates/notification-macos/src/lib.rs (4)

1-3: LGTM! Necessary imports for FFI.

The added imports for C string handling are required for the new FFI callback functions.


18-19: Thread-safe storage for callbacks well implemented.

Using Mutex ensures thread-safe access to the callback handlers.


21-33: Clean API for lifecycle handlers.

The setup functions provide a clear interface for registering notification event handlers with appropriate trait bounds.


76-80: LGTM! Clean wrapper for dismissing all notifications.

The function properly wraps the Swift FFI call with appropriate unsafe block.

crates/notification-macos/swift-lib/src/lib.swift (6)

21-21: LGTM! Consistent dismissal reason for timeout events.

The timeout dismissal now properly identifies itself with the "timeout" reason, which enables better tracking of how notifications are being dismissed.


27-36: LGTM! Well-implemented dismissal callback mechanism.

The implementation properly invokes the Rust callback with both notification ID and dismissal reason before animating the notification dismissal. The use of withCString ensures safe C string interop.


137-145: Confirm callback should precede URL opening for proper event ordering.

The current implementation calls rustOnNotificationConfirm before opening the URL, which is correct. This ensures that the confirm event is registered even if the URL opening fails or takes time.


218-218: LGTM! Appropriate dismissal reason for close button.

The "close" reason clearly identifies user-initiated dismissals via the close button.


648-654: LGTM! Consistent handling for action button clicks.

The action button properly triggers the confirm callback before opening the URL and dismissing with "click" reason, maintaining consistency with the clickable view behavior.


826-830: LGTM! Clean implementation of dismiss-all functionality.

The _dismissAllNotifications function provides a simple and effective way to clear all active notifications, properly exposing it through the FFI boundary.

@yujonglee yujonglee merged commit 1f89489 into main Sep 22, 2025
6 of 7 checks passed
@yujonglee yujonglee deleted the macos-notif-dismiss branch September 22, 2025 07:23
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 902d3ae and bf048c9.

📒 Files selected for processing (7)
  • apps/desktop/src-tauri/src/ext.rs (1 hunks)
  • crates/notification-macos/examples/test_notification.rs (1 hunks)
  • crates/notification-macos/src/lib.rs (2 hunks)
  • crates/notification-macos/swift-lib/src/lib.swift (12 hunks)
  • crates/notification/src/lib.rs (2 hunks)
  • plugins/notification/src/ext.rs (4 hunks)
  • plugins/notification/src/lib.rs (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • plugins/notification/src/lib.rs
  • crates/notification/src/lib.rs
  • crates/notification-macos/examples/test_notification.rs
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{js,ts,tsx,rs}

⚙️ CodeRabbit configuration file

**/*.{js,ts,tsx,rs}: 1. Do not add any error handling. Keep the existing one.
2. No unused imports, variables, or functions.
3. For comments, keep it minimal. It should be about "Why", not "What".

Files:

  • plugins/notification/src/ext.rs
  • apps/desktop/src-tauri/src/ext.rs
  • crates/notification-macos/src/lib.rs
🧬 Code graph analysis (2)
plugins/notification/src/ext.rs (2)
crates/notification/src/lib.rs (3)
  • clear (51-54)
  • setup_notification_confirm_handler (64-70)
  • setup_notification_dismiss_handler (56-62)
crates/notification-macos/src/lib.rs (2)
  • setup_notification_confirm_handler (28-33)
  • setup_notification_dismiss_handler (21-26)
crates/notification-macos/src/lib.rs (1)
crates/notification/src/lib.rs (2)
  • setup_notification_dismiss_handler (56-62)
  • setup_notification_confirm_handler (64-70)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: ci (macos, macos-14)
  • GitHub Check: ci (windows, windows-latest)
🔇 Additional comments (11)
plugins/notification/src/ext.rs (3)

10-10: API addition looks good.

Exposing clear_notifications on the trait makes the macOS dismiss-all reachable from app code.


31-32: Analytics start/stop API surface is reasonable.

Naming is consistent with existing start_/stop_ pattern.


54-58: clear_notifications implementation is fine.

Directly delegates to hypr_notification::clear and returns Ok(()).

apps/desktop/src-tauri/src/ext.rs (1)

117-120: Verify plugin init order and OS behavior for analytics start.

  • Ensure tauri_plugin_notification is initialized before this call; otherwise state::() inside the plugin may panic during startup.
  • Call is unconditional (also on non‑macOS). That’s fine given the macOS‑gated internals, but please confirm this runs after plugin setup in all modes.
crates/notification-macos/src/lib.rs (2)

72-76: dismiss_all wrapper LGTM.

Matches the Swift @_cdecl and provides a simple Rust API surface.


18-55: Don’t invoke callbacks while holding the Mutex; avoid UTF‑8 unwrap panics.

Holding CONFIRM_CB/DISMISS_CB locks during cb(id) risks deadlocks and blocks Swift’s main thread; to_str().unwrap() can also panic on invalid UTF‑8. Clone an Arc out of the lock, drop the lock, then invoke; use to_string_lossy.

-use std::sync::Mutex;
+use std::sync::{Arc, Mutex};
@@
-static CONFIRM_CB: Mutex<Option<Box<dyn Fn(String) + Send + Sync>>> = Mutex::new(None);
-static DISMISS_CB: Mutex<Option<Box<dyn Fn(String) + Send + Sync>>> = Mutex::new(None);
+static CONFIRM_CB: Mutex<Option<Arc<dyn Fn(String) + Send + Sync>>> = Mutex::new(None);
+static DISMISS_CB: Mutex<Option<Arc<dyn Fn(String) + Send + Sync>>> = Mutex::new(None);
@@
 pub fn setup_notification_dismiss_handler<F>(f: F)
 where
     F: Fn(String) + Send + Sync + 'static,
 {
-    *DISMISS_CB.lock().unwrap() = Some(Box::new(f));
+    *DISMISS_CB.lock().unwrap() = Some(Arc::new(f));
 }
@@
 pub fn setup_notification_confirm_handler<F>(f: F)
 where
     F: Fn(String) + Send + Sync + 'static,
 {
-    *CONFIRM_CB.lock().unwrap() = Some(Box::new(f));
+    *CONFIRM_CB.lock().unwrap() = Some(Arc::new(f));
 }
@@
 #[no_mangle]
 pub extern "C" fn rust_on_notification_confirm(id_ptr: *const c_char) {
-    if let Some(cb) = CONFIRM_CB.lock().unwrap().as_ref() {
-        let id = unsafe { CStr::from_ptr(id_ptr) }
-            .to_str()
-            .unwrap()
-            .to_string();
-        cb(id);
-    }
+    let cb = CONFIRM_CB.lock().unwrap().as_ref().cloned();
+    if let Some(cb) = cb {
+        let id = unsafe { CStr::from_ptr(id_ptr) }.to_string_lossy().into_owned();
+        cb(id);
+    }
 }
@@
 #[no_mangle]
 pub extern "C" fn rust_on_notification_dismiss(id_ptr: *const c_char) {
-    if let Some(cb) = DISMISS_CB.lock().unwrap().as_ref() {
-        let id = unsafe { CStr::from_ptr(id_ptr) }
-            .to_str()
-            .unwrap()
-            .to_string();
-        cb(id);
-    }
+    let cb = DISMISS_CB.lock().unwrap().as_ref().cloned();
+    if let Some(cb) = cb {
+        let id = unsafe { CStr::from_ptr(id_ptr) }.to_string_lossy().into_owned();
+        cb(id);
+    }
 }
crates/notification-macos/swift-lib/src/lib.swift (5)

41-46: Good: user‑initiated dismiss emits a Rust callback before closing.

This cleanly separates user vs programmatic dismiss paths.


139-146: Confirm and then dismiss on user actions—LGTM.

ClickableView and ActionButton both notify Rust (confirm) prior to dismissal; CloseButton routes through dismissWithUserAction.

Also applies to: 647-656, 219-219


159-161: UI retune cluster looks consistent.

Sizing/typography reductions and spacing adjustments are coherent across components.

Also applies to: 294-294, 571-571, 576-579, 585-587, 591-591, 599-599, 610-610


728-730: Window fronting: verify focus behavior.

Using orderFrontRegardless + makeKeyAndOrderFront on a non‑activating panel can still influence focus in edge cases. Please verify it doesn’t steal focus and that auto‑dismiss‑on‑main‑window‑focus still triggers as intended. If needed, prefer only orderFrontRegardless().


828-832: FFI dismissAll bridge LGTM.

Matches Rust swift!(fn _dismiss_all_notifications) signature.

Comment on lines +218 to +266
fn start_notification_analytics(&self, user_id: String) -> Result<(), Error> {
let state = self.state::<crate::SharedState>();
let mut guard = state.lock().unwrap();

let analytics_task = {
use hypr_notification::NotificationMutation;
use tauri_plugin_analytics::{AnalyticsPayload, AnalyticsPluginExt};

let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<NotificationMutation>();
let app_handle = self.app_handle().clone();

let confirm_tx = tx.clone();
hypr_notification::setup_notification_confirm_handler(move |_id| {
confirm_tx.send(NotificationMutation::Confirm).unwrap();
});

let dismiss_tx = tx.clone();
hypr_notification::setup_notification_dismiss_handler(move |_id| {
dismiss_tx.send(NotificationMutation::Dismiss).unwrap();
});

tokio::spawn(async move {
while let Some(event) = rx.recv().await {
match event {
NotificationMutation::Confirm => {
let _ = app_handle
.event(
AnalyticsPayload::for_user(user_id.clone())
.event("notification_confirm")
.build(),
)
.await;
}
NotificationMutation::Dismiss => {
let _ = app_handle
.event(
AnalyticsPayload::for_user(user_id.clone())
.event("notification_dismiss")
.build(),
)
.await;
}
}
}
})
};

guard.analytics_task = Some(analytics_task);
Ok(())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Avoid panics on channel send; abort any prior task; don’t hold the state lock while registering callbacks/spawning.

  • tx.send(...).unwrap() will panic once stop_notification_analytics drops rx.
  • start_notification_analytics overwrites guard.analytics_task without aborting an existing task → orphaned task and leaked receiver.
  • Registering callbacks and spawning while holding the SharedState lock is unnecessary and can prolong the critical section.

Apply this diff:

 fn start_notification_analytics(&self, user_id: String) -> Result<(), Error> {
-        let state = self.state::<crate::SharedState>();
-        let mut guard = state.lock().unwrap();
+        let state = self.state::<crate::SharedState>();

         let analytics_task = {
             use hypr_notification::NotificationMutation;
             use tauri_plugin_analytics::{AnalyticsPayload, AnalyticsPluginExt};

             let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<NotificationMutation>();
             let app_handle = self.app_handle().clone();

             let confirm_tx = tx.clone();
             hypr_notification::setup_notification_confirm_handler(move |_id| {
-                confirm_tx.send(NotificationMutation::Confirm).unwrap();
+                let _ = confirm_tx.send(NotificationMutation::Confirm);
             });

             let dismiss_tx = tx.clone();
             hypr_notification::setup_notification_dismiss_handler(move |_id| {
-                dismiss_tx.send(NotificationMutation::Dismiss).unwrap();
+                let _ = dismiss_tx.send(NotificationMutation::Dismiss);
             });

             tokio::spawn(async move {
                 while let Some(event) = rx.recv().await {
                     match event {
                         NotificationMutation::Confirm => {
                             let _ = app_handle
                                 .event(
-                                    AnalyticsPayload::for_user(user_id.clone())
+                                    AnalyticsPayload::for_user(&user_id)
                                         .event("notification_confirm")
                                         .build(),
                                 )
                                 .await;
                         }
                         NotificationMutation::Dismiss => {
                             let _ = app_handle
                                 .event(
-                                    AnalyticsPayload::for_user(user_id.clone())
+                                    AnalyticsPayload::for_user(&user_id)
                                         .event("notification_dismiss")
                                         .build(),
                                 )
                                 .await;
                         }
                     }
                 }
             })
         };

-        guard.analytics_task = Some(analytics_task);
+        let mut guard = state.lock().unwrap();
+        if let Some(h) = guard.analytics_task.take() {
+            h.abort();
+        }
+        guard.analytics_task = Some(analytics_task);
         Ok(())
     }

Please confirm AnalyticsPayload::for_user accepts &str (borrow) so we can avoid cloning per above.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Searching repository for AnalyticsPayload / AnalyticsPluginExt / tauri_plugin_analytics / for_user..."
rg -n --hidden --no-ignore 'AnalyticsPayload|AnalyticsPluginExt|tauri_plugin_analytics|for_user\(' -S || true

echo
echo "Show files that mention AnalyticsPayload (if any) with context..."
rg -n --hidden --no-ignore 'AnalyticsPayload' -S -C3 || true

echo
echo "Show Cargo.toml and workspace Cargo.toml (first 200 lines) for dependency lookups..."
if [ -f Cargo.toml ]; then
  sed -n '1,200p' Cargo.toml
else
  echo "No top-level Cargo.toml found"
fi

for f in $(rg -n --hidden --no-ignore 'tauri_plugin_analytics' -S -l || true); do
  echo
  echo "---- Showing $f (first 200 lines) ----"
  sed -n '1,200p' "$f"
done

Length of output: 46916


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Find SharedState definition and analytics_task field..."
rg -n --hidden --no-ignore "struct SharedState|enum SharedState|type SharedState" -S || true

echo
echo "Show files mentioning 'analytics_task' or '.analytics_task' with context..."
rg -n --hidden --no-ignore '\.analytics_task\b|analytics_task\b' -S -C3 || true

echo
echo "Show definition of crates/analytics AnalyticsPayload::for_user..."
sed -n '1,240p' crates/analytics/src/lib.rs || true

echo
echo "Show the start_notification_analytics function region (plugins/notification/src/ext.rs) for full context..."
sed -n '1,400p' plugins/notification/src/ext.rs || true

Length of output: 17746


Avoid panics on channel send; abort previous analytics task; don't hold SharedState lock while registering/spawning

  • Replace confirm_tx.send(...).unwrap() / dismiss_tx.send(...).unwrap() with let _ = confirm_tx.send(...) / let _ = dismiss_tx.send(...); send can fail when rx is closed.
  • Create the channel, register hypr_notification handlers, and spawn the tokio task outside the SharedState lock; only lock to abort any existing task and set guard.analytics_task.
  • Before assigning guard.analytics_task, abort any existing handle: if let Some(h) = guard.analytics_task.take() { h.abort(); }.
  • AnalyticsPayload::for_user has signature pub fn for_user(user_id: impl Into) — passing &user_id is accepted (apps/desktop uses for_user(&user_id)); use AnalyticsPayload::for_user(&user_id) inside the task to avoid per-event cloning.
  • Make stop_notification_analytics robust: replace guard.analytics_task.take().unwrap().abort() with the conditional abort above to avoid panics.
🤖 Prompt for AI Agents
In plugins/notification/src/ext.rs around lines 218 to 266, avoid panics and
holding the SharedState lock while registering/spawning: create the unbounded
channel, register hypr_notification confirm/dismiss handlers and spawn the tokio
task outside the SharedState lock; inside the handlers replace
confirm_tx.send(...).unwrap() / dismiss_tx.send(...).unwrap() with let _ =
confirm_tx.send(...) / let _ = dismiss_tx.send(...) so failed sends don't panic;
when setting guard.analytics_task, first abort any existing task with if let
Some(h) = guard.analytics_task.take() { h.abort(); } and then assign the new
handle; inside the spawned loop use AnalyticsPayload::for_user(&user_id)
(borrow) to avoid cloning per-event; also update stop_notification_analytics to
conditionally take() and abort the task instead of unwrapping to prevent panics.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant