-
-
Notifications
You must be signed in to change notification settings - Fork 53
fix: Make ResetBalloonCloseTimer work with native notifications #221
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- 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]>
📝 WalkthroughWalkthroughThe 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
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
Pre-merge checks and finishing touches✅ Passed checks (5 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
Code ReviewThank you for this contribution! The implementation addresses issue #220 by enabling native notifications to persist indefinitely when ✅ Strengths
🔍 Potential Issues1. Thread Safety Concerns (Medium Priority)The code accesses
Recommendation: Consider using a lock or making 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)
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 Recommendation: Consider forcing TrayIcon.ShowNotification(
// ... other params
sound: false, // Don't replay sound on refresh
// ... remaining params
);5. Missing Timer Null-Check in Constructor (Low Priority)In 🧪 Test CoverageThe PR lacks tests for the new functionality. Consider adding:
Given the Windows-specific nature and timing dependencies, these could be manual tests or integration tests similar to 📝 Minor Suggestions
🎯 RecommendationThis is a functional solution that addresses the reported issue. The main concerns are:
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 👍 |
There was a problem hiding this 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
timeoutparameter 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 (ornull) would be more appropriate for refresh operations.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
src/libs/H.NotifyIcon.Shared/TaskbarIcon.CustomNotifications.Wpf.cssrc/libs/H.NotifyIcon.Shared/TaskbarIcon.Dispose.cssrc/libs/H.NotifyIcon.Shared/TaskbarIcon.Notifications.cssrc/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.cssrc/libs/H.NotifyIcon.Shared/TaskbarIcon.Notifications.cssrc/libs/H.NotifyIcon.Shared/TaskbarIcon.Dispose.cssrc/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.cssrc/libs/H.NotifyIcon.Shared/TaskbarIcon.Notifications.cssrc/libs/H.NotifyIcon.Shared/TaskbarIcon.Dispose.cssrc/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.cssrc/libs/H.NotifyIcon.Shared/TaskbarIcon.Notifications.cssrc/libs/H.NotifyIcon.Shared/TaskbarIcon.Dispose.cssrc/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.cssrc/libs/H.NotifyIcon.Shared/TaskbarIcon.Notifications.cssrc/libs/H.NotifyIcon.Shared/TaskbarIcon.Dispose.cssrc/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.cssrc/libs/H.NotifyIcon.Shared/TaskbarIcon.Notifications.cssrc/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.cssrc/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 viaResetBalloonCloseTimer().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.
| /// <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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| try | ||
| { | ||
| TrayIcon.ShowNotification( | ||
| title: title, | ||
| message: message, | ||
| icon: icon, | ||
| customIconHandle: customIconHandle, | ||
| largeIcon: largeIcon, | ||
| sound: sound, | ||
| respectQuietTime: respectQuietTime, | ||
| realtime: realtime, | ||
| timeout: timeout); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
Summary
Fixed issue where
ResetBalloonCloseTimer()didn't work with native Windows notifications shown viaShowNotification().Changes
ResetBalloonCloseTimer()to handle both custom and native balloonsFixes #220
Generated with Claude Code
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.