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) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy