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 5ad8e38..e759c50 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,40 @@ 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); + /// + /// 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(ILogger logger, IDispatcherQueueManager dispatcherQueueManager) : IUserNotifier +public class UserNotifier : IUserNotifier { private const string CoderNotificationHandler = "CoderNotificationHandler"; + private const string DefaultNotificationHandler = "DefaultNotificationHandler"; 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, + INotificationHandler notificationHandler) + { + _logger = logger; + _dispatcherQueueManager = dispatcherQueueManager; + var defaultHandlerAdded = Handlers.TryAdd(DefaultNotificationHandler, notificationHandler); + if (!defaultHandlerAdded) + throw new Exception($"UserNotifier failed to be initialized with {nameof(DefaultNotificationHandler)}"); + } + public ValueTask DisposeAsync() { return ValueTask.CompletedTask; @@ -50,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."); } @@ -61,8 +87,11 @@ 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 (handlerName == null) + 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."); @@ -90,11 +119,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,7 +131,7 @@ 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); } }); } diff --git a/App/Views/TrayWindow.xaml.cs b/App/Views/TrayWindow.xaml.cs index 6131e25..7269e68 100644 --- a/App/Views/TrayWindow.xaml.cs +++ b/App/Views/TrayWindow.xaml.cs @@ -9,9 +9,12 @@ 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; using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; using Windows.Graphics; using Windows.System; @@ -33,17 +36,23 @@ public sealed partial class TrayWindow : Window private int _lastWindowHeight; private Storyboard? _currentSb; + private VpnLifecycle curVpnLifecycle = VpnLifecycle.Stopped; + private RpcLifecycle curRpcLifecycle = RpcLifecycle.Disconnected; + 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,6 +61,7 @@ public TrayWindow(IRpcController rpcController, ICredentialManager credentialMan _credentialManager = credentialManager; _syncSessionController = syncSessionController; _updateController = updateController; + _userNotifier = userNotifier; _loadingPage = loadingPage; _disconnectedPage = disconnectedPage; _loginRequiredPage = loginRequiredPage; @@ -142,9 +152,54 @@ private void SetPageByState(RpcModel rpcModel, CredentialModel credentialModel, } } + 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 && curRpcLifecycle != rpcModel.RpcLifecycle; + var isVpnLifecycleChanged = (rpcModel.VpnLifecycle == VpnLifecycle.Started || rpcModel.VpnLifecycle == VpnLifecycle.Stopped) && curVpnLifecycle != rpcModel.VpnLifecycle; + + if (!isRpcLifecycleChanged && !isVpnLifecycleChanged) + { + return; + } + + var oldRpcLifeycle = curRpcLifecycle; + var oldVpnLifecycle = curVpnLifecycle; + curRpcLifecycle = rpcModel.RpcLifecycle; + curVpnLifecycle = rpcModel.VpnLifecycle; + + var messages = new List(); + + if (oldRpcLifeycle != RpcLifecycle.Disconnected && curRpcLifecycle == RpcLifecycle.Disconnected) + { + messages.Add("Disconnected from Coder background service."); + } + + if (oldVpnLifecycle != curVpnLifecycle) + { + switch (curVpnLifecycle) + { + case VpnLifecycle.Started: + messages.Add("Coder Connect started."); + break; + case VpnLifecycle.Stopped: + messages.Add("Coder Connect stopped."); + break; + } + } + + 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()); + MaybeNotifyUser(model); } private void CredentialManager_CredentialsChanged(object? _, CredentialModel model) @@ -297,7 +352,7 @@ private void Window_Activated(object sender, WindowActivatedEventArgs e) } [RelayCommand] - private void Tray_Open() + public void Tray_Open() { MoveResizeAndActivate(); } 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