Skip to content

Conversation

@HavenDV
Copy link
Owner

@HavenDV HavenDV commented Dec 26, 2025

Summary

Fixed issue where ResetBalloonCloseTimer() didn't work with native Windows notifications shown via ShowNotification().

Changes

  • Added timer to periodically refresh native balloon notifications
  • Track last notification parameters for refresh
  • Update ResetBalloonCloseTimer() to handle both custom and native balloons
  • Refresh native notifications every 25 seconds to keep them visible indefinitely
  • Stop refresh timer when balloon is closed or notifications are cleared

Fixes #220

Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Enhanced native notification visibility on non-Mac platforms with automatic periodic refresh (every 25 seconds) to prevent notifications from disappearing prematurely.
    • Improved notification state tracking and lifecycle management for better application stability.
    • Refined resource cleanup with proper disposal of notification timers.

✏️ Tip: You can customize this high-level summary in your review settings.

- Add timer to periodically refresh native balloon notifications
- Track last notification parameters for refresh
- Update ResetBalloonCloseTimer to handle both custom and native balloons
- Refresh native notifications every 25 seconds to keep them visible indefinitely
- Stop refresh timer when balloon is closed or notifications are cleared

Fixes #220

Co-authored-by: Konstantin S. <[email protected]>
@coderabbitai
Copy link

coderabbitai bot commented Dec 26, 2025

📝 Walkthrough

Walkthrough

The changes introduce a platform-specific refresh timer mechanism for native notifications on non-macOS systems. When ShowNotification is called, notification parameters are captured and a refresh timer is started to periodically re-display the notification every 25 seconds, keeping it visible until explicitly cleared or disposed.

Changes

Cohort / File(s) Summary
Notification Refresh Logic
src/libs/H.NotifyIcon.Shared/TaskbarIcon.Notifications.cs
Introduced private state to track native notifications: nativeBalloonRefreshTimer (Timer), lastNotification (tuple with notification parameters). Enhanced ShowNotification to capture parameters; added RefreshNativeBalloon() private method to re-display notifications; enhanced ClearNotifications to stop timer and clear state.
Refresh Timer Initialization
src/libs/H.NotifyIcon.Shared/TaskbarIcon.cs
Added non-macOS conditional initialization of nativeBalloonRefreshTimer in constructor with callback to RefreshNativeBalloon().
Resource Cleanup
src/libs/H.NotifyIcon.Shared/TaskbarIcon.Dispose.cs
Added platform-specific disposal of nativeBalloonRefreshTimer alongside existing timer cleanup, gated by #if !MACOS.
Balloon Close Timer Behavior
src/libs/H.NotifyIcon.Shared/TaskbarIcon.CustomNotifications.Wpf.cs
Updated ResetBalloonCloseTimer to conditionally start refresh timer when native notifications are active; enhanced CloseBalloon to stop refresh timer and clear lastNotification, both gated by #if !MACOS. Added documentation distinguishing custom balloon vs. native notification behaviors.

Sequence Diagram

sequenceDiagram
    participant User
    participant TaskbarIcon
    participant Timer as nativeBalloonRefreshTimer
    participant TrayIcon
    participant OS as OS Notification

    User->>TaskbarIcon: ShowNotification(params)
    TaskbarIcon->>TaskbarIcon: Store params in lastNotification
    TaskbarIcon->>TrayIcon: ShowNotification(params)
    TrayIcon->>OS: Display notification
    TaskbarIcon->>Timer: Start timer (25s interval)
    
    loop Every 25 seconds
        Timer->>TaskbarIcon: RefreshNativeBalloon()
        TaskbarIcon->>TrayIcon: ShowNotification(lastNotification)
        TrayIcon->>OS: Re-display notification
    end
    
    User->>TaskbarIcon: ClearNotifications() or Dispose()
    TaskbarIcon->>Timer: Stop timer
    TaskbarIcon->>TaskbarIcon: Clear lastNotification
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A timer hops through Windows bright,
Keeping notifications in sight,
Twenty-five seconds, refresh they go,
Until you bid them farewell, you know!
No mac shall see this hopping dance—
Platform-specific circumstance!

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately describes the main change: fixing ResetBalloonCloseTimer to work with native notifications, which aligns with the core objective.
Linked Issues check ✅ Passed The PR implementation fully addresses issue #220: it enables ResetBalloonCloseTimer to keep native notifications visible indefinitely by introducing a refresh timer that re-shows notifications every 25 seconds.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the native notification refresh mechanism. No unrelated modifications to other features or platform-specific code outside the required scope are present.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch claude/issue-220-20251223-1122

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.

@github-actions github-actions bot enabled auto-merge December 26, 2025 13:55
@claude
Copy link

claude bot commented Dec 26, 2025

Code Review

Thank you for this contribution! The implementation addresses issue #220 by enabling native notifications to persist indefinitely when ResetBalloonCloseTimer() is called. Here's my detailed review:

Strengths

  1. Clear problem-solution mapping: The approach of periodically refreshing native notifications to keep them alive is a practical workaround for Windows' 30-second timeout limitation.

  2. Consistent platform handling: Proper use of #if \!MACOS conditional compilation follows the codebase's multi-platform architecture.

  3. Resource cleanup: Timer disposal is correctly handled in both Dispose() and CloseBalloon() methods.

  4. Updated documentation: The XML documentation on ResetBalloonCloseTimer() clearly explains the different behavior for custom vs. native balloons.

🔍 Potential Issues

1. Thread Safety Concerns (Medium Priority)

The code accesses lastNotification from multiple threads without synchronization:

  • ShowNotification() sets it (UI thread)
  • RefreshNativeBalloon() reads it (timer thread)
  • CloseBalloon() and ClearNotifications() clear it (potentially different threads)

Recommendation: Consider using a lock or making lastNotification volatile. The existing code has commented-out lock(lockObject) statements suggesting this was previously considered.

private readonly object notificationLock = new object();

// In ShowNotification:
lock (notificationLock)
{
    lastNotification = (...);
}

// In RefreshNativeBalloon:
lock (notificationLock)
{
    if (IsDisposed || lastNotification == null) { ... }
    var params = lastNotification.Value;
}

2. Silent Exception Swallowing (Low-Medium Priority)

RefreshNativeBalloon() catches all exceptions without logging:

catch
{
    // If refresh fails, stop the timer
    _ = nativeBalloonRefreshTimer?.Change(Timeout.Infinite, Timeout.Infinite);
}

Recommendation: At minimum, consider logging the exception or being more specific about which exceptions to catch. This would aid debugging if the refresh mechanism fails silently.

3. User Experience Consideration (Low Priority)

Refreshing every 25 seconds means the notification will briefly disappear and reappear, which could be jarring. The documentation mentions this is deprecated behavior on Windows Vista+, where "Notification display times are now based on system accessibility settings."

Question: Have you tested this on Windows 11 with accessibility features enabled? The notification might behave differently than expected.

4. Potential Notification Sound Spam (Medium Priority)

The sound parameter is preserved in lastNotification and passed to each refresh. This means if sound: true, the notification sound might play every 25 seconds.

Recommendation: Consider forcing sound: false for refresh calls:

TrayIcon.ShowNotification(
    // ... other params
    sound: false,  // Don't replay sound on refresh
    // ... remaining params
);

5. Missing Timer Null-Check in Constructor (Low Priority)

In TaskbarIcon.cs:93, the timer is initialized but only conditionally (under #if \!MACOS). However, it's used unconditionally in ResetBalloonCloseTimer() with the null-conditional operator ?. This is fine, but for consistency with balloonCloseTimer (which is non-nullable and WPF-only), consider documenting why one is nullable and one isn't.

🧪 Test Coverage

The PR lacks tests for the new functionality. Consider adding:

  1. A test verifying ResetBalloonCloseTimer() starts the refresh timer for native notifications
  2. A test verifying the timer stops when CloseBalloon() is called
  3. A test verifying the timer stops when ClearNotifications() is called
  4. A test verifying notifications are refreshed at ~25-second intervals

Given the Windows-specific nature and timing dependencies, these could be manual tests or integration tests similar to TrayIconTests.SimpleTest().

📝 Minor Suggestions

  1. Typo in comment: TaskbarIcon.Notifications.cs:103 - "deffered" should be "deferred"

  2. Magic number: The 25000ms interval could be extracted to a named constant:

    private const int NativeBalloonRefreshIntervalMs = 25000; // Refresh before 30s Windows timeout
  3. Code consistency: Consider whether ShowNotification() should also stop any existing refresh timer before tracking new notification parameters, similar to how ShowCustomBalloon() calls CloseBalloon() first.

🎯 Recommendation

This is a functional solution that addresses the reported issue. The main concerns are:

  • Thread safety around lastNotification
  • Potential notification sound spam every 25 seconds
  • Missing test coverage

I'd recommend addressing the sound issue (high user impact) and thread safety (correctness) before merging. The other issues are nice-to-haves.

Overall: Approve with requested changes 👍

Copy link

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/libs/H.NotifyIcon.Shared/TaskbarIcon.Notifications.cs (1)

103-103: Minor typo in documentation.

"deffered" should be "deferred".

🔎 Fix
-    /// Clears all notifications(active and deffered) by recreating tray icon.
+    /// Clears all notifications(active and deferred) by recreating tray icon.
🧹 Nitpick comments (1)
src/libs/H.NotifyIcon.Shared/TaskbarIcon.Notifications.cs (1)

144-144: Consider timeout behavior on refresh.

The original timeout parameter is passed when refreshing the notification. Since the goal is to keep the notification visible indefinitely through periodic refreshes, you might want to consider whether passing the original timeout is the desired behavior, or if a different timeout value (or null) would be more appropriate for refresh operations.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bcaaabe and d2191a0.

📒 Files selected for processing (4)
  • src/libs/H.NotifyIcon.Shared/TaskbarIcon.CustomNotifications.Wpf.cs
  • src/libs/H.NotifyIcon.Shared/TaskbarIcon.Dispose.cs
  • src/libs/H.NotifyIcon.Shared/TaskbarIcon.Notifications.cs
  • src/libs/H.NotifyIcon.Shared/TaskbarIcon.cs
🧰 Additional context used
🧠 Learnings (7)
📚 Learning: 2025-10-23T11:04:44.417Z
Learnt from: CR
Repo: HavenDV/H.NotifyIcon PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-23T11:04:44.417Z
Learning: Applies to src/libs/H.NotifyIcon/**/TaskbarIcon.*.cs : Keep TaskbarIcon split by concern (Properties, ContextMenu, MouseEvents, KeyboardEvents, Notifications, IconSource) rather than monolithic files

Applied to files:

  • src/libs/H.NotifyIcon.Shared/TaskbarIcon.cs
  • src/libs/H.NotifyIcon.Shared/TaskbarIcon.Notifications.cs
  • src/libs/H.NotifyIcon.Shared/TaskbarIcon.Dispose.cs
  • src/libs/H.NotifyIcon.Shared/TaskbarIcon.CustomNotifications.Wpf.cs
📚 Learning: 2025-10-23T11:04:44.417Z
Learnt from: CR
Repo: HavenDV/H.NotifyIcon PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-23T11:04:44.417Z
Learning: Applies to src/libs/H.NotifyIcon.Shared/**/*.cs : Place cross-platform shared code under src/libs/H.NotifyIcon.Shared/, organized by feature files (e.g., TaskbarIcon.cs, TaskbarIcon.Properties.cs, TaskbarIcon.ContextMenu.cs)

Applied to files:

  • src/libs/H.NotifyIcon.Shared/TaskbarIcon.cs
  • src/libs/H.NotifyIcon.Shared/TaskbarIcon.Notifications.cs
  • src/libs/H.NotifyIcon.Shared/TaskbarIcon.Dispose.cs
  • src/libs/H.NotifyIcon.Shared/TaskbarIcon.CustomNotifications.Wpf.cs
📚 Learning: 2025-10-23T11:04:44.417Z
Learnt from: CR
Repo: HavenDV/H.NotifyIcon PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-23T11:04:44.417Z
Learning: Applies to src/libs/H.NotifyIcon/** : Use CsWin32-generated P/Invoke for Windows interop (Shell_NotifyIcon and NOTIFYICONDATA) instead of handwritten signatures

Applied to files:

  • src/libs/H.NotifyIcon.Shared/TaskbarIcon.cs
  • src/libs/H.NotifyIcon.Shared/TaskbarIcon.Notifications.cs
  • src/libs/H.NotifyIcon.Shared/TaskbarIcon.Dispose.cs
  • src/libs/H.NotifyIcon.Shared/TaskbarIcon.CustomNotifications.Wpf.cs
📚 Learning: 2025-10-23T11:04:44.417Z
Learnt from: CR
Repo: HavenDV/H.NotifyIcon PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-23T11:04:44.417Z
Learning: Applies to src/libs/H.NotifyIcon.Shared/**/*.{Wpf,WinUI}.cs : For WinUI/Uno context menus, keep implementations under TaskbarIcon.ContextMenu.* partials and adhere to ContextMenuMode behavior contracts

Applied to files:

  • src/libs/H.NotifyIcon.Shared/TaskbarIcon.cs
  • src/libs/H.NotifyIcon.Shared/TaskbarIcon.Notifications.cs
  • src/libs/H.NotifyIcon.Shared/TaskbarIcon.Dispose.cs
  • src/libs/H.NotifyIcon.Shared/TaskbarIcon.CustomNotifications.Wpf.cs
📚 Learning: 2025-10-23T11:04:44.417Z
Learnt from: CR
Repo: HavenDV/H.NotifyIcon PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-23T11:04:44.417Z
Learning: Applies to src/libs/H.NotifyIcon.Shared/**/*.{Wpf,WinUI,WinRT}.cs : Use platform-specific filename suffixes .Wpf.cs, .WinUI.cs, .WinRT.cs for platform-specific implementations in the Shared project

Applied to files:

  • src/libs/H.NotifyIcon.Shared/TaskbarIcon.cs
  • src/libs/H.NotifyIcon.Shared/TaskbarIcon.Notifications.cs
  • src/libs/H.NotifyIcon.Shared/TaskbarIcon.Dispose.cs
📚 Learning: 2025-10-23T11:04:44.417Z
Learnt from: CR
Repo: HavenDV/H.NotifyIcon PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-23T11:04:44.417Z
Learning: Applies to src/libs/{H.GeneratedIcons.System.Drawing,H.GeneratedIcons.SkiaSharp}/**/*.cs : When modifying icon generation, update both System.Drawing and SkiaSharp implementations to maintain feature parity

Applied to files:

  • src/libs/H.NotifyIcon.Shared/TaskbarIcon.Notifications.cs
  • src/libs/H.NotifyIcon.Shared/TaskbarIcon.Dispose.cs
📚 Learning: 2025-10-23T11:04:44.417Z
Learnt from: CR
Repo: HavenDV/H.NotifyIcon PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-23T11:04:44.417Z
Learning: Applies to tests/H.NotifyIcon.IntegrationTests/*.csproj : Maintain test target frameworks for Windows (e.g., net4.8 and net9.0-windows) in the integration test project

Applied to files:

  • src/libs/H.NotifyIcon.Shared/TaskbarIcon.Notifications.cs
🧬 Code graph analysis (1)
src/libs/H.NotifyIcon.Shared/TaskbarIcon.cs (1)
src/libs/H.NotifyIcon.Shared/TaskbarIcon.Notifications.cs (1)
  • RefreshNativeBalloon (124-152)
⏰ 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: Build and test PR / Build, test and publish
  • GitHub Check: claude-review
🔇 Additional comments (7)
src/libs/H.NotifyIcon.Shared/TaskbarIcon.cs (1)

92-94: LGTM - Timer initialization is correct.

The native balloon refresh timer is properly initialized for non-MacOS platforms with a callback to RefreshNativeBalloon(). The timer is created in a stopped state and will be started when needed via ResetBalloonCloseTimer().

src/libs/H.NotifyIcon.Shared/TaskbarIcon.Dispose.cs (1)

123-125: LGTM - Proper cleanup of refresh timer.

The native balloon refresh timer is correctly disposed alongside other timers for non-MacOS platforms, with appropriate null-safe access.

src/libs/H.NotifyIcon.Shared/TaskbarIcon.CustomNotifications.Wpf.cs (2)

153-178: Documentation and implementation are clear.

The updated documentation clearly describes the dual behavior for custom vs. native balloons. The 25-second refresh interval is a reasonable choice to stay within Windows' 30-second notification timeout limit.


206-210: Proper cleanup on balloon close.

The implementation correctly stops the native refresh timer and clears the tracked notification when the balloon is closed.

src/libs/H.NotifyIcon.Shared/TaskbarIcon.Notifications.cs (3)

87-88: Track notification parameters for refresh.

The notification parameters are properly captured for potential refresh operations.


114-116: Proper cleanup in ClearNotifications.

The timer is correctly stopped and the tracked notification is cleared before clearing notifications.


147-150: Good defensive programming with exception handling.

The try-catch block ensures that if a refresh fails, the timer is stopped gracefully. This prevents the timer from continuing to fire and potentially logging errors or causing other issues.

Comment on lines +14 to +22
/// <summary>
/// Timer for keeping native balloon notifications alive.
/// </summary>
private Timer? nativeBalloonRefreshTimer;

/// <summary>
/// Tracks the last notification parameters for refresh.
/// </summary>
private (string title, string message, NotificationIcon icon, nint? customIconHandle, bool largeIcon, bool sound, bool respectQuietTime, bool realtime, TimeSpan? timeout)? lastNotification;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Consider thread synchronization for lastNotification.

The lastNotification field is accessed from multiple threads (UI thread in ShowNotification, ClearNotifications, CloseBalloon, and timer thread in RefreshNativeBalloon) without synchronization. While the nullable check provides some protection, there's a race condition where lastNotification could be set to null between the check on Line 126 and the deconstruction on Line 132, potentially causing a NullReferenceException.

Consider adding a lock or using Interlocked operations to ensure thread-safe access to this field.

🔎 Suggested approach using local copy
 private void RefreshNativeBalloon()
 {
-    if (IsDisposed || lastNotification == null)
+    var notification = lastNotification;
+    if (IsDisposed || notification == null)
     {
         _ = nativeBalloonRefreshTimer?.Change(Timeout.Infinite, Timeout.Infinite);
         return;
     }
 
-    var (title, message, icon, customIconHandle, largeIcon, sound, respectQuietTime, realtime, timeout) = lastNotification.Value;
+    var (title, message, icon, customIconHandle, largeIcon, sound, respectQuietTime, realtime, timeout) = notification.Value;

This captures the field value atomically and works with the local copy, eliminating the race condition.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/libs/H.NotifyIcon.Shared/TaskbarIcon.Notifications.cs around lines 14 to
22, the field lastNotification is read/written from multiple threads which
creates a race between the null check and deconstruction; fix it by making reads
atomic — either protect all accesses with a private readonly sync object and
lock around both writers and readers, or (simpler) capture lastNotification into
a local variable at the start of the method on the timer/UI paths, check that
local for null and then deconstruct/use it; if you clear it set via
Interlocked.Exchange(null) to avoid torn writes and ensure all code paths
consistently use the same synchronization approach.

Comment on lines +134 to +145
try
{
TrayIcon.ShowNotification(
title: title,
message: message,
icon: icon,
customIconHandle: customIconHandle,
largeIcon: largeIcon,
sound: sound,
respectQuietTime: respectQuietTime,
realtime: realtime,
timeout: timeout);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Sound will play on every refresh.

Line 142 passes the original sound parameter when refreshing the notification. This means if sound is enabled, the notification sound will play every 25 seconds, which would be very annoying to users. The refresh operation should disable sound to avoid repeatedly playing the notification sound.

🔎 Suggested fix
         try
         {
             TrayIcon.ShowNotification(
                 title: title,
                 message: message,
                 icon: icon,
                 customIconHandle: customIconHandle,
                 largeIcon: largeIcon,
-                sound: sound,
+                sound: false, // Disable sound on refresh to avoid repeated notifications
                 respectQuietTime: respectQuietTime,
                 realtime: realtime,
                 timeout: timeout);
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try
{
TrayIcon.ShowNotification(
title: title,
message: message,
icon: icon,
customIconHandle: customIconHandle,
largeIcon: largeIcon,
sound: sound,
respectQuietTime: respectQuietTime,
realtime: realtime,
timeout: timeout);
try
{
TrayIcon.ShowNotification(
title: title,
message: message,
icon: icon,
customIconHandle: customIconHandle,
largeIcon: largeIcon,
sound: false, // Disable sound on refresh to avoid repeated notifications
respectQuietTime: respectQuietTime,
realtime: realtime,
timeout: timeout);
}
🤖 Prompt for AI Agents
In src/libs/H.NotifyIcon.Shared/TaskbarIcon.Notifications.cs around lines 134 to
145, the refresh call to TrayIcon.ShowNotification reuses the original sound
parameter so the notification sound plays on every refresh; change the refresh
path to explicitly disable sound (pass the no-sound value/flag) when calling
ShowNotification during refreshes so that repeat refreshes do not trigger the
notification sound.

@github-actions github-actions bot merged commit cce77a7 into master Dec 26, 2025
7 checks passed
@HavenDV HavenDV deleted the claude/issue-220-20251223-1122 branch December 26, 2025 14:15
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.

ResetBalloonCloseTimer not working

2 participants