From 7ff7914c14e04a4f5e43d1d2f8640962bee301d0 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 24 Jun 2025 11:21:33 +0200 Subject: [PATCH 1/7] chore: added notifications for vpn lifecycle start/stop --- App/Views/TrayWindow.xaml.cs | 69 ++++++++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 3 deletions(-) diff --git a/App/Views/TrayWindow.xaml.cs b/App/Views/TrayWindow.xaml.cs index 6131e25..4b638c8 100644 --- a/App/Views/TrayWindow.xaml.cs +++ b/App/Views/TrayWindow.xaml.cs @@ -11,7 +11,9 @@ using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media.Animation; using System; +using System.Collections.Generic; using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; using Windows.Graphics; using Windows.System; @@ -21,7 +23,7 @@ namespace Coder.Desktop.App.Views; -public sealed partial class TrayWindow : Window +public sealed partial class TrayWindow : Window, INotificationHandler { private const int WIDTH = 300; @@ -33,17 +35,25 @@ public sealed partial class TrayWindow : Window private int _lastWindowHeight; private Storyboard? _currentSb; + private VpnLifecycle prevVpnLifecycle = VpnLifecycle.Stopped; + private RpcLifecycle prevRpcLifecycle = RpcLifecycle.Disconnected; + + private NativeApi.POINT? _lastActivatePosition; + private readonly IRpcController _rpcController; private readonly ICredentialManager _credentialManager; private readonly ISyncSessionController _syncSessionController; private readonly IUpdateController _updateController; + private readonly IUserNotifier _userNotifier; private readonly TrayWindowLoadingPage _loadingPage; private readonly TrayWindowDisconnectedPage _disconnectedPage; private readonly TrayWindowLoginRequiredPage _loginRequiredPage; private readonly TrayWindowMainPage _mainPage; - public TrayWindow(IRpcController rpcController, ICredentialManager credentialManager, + public TrayWindow( + IRpcController rpcController, ICredentialManager credentialManager, ISyncSessionController syncSessionController, IUpdateController updateController, + IUserNotifier userNotifier, TrayWindowLoadingPage loadingPage, TrayWindowDisconnectedPage disconnectedPage, TrayWindowLoginRequiredPage loginRequiredPage, TrayWindowMainPage mainPage) @@ -52,12 +62,14 @@ public TrayWindow(IRpcController rpcController, ICredentialManager credentialMan _credentialManager = credentialManager; _syncSessionController = syncSessionController; _updateController = updateController; + _userNotifier = userNotifier; _loadingPage = loadingPage; _disconnectedPage = disconnectedPage; _loginRequiredPage = loginRequiredPage; _mainPage = mainPage; InitializeComponent(); + _userNotifier.RegisterHandler("TrayWindow", this); AppWindow.Hide(); Activated += Window_Activated; RootFrame.SizeChanged += RootFrame_SizeChanged; @@ -142,9 +154,55 @@ private void SetPageByState(RpcModel rpcModel, CredentialModel credentialModel, } } + private void NotifyUser(RpcModel rpcModel) + { + // This method is called when the state changes, but we don't want to notify + // the user if the state hasn't changed. + var isRpcLifecycleChanged = rpcModel.RpcLifecycle != RpcLifecycle.Connecting && prevRpcLifecycle != rpcModel.RpcLifecycle; + var isVpnLifecycleChanged = (rpcModel.VpnLifecycle == VpnLifecycle.Started || rpcModel.VpnLifecycle == VpnLifecycle.Stopped) && prevVpnLifecycle != rpcModel.VpnLifecycle; + + if (!isRpcLifecycleChanged && !isVpnLifecycleChanged) + { + return; + } + var message = string.Empty; + // Compose the message based on the lifecycle changes + if (isRpcLifecycleChanged) + message += rpcModel.RpcLifecycle switch + { + RpcLifecycle.Connected => "Connected to Coder vpn service.", + RpcLifecycle.Disconnected => "Disconnected from Coder vpn service.", + _ => "" // This will never be hit. + }; + + if(message.Length > 0 && isVpnLifecycleChanged) + message += " "; + + if (isVpnLifecycleChanged) + message += rpcModel.VpnLifecycle switch + { + VpnLifecycle.Started => "Coder Connect started.", + VpnLifecycle.Stopped => "Coder Connect stopped.", + _ => "" // This will never be hit. + }; + + // Save state for the next notification check + prevRpcLifecycle = rpcModel.RpcLifecycle; + prevVpnLifecycle = rpcModel.VpnLifecycle; + + if (_aw.IsVisible) + { + return; // No need to notify if the window is not visible. + } + + // Trigger notification + _userNotifier.ShowActionNotification(message, string.Empty, nameof(TrayWindow), null, CancellationToken.None); + } + private void RpcController_StateChanged(object? _, RpcModel model) { SetPageByState(model, _credentialManager.GetCachedCredentials(), _syncSessionController.GetState()); + NotifyUser(model); } private void CredentialManager_CredentialsChanged(object? _, CredentialModel model) @@ -316,6 +374,11 @@ private void Tray_Exit() _ = ((App)Application.Current).ExitApplication(); } + public void HandleNotificationActivation(IDictionary args) + { + Tray_Open(); + } + public static class NativeApi { [DllImport("dwmapi.dll")] @@ -336,7 +399,7 @@ internal enum TaskbarPosition { Left, Top, Right, Bottom } internal readonly record struct TaskbarInfo(TaskbarPosition Position, int Gap, bool AutoHide); // ----------------------------------------------------------------------------- - // Taskbar helpers – ABM_GETTASKBARPOS / ABM_GETSTATE via SHAppBarMessage + // Taskbar helpers � ABM_GETTASKBARPOS / ABM_GETSTATE via SHAppBarMessage // ----------------------------------------------------------------------------- private static TaskbarInfo GetTaskbarInfo(DisplayArea area) { From 08d523fd2caea672309e63fa89b4d0d370732248 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Thu, 26 Jun 2025 15:06:35 +0200 Subject: [PATCH 2/7] added a default handler --- App/Services/UserNotifier.cs | 46 +++++++++++++++++++++++++++++------- App/Views/TrayWindow.xaml.cs | 19 ++++----------- 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/App/Services/UserNotifier.cs b/App/Services/UserNotifier.cs index 5ad8e38..4547a04 100644 --- a/App/Services/UserNotifier.cs +++ b/App/Services/UserNotifier.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Coder.Desktop.App.Views; using Microsoft.Extensions.Logging; using Microsoft.Windows.AppNotifications; using Microsoft.Windows.AppNotifications.Builder; @@ -20,17 +21,26 @@ public interface IUserNotifier : INotificationHandler, IAsyncDisposable public void UnregisterHandler(string name); public Task ShowErrorNotification(string title, string message, CancellationToken ct = default); - public Task ShowActionNotification(string title, string message, string handlerName, IDictionary? args = null, CancellationToken ct = default); + public Task ShowActionNotification(string title, string message, string? handlerName, IDictionary? args = null, CancellationToken ct = default); } -public class UserNotifier(ILogger logger, IDispatcherQueueManager dispatcherQueueManager) : IUserNotifier +public class UserNotifier : IUserNotifier { private const string CoderNotificationHandler = "CoderNotificationHandler"; private readonly AppNotificationManager _notificationManager = AppNotificationManager.Default; + private readonly ILogger _logger; + private readonly IDispatcherQueueManager _dispatcherQueueManager; private ConcurrentDictionary Handlers { get; } = new(); + public UserNotifier(ILogger logger, IDispatcherQueueManager dispatcherQueueManager) + { + _logger = logger; + _dispatcherQueueManager = dispatcherQueueManager; + Handlers.TryAdd(nameof(DefaultNotificationHandler), new DefaultNotificationHandler()); + } + public ValueTask DisposeAsync() { return ValueTask.CompletedTask; @@ -61,10 +71,18 @@ public Task ShowErrorNotification(string title, string message, CancellationToke return Task.CompletedTask; } - public Task ShowActionNotification(string title, string message, string handlerName, IDictionary? args = null, CancellationToken ct = default) + public Task ShowActionNotification(string title, string message, string? handlerName, IDictionary? args = null, CancellationToken ct = default) { - if (!Handlers.TryGetValue(handlerName, out _)) - throw new InvalidOperationException($"No action handler with the name '{handlerName}' is registered."); + if (handlerName == null) + { + // Use default handler if no handler name is provided + handlerName = nameof(DefaultNotificationHandler); + } + else + { + if (!Handlers.TryGetValue(handlerName, out _)) + throw new InvalidOperationException($"No action handler with the name '{handlerName}' is registered. Use null for default"); + } var builder = new AppNotificationBuilder() .AddText(title) @@ -90,11 +108,11 @@ public void HandleNotificationActivation(IDictionary args) if (!Handlers.TryGetValue(handlerName, out var handler)) { - logger.LogWarning("no action handler '{HandlerName}' found for notification activation, ignoring", handlerName); + _logger.LogWarning("no action handler '{HandlerName}' found for notification activation, ignoring", handlerName); return; } - dispatcherQueueManager.RunInUiThread(() => + _dispatcherQueueManager.RunInUiThread(() => { try { @@ -102,8 +120,20 @@ public void HandleNotificationActivation(IDictionary args) } catch (Exception ex) { - logger.LogWarning(ex, "could not handle activation for notification with handler '{HandlerName}", handlerName); + _logger.LogWarning(ex, "could not handle activation for notification with handler '{HandlerName}", handlerName); } }); } } + +public class DefaultNotificationHandler : INotificationHandler +{ + public void HandleNotificationActivation(IDictionary _) + { + var app = (App)Microsoft.UI.Xaml.Application.Current; + if (app != null && app.TrayWindow != null) + { + app.TrayWindow.Tray_Open(); + } + } +} diff --git a/App/Views/TrayWindow.xaml.cs b/App/Views/TrayWindow.xaml.cs index 4b638c8..7bbf946 100644 --- a/App/Views/TrayWindow.xaml.cs +++ b/App/Views/TrayWindow.xaml.cs @@ -23,7 +23,7 @@ namespace Coder.Desktop.App.Views; -public sealed partial class TrayWindow : Window, INotificationHandler +public sealed partial class TrayWindow : Window { private const int WIDTH = 300; @@ -38,8 +38,6 @@ public sealed partial class TrayWindow : Window, INotificationHandler private VpnLifecycle prevVpnLifecycle = VpnLifecycle.Stopped; private RpcLifecycle prevRpcLifecycle = RpcLifecycle.Disconnected; - private NativeApi.POINT? _lastActivatePosition; - private readonly IRpcController _rpcController; private readonly ICredentialManager _credentialManager; private readonly ISyncSessionController _syncSessionController; @@ -69,7 +67,6 @@ public TrayWindow( _mainPage = mainPage; InitializeComponent(); - _userNotifier.RegisterHandler("TrayWindow", this); AppWindow.Hide(); Activated += Window_Activated; RootFrame.SizeChanged += RootFrame_SizeChanged; @@ -158,7 +155,7 @@ private void NotifyUser(RpcModel rpcModel) { // This method is called when the state changes, but we don't want to notify // the user if the state hasn't changed. - var isRpcLifecycleChanged = rpcModel.RpcLifecycle != RpcLifecycle.Connecting && prevRpcLifecycle != rpcModel.RpcLifecycle; + var isRpcLifecycleChanged = rpcModel.RpcLifecycle == RpcLifecycle.Disconnected && prevRpcLifecycle != rpcModel.RpcLifecycle; var isVpnLifecycleChanged = (rpcModel.VpnLifecycle == VpnLifecycle.Started || rpcModel.VpnLifecycle == VpnLifecycle.Stopped) && prevVpnLifecycle != rpcModel.VpnLifecycle; if (!isRpcLifecycleChanged && !isVpnLifecycleChanged) @@ -170,8 +167,7 @@ private void NotifyUser(RpcModel rpcModel) if (isRpcLifecycleChanged) message += rpcModel.RpcLifecycle switch { - RpcLifecycle.Connected => "Connected to Coder vpn service.", - RpcLifecycle.Disconnected => "Disconnected from Coder vpn service.", + RpcLifecycle.Disconnected => "Disconnected from Coder background service.", _ => "" // This will never be hit. }; @@ -196,7 +192,7 @@ private void NotifyUser(RpcModel rpcModel) } // Trigger notification - _userNotifier.ShowActionNotification(message, string.Empty, nameof(TrayWindow), null, CancellationToken.None); + _userNotifier.ShowActionNotification(message, string.Empty, null, null, CancellationToken.None); } private void RpcController_StateChanged(object? _, RpcModel model) @@ -355,7 +351,7 @@ private void Window_Activated(object sender, WindowActivatedEventArgs e) } [RelayCommand] - private void Tray_Open() + public void Tray_Open() { MoveResizeAndActivate(); } @@ -374,11 +370,6 @@ private void Tray_Exit() _ = ((App)Application.Current).ExitApplication(); } - public void HandleNotificationActivation(IDictionary args) - { - Tray_Open(); - } - public static class NativeApi { [DllImport("dwmapi.dll")] From 9e12405a1269b7e2673a2430ddaa8394512221f4 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Thu, 26 Jun 2025 15:08:47 +0200 Subject: [PATCH 3/7] typo --- App/Views/TrayWindow.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/App/Views/TrayWindow.xaml.cs b/App/Views/TrayWindow.xaml.cs index 7bbf946..7485e82 100644 --- a/App/Views/TrayWindow.xaml.cs +++ b/App/Views/TrayWindow.xaml.cs @@ -390,7 +390,7 @@ internal enum TaskbarPosition { Left, Top, Right, Bottom } internal readonly record struct TaskbarInfo(TaskbarPosition Position, int Gap, bool AutoHide); // ----------------------------------------------------------------------------- - // Taskbar helpers � ABM_GETTASKBARPOS / ABM_GETSTATE via SHAppBarMessage + // Taskbar helpers - ABM_GETTASKBARPOS / ABM_GETSTATE via SHAppBarMessage // ----------------------------------------------------------------------------- private static TaskbarInfo GetTaskbarInfo(DisplayArea area) { From ea7d39b681bc0a24db32da950898b43bc99e55a7 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Thu, 26 Jun 2025 15:17:02 +0200 Subject: [PATCH 4/7] format --- App/Views/TrayWindow.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/App/Views/TrayWindow.xaml.cs b/App/Views/TrayWindow.xaml.cs index 7485e82..bc52330 100644 --- a/App/Views/TrayWindow.xaml.cs +++ b/App/Views/TrayWindow.xaml.cs @@ -171,7 +171,7 @@ private void NotifyUser(RpcModel rpcModel) _ => "" // This will never be hit. }; - if(message.Length > 0 && isVpnLifecycleChanged) + if (message.Length > 0 && isVpnLifecycleChanged) message += " "; if (isVpnLifecycleChanged) From 87a6a781f604fc1062af59c2318124bc6890c0f0 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Thu, 26 Jun 2025 15:19:18 +0200 Subject: [PATCH 5/7] remove unecessary change --- App/Views/TrayWindow.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/App/Views/TrayWindow.xaml.cs b/App/Views/TrayWindow.xaml.cs index bc52330..c2faea0 100644 --- a/App/Views/TrayWindow.xaml.cs +++ b/App/Views/TrayWindow.xaml.cs @@ -390,7 +390,7 @@ internal enum TaskbarPosition { Left, Top, Right, Bottom } internal readonly record struct TaskbarInfo(TaskbarPosition Position, int Gap, bool AutoHide); // ----------------------------------------------------------------------------- - // Taskbar helpers - ABM_GETTASKBARPOS / ABM_GETSTATE via SHAppBarMessage + // Taskbar helpers – ABM_GETTASKBARPOS / ABM_GETSTATE via SHAppBarMessage // ----------------------------------------------------------------------------- private static TaskbarInfo GetTaskbarInfo(DisplayArea area) { From 370f3d28be3b7b04a1e81cf0a87cec2de707b2b6 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Mon, 30 Jun 2025 17:32:24 +0200 Subject: [PATCH 6/7] PR review fixes --- App/App.xaml.cs | 12 +++++++- App/Services/UserNotifier.cs | 45 ++++++++++++++------------- App/Views/TrayWindow.xaml.cs | 59 ++++++++++++++++++------------------ 3 files changed, 63 insertions(+), 53 deletions(-) diff --git a/App/App.xaml.cs b/App/App.xaml.cs index f4c05a2..3165e2f 100644 --- a/App/App.xaml.cs +++ b/App/App.xaml.cs @@ -27,7 +27,7 @@ namespace Coder.Desktop.App; -public partial class App : Application, IDispatcherQueueManager +public partial class App : Application, IDispatcherQueueManager, INotificationHandler { private const string MutagenControllerConfigSection = "MutagenController"; private const string UpdaterConfigSection = "Updater"; @@ -91,6 +91,7 @@ public App() services.AddSingleton(); services.AddSingleton(_ => this); + services.AddSingleton(_ => this); services.AddSingleton(_ => new WindowsCredentialBackend(WindowsCredentialBackend.CoderCredentialsTargetName)); services.AddSingleton(); @@ -335,4 +336,13 @@ public void RunInUiThread(DispatcherQueueHandler action) } dispatcherQueue.TryEnqueue(action); } + + public void HandleNotificationActivation(IDictionary args) + { + var app = (App)Current; + if (app != null && app.TrayWindow != null) + { + app.TrayWindow.Tray_Open(); + } + } } diff --git a/App/Services/UserNotifier.cs b/App/Services/UserNotifier.cs index 4547a04..19b5846 100644 --- a/App/Services/UserNotifier.cs +++ b/App/Services/UserNotifier.cs @@ -21,12 +21,23 @@ public interface IUserNotifier : INotificationHandler, IAsyncDisposable public void UnregisterHandler(string name); public Task ShowErrorNotification(string title, string message, CancellationToken ct = default); + /// + /// This method allows to display a Windows-native notification with an action defined in + /// and provided . + /// + /// Title of the notification. + /// Message to be displayed in the notification body. + /// Handler should be e.g. nameof(Handler) where Handler + /// implements . + /// If handler is null the action will open Coder Desktop. + /// Arguments to be provided to the handler when executing the action. public Task ShowActionNotification(string title, string message, string? handlerName, IDictionary? args = null, CancellationToken ct = default); } public class UserNotifier : IUserNotifier { private const string CoderNotificationHandler = "CoderNotificationHandler"; + private const string DefaultNotificationHandler = "DefaultNotificationHandler"; private readonly AppNotificationManager _notificationManager = AppNotificationManager.Default; private readonly ILogger _logger; @@ -34,11 +45,14 @@ public class UserNotifier : IUserNotifier private ConcurrentDictionary Handlers { get; } = new(); - public UserNotifier(ILogger logger, IDispatcherQueueManager dispatcherQueueManager) + public UserNotifier(ILogger logger, IDispatcherQueueManager dispatcherQueueManager, + INotificationHandler notificationHandler) { _logger = logger; _dispatcherQueueManager = dispatcherQueueManager; - Handlers.TryAdd(nameof(DefaultNotificationHandler), new DefaultNotificationHandler()); + var defaultHandlerAdded = Handlers.TryAdd(DefaultNotificationHandler, notificationHandler); + if (!defaultHandlerAdded) + throw new Exception($"UserNotifier failed to be initialized with {nameof(DefaultNotificationHandler)}"); } public ValueTask DisposeAsync() @@ -60,6 +74,8 @@ public void RegisterHandler(string name, INotificationHandler handler) public void UnregisterHandler(string name) { + if (name == nameof(DefaultNotificationHandler)) + throw new InvalidOperationException($"You cannot remove '{name}'."); if (!Handlers.TryRemove(name, out _)) throw new InvalidOperationException($"No handler with the name '{name}' is registered."); } @@ -74,16 +90,11 @@ public Task ShowErrorNotification(string title, string message, CancellationToke public Task ShowActionNotification(string title, string message, string? handlerName, IDictionary? args = null, CancellationToken ct = default) { if (handlerName == null) - { - // Use default handler if no handler name is provided - handlerName = nameof(DefaultNotificationHandler); - } - else - { - if (!Handlers.TryGetValue(handlerName, out _)) - throw new InvalidOperationException($"No action handler with the name '{handlerName}' is registered. Use null for default"); - } + handlerName = nameof(DefaultNotificationHandler); // Use default handler if no handler name is provided + if (!Handlers.TryGetValue(handlerName, out _)) + throw new InvalidOperationException($"No action handler with the name '{handlerName}' is registered."); + var builder = new AppNotificationBuilder() .AddText(title) .AddText(message) @@ -125,15 +136,3 @@ public void HandleNotificationActivation(IDictionary args) }); } } - -public class DefaultNotificationHandler : INotificationHandler -{ - public void HandleNotificationActivation(IDictionary _) - { - var app = (App)Microsoft.UI.Xaml.Application.Current; - if (app != null && app.TrayWindow != null) - { - app.TrayWindow.Tray_Open(); - } - } -} diff --git a/App/Views/TrayWindow.xaml.cs b/App/Views/TrayWindow.xaml.cs index c2faea0..7269e68 100644 --- a/App/Views/TrayWindow.xaml.cs +++ b/App/Views/TrayWindow.xaml.cs @@ -9,6 +9,7 @@ using Microsoft.UI.Windowing; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Documents; using Microsoft.UI.Xaml.Media.Animation; using System; using System.Collections.Generic; @@ -35,8 +36,8 @@ public sealed partial class TrayWindow : Window private int _lastWindowHeight; private Storyboard? _currentSb; - private VpnLifecycle prevVpnLifecycle = VpnLifecycle.Stopped; - private RpcLifecycle prevRpcLifecycle = RpcLifecycle.Disconnected; + private VpnLifecycle curVpnLifecycle = VpnLifecycle.Stopped; + private RpcLifecycle curRpcLifecycle = RpcLifecycle.Disconnected; private readonly IRpcController _rpcController; private readonly ICredentialManager _credentialManager; @@ -151,54 +152,54 @@ private void SetPageByState(RpcModel rpcModel, CredentialModel credentialModel, } } - private void NotifyUser(RpcModel rpcModel) + private void MaybeNotifyUser(RpcModel rpcModel) { // This method is called when the state changes, but we don't want to notify // the user if the state hasn't changed. - var isRpcLifecycleChanged = rpcModel.RpcLifecycle == RpcLifecycle.Disconnected && prevRpcLifecycle != rpcModel.RpcLifecycle; - var isVpnLifecycleChanged = (rpcModel.VpnLifecycle == VpnLifecycle.Started || rpcModel.VpnLifecycle == VpnLifecycle.Stopped) && prevVpnLifecycle != rpcModel.VpnLifecycle; + var isRpcLifecycleChanged = rpcModel.RpcLifecycle == RpcLifecycle.Disconnected && curRpcLifecycle != rpcModel.RpcLifecycle; + var isVpnLifecycleChanged = (rpcModel.VpnLifecycle == VpnLifecycle.Started || rpcModel.VpnLifecycle == VpnLifecycle.Stopped) && curVpnLifecycle != rpcModel.VpnLifecycle; if (!isRpcLifecycleChanged && !isVpnLifecycleChanged) { return; } - var message = string.Empty; - // Compose the message based on the lifecycle changes - if (isRpcLifecycleChanged) - message += rpcModel.RpcLifecycle switch - { - RpcLifecycle.Disconnected => "Disconnected from Coder background service.", - _ => "" // This will never be hit. - }; - if (message.Length > 0 && isVpnLifecycleChanged) - message += " "; + var oldRpcLifeycle = curRpcLifecycle; + var oldVpnLifecycle = curVpnLifecycle; + curRpcLifecycle = rpcModel.RpcLifecycle; + curVpnLifecycle = rpcModel.VpnLifecycle; - if (isVpnLifecycleChanged) - message += rpcModel.VpnLifecycle switch - { - VpnLifecycle.Started => "Coder Connect started.", - VpnLifecycle.Stopped => "Coder Connect stopped.", - _ => "" // This will never be hit. - }; + var messages = new List(); - // Save state for the next notification check - prevRpcLifecycle = rpcModel.RpcLifecycle; - prevVpnLifecycle = rpcModel.VpnLifecycle; + if (oldRpcLifeycle != RpcLifecycle.Disconnected && curRpcLifecycle == RpcLifecycle.Disconnected) + { + messages.Add("Disconnected from Coder background service."); + } - if (_aw.IsVisible) + if (oldVpnLifecycle != curVpnLifecycle) { - return; // No need to notify if the window is not visible. + switch (curVpnLifecycle) + { + case VpnLifecycle.Started: + messages.Add("Coder Connect started."); + break; + case VpnLifecycle.Stopped: + messages.Add("Coder Connect stopped."); + break; + } } - // Trigger notification + if (messages.Count == 0) return; + if (_aw.IsVisible) return; + + var message = string.Join(" ", messages); _userNotifier.ShowActionNotification(message, string.Empty, null, null, CancellationToken.None); } private void RpcController_StateChanged(object? _, RpcModel model) { SetPageByState(model, _credentialManager.GetCachedCredentials(), _syncSessionController.GetState()); - NotifyUser(model); + MaybeNotifyUser(model); } private void CredentialManager_CredentialsChanged(object? _, CredentialModel model) From 2411356d2b8065d83c084227b5c691dc12c8eb67 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Mon, 30 Jun 2025 17:51:38 +0200 Subject: [PATCH 7/7] fmt fixes --- App/Services/UserNotifier.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/App/Services/UserNotifier.cs b/App/Services/UserNotifier.cs index 19b5846..e759c50 100644 --- a/App/Services/UserNotifier.cs +++ b/App/Services/UserNotifier.cs @@ -94,7 +94,7 @@ public Task ShowActionNotification(string title, string message, string? handler if (!Handlers.TryGetValue(handlerName, out _)) throw new InvalidOperationException($"No action handler with the name '{handlerName}' is registered."); - + var builder = new AppNotificationBuilder() .AddText(title) .AddText(message)