Skip to content

Commit 7e3782f

Browse files
authored
chore: add connect/disconnect notifications on Coder Connect (#140)
1 parent b8d7993 commit 7e3782f

File tree

3 files changed

+103
-9
lines changed

3 files changed

+103
-9
lines changed

App/App.xaml.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
namespace Coder.Desktop.App;
2929

30-
public partial class App : Application, IDispatcherQueueManager
30+
public partial class App : Application, IDispatcherQueueManager, INotificationHandler
3131
{
3232
private const string MutagenControllerConfigSection = "MutagenController";
3333
private const string UpdaterConfigSection = "Updater";
@@ -91,6 +91,7 @@ public App()
9191
services.AddSingleton<IAgentApiClientFactory, AgentApiClientFactory>();
9292

9393
services.AddSingleton<IDispatcherQueueManager>(_ => this);
94+
services.AddSingleton<INotificationHandler>(_ => this);
9495
services.AddSingleton<ICredentialBackend>(_ =>
9596
new WindowsCredentialBackend(WindowsCredentialBackend.CoderCredentialsTargetName));
9697
services.AddSingleton<ICredentialManager, CredentialManager>();
@@ -335,4 +336,13 @@ public void RunInUiThread(DispatcherQueueHandler action)
335336
}
336337
dispatcherQueue.TryEnqueue(action);
337338
}
339+
340+
public void HandleNotificationActivation(IDictionary<string, string> args)
341+
{
342+
var app = (App)Current;
343+
if (app != null && app.TrayWindow != null)
344+
{
345+
app.TrayWindow.Tray_Open();
346+
}
347+
}
338348
}

App/Services/UserNotifier.cs

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Collections.Generic;
44
using System.Threading;
55
using System.Threading.Tasks;
6+
using Coder.Desktop.App.Views;
67
using Microsoft.Extensions.Logging;
78
using Microsoft.Windows.AppNotifications;
89
using Microsoft.Windows.AppNotifications.Builder;
@@ -20,17 +21,40 @@ public interface IUserNotifier : INotificationHandler, IAsyncDisposable
2021
public void UnregisterHandler(string name);
2122

2223
public Task ShowErrorNotification(string title, string message, CancellationToken ct = default);
23-
public Task ShowActionNotification(string title, string message, string handlerName, IDictionary<string, string>? args = null, CancellationToken ct = default);
24+
/// <summary>
25+
/// This method allows to display a Windows-native notification with an action defined in
26+
/// <paramref name="handlerName"/> and provided <paramref name="args"/>.
27+
/// </summary>
28+
/// <param name="title">Title of the notification.</param>
29+
/// <param name="message">Message to be displayed in the notification body.</param>
30+
/// <param name="handlerName">Handler should be e.g. <c>nameof(Handler)</c> where <c>Handler</c>
31+
/// implements <see cref="Coder.Desktop.App.Services.INotificationHandler" />.
32+
/// If handler is <c>null</c> the action will open Coder Desktop.</param>
33+
/// <param name="args">Arguments to be provided to the handler when executing the action.</param>
34+
public Task ShowActionNotification(string title, string message, string? handlerName, IDictionary<string, string>? args = null, CancellationToken ct = default);
2435
}
2536

26-
public class UserNotifier(ILogger<UserNotifier> logger, IDispatcherQueueManager dispatcherQueueManager) : IUserNotifier
37+
public class UserNotifier : IUserNotifier
2738
{
2839
private const string CoderNotificationHandler = "CoderNotificationHandler";
40+
private const string DefaultNotificationHandler = "DefaultNotificationHandler";
2941

3042
private readonly AppNotificationManager _notificationManager = AppNotificationManager.Default;
43+
private readonly ILogger<UserNotifier> _logger;
44+
private readonly IDispatcherQueueManager _dispatcherQueueManager;
3145

3246
private ConcurrentDictionary<string, INotificationHandler> Handlers { get; } = new();
3347

48+
public UserNotifier(ILogger<UserNotifier> logger, IDispatcherQueueManager dispatcherQueueManager,
49+
INotificationHandler notificationHandler)
50+
{
51+
_logger = logger;
52+
_dispatcherQueueManager = dispatcherQueueManager;
53+
var defaultHandlerAdded = Handlers.TryAdd(DefaultNotificationHandler, notificationHandler);
54+
if (!defaultHandlerAdded)
55+
throw new Exception($"UserNotifier failed to be initialized with {nameof(DefaultNotificationHandler)}");
56+
}
57+
3458
public ValueTask DisposeAsync()
3559
{
3660
return ValueTask.CompletedTask;
@@ -50,6 +74,8 @@ public void RegisterHandler(string name, INotificationHandler handler)
5074

5175
public void UnregisterHandler(string name)
5276
{
77+
if (name == nameof(DefaultNotificationHandler))
78+
throw new InvalidOperationException($"You cannot remove '{name}'.");
5379
if (!Handlers.TryRemove(name, out _))
5480
throw new InvalidOperationException($"No handler with the name '{name}' is registered.");
5581
}
@@ -61,8 +87,11 @@ public Task ShowErrorNotification(string title, string message, CancellationToke
6187
return Task.CompletedTask;
6288
}
6389

64-
public Task ShowActionNotification(string title, string message, string handlerName, IDictionary<string, string>? args = null, CancellationToken ct = default)
90+
public Task ShowActionNotification(string title, string message, string? handlerName, IDictionary<string, string>? args = null, CancellationToken ct = default)
6591
{
92+
if (handlerName == null)
93+
handlerName = nameof(DefaultNotificationHandler); // Use default handler if no handler name is provided
94+
6695
if (!Handlers.TryGetValue(handlerName, out _))
6796
throw new InvalidOperationException($"No action handler with the name '{handlerName}' is registered.");
6897

@@ -90,19 +119,19 @@ public void HandleNotificationActivation(IDictionary<string, string> args)
90119

91120
if (!Handlers.TryGetValue(handlerName, out var handler))
92121
{
93-
logger.LogWarning("no action handler '{HandlerName}' found for notification activation, ignoring", handlerName);
122+
_logger.LogWarning("no action handler '{HandlerName}' found for notification activation, ignoring", handlerName);
94123
return;
95124
}
96125

97-
dispatcherQueueManager.RunInUiThread(() =>
126+
_dispatcherQueueManager.RunInUiThread(() =>
98127
{
99128
try
100129
{
101130
handler.HandleNotificationActivation(args);
102131
}
103132
catch (Exception ex)
104133
{
105-
logger.LogWarning(ex, "could not handle activation for notification with handler '{HandlerName}", handlerName);
134+
_logger.LogWarning(ex, "could not handle activation for notification with handler '{HandlerName}", handlerName);
106135
}
107136
});
108137
}

App/Views/TrayWindow.xaml.cs

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@
99
using Microsoft.UI.Windowing;
1010
using Microsoft.UI.Xaml;
1111
using Microsoft.UI.Xaml.Controls;
12+
using Microsoft.UI.Xaml.Documents;
1213
using Microsoft.UI.Xaml.Media.Animation;
1314
using System;
15+
using System.Collections.Generic;
1416
using System.Runtime.InteropServices;
17+
using System.Threading;
1518
using System.Threading.Tasks;
1619
using Windows.Graphics;
1720
using Windows.System;
@@ -33,17 +36,23 @@ public sealed partial class TrayWindow : Window
3336
private int _lastWindowHeight;
3437
private Storyboard? _currentSb;
3538

39+
private VpnLifecycle curVpnLifecycle = VpnLifecycle.Stopped;
40+
private RpcLifecycle curRpcLifecycle = RpcLifecycle.Disconnected;
41+
3642
private readonly IRpcController _rpcController;
3743
private readonly ICredentialManager _credentialManager;
3844
private readonly ISyncSessionController _syncSessionController;
3945
private readonly IUpdateController _updateController;
46+
private readonly IUserNotifier _userNotifier;
4047
private readonly TrayWindowLoadingPage _loadingPage;
4148
private readonly TrayWindowDisconnectedPage _disconnectedPage;
4249
private readonly TrayWindowLoginRequiredPage _loginRequiredPage;
4350
private readonly TrayWindowMainPage _mainPage;
4451

45-
public TrayWindow(IRpcController rpcController, ICredentialManager credentialManager,
52+
public TrayWindow(
53+
IRpcController rpcController, ICredentialManager credentialManager,
4654
ISyncSessionController syncSessionController, IUpdateController updateController,
55+
IUserNotifier userNotifier,
4756
TrayWindowLoadingPage loadingPage,
4857
TrayWindowDisconnectedPage disconnectedPage, TrayWindowLoginRequiredPage loginRequiredPage,
4958
TrayWindowMainPage mainPage)
@@ -52,6 +61,7 @@ public TrayWindow(IRpcController rpcController, ICredentialManager credentialMan
5261
_credentialManager = credentialManager;
5362
_syncSessionController = syncSessionController;
5463
_updateController = updateController;
64+
_userNotifier = userNotifier;
5565
_loadingPage = loadingPage;
5666
_disconnectedPage = disconnectedPage;
5767
_loginRequiredPage = loginRequiredPage;
@@ -142,9 +152,54 @@ private void SetPageByState(RpcModel rpcModel, CredentialModel credentialModel,
142152
}
143153
}
144154

155+
private void MaybeNotifyUser(RpcModel rpcModel)
156+
{
157+
// This method is called when the state changes, but we don't want to notify
158+
// the user if the state hasn't changed.
159+
var isRpcLifecycleChanged = rpcModel.RpcLifecycle == RpcLifecycle.Disconnected && curRpcLifecycle != rpcModel.RpcLifecycle;
160+
var isVpnLifecycleChanged = (rpcModel.VpnLifecycle == VpnLifecycle.Started || rpcModel.VpnLifecycle == VpnLifecycle.Stopped) && curVpnLifecycle != rpcModel.VpnLifecycle;
161+
162+
if (!isRpcLifecycleChanged && !isVpnLifecycleChanged)
163+
{
164+
return;
165+
}
166+
167+
var oldRpcLifeycle = curRpcLifecycle;
168+
var oldVpnLifecycle = curVpnLifecycle;
169+
curRpcLifecycle = rpcModel.RpcLifecycle;
170+
curVpnLifecycle = rpcModel.VpnLifecycle;
171+
172+
var messages = new List<string>();
173+
174+
if (oldRpcLifeycle != RpcLifecycle.Disconnected && curRpcLifecycle == RpcLifecycle.Disconnected)
175+
{
176+
messages.Add("Disconnected from Coder background service.");
177+
}
178+
179+
if (oldVpnLifecycle != curVpnLifecycle)
180+
{
181+
switch (curVpnLifecycle)
182+
{
183+
case VpnLifecycle.Started:
184+
messages.Add("Coder Connect started.");
185+
break;
186+
case VpnLifecycle.Stopped:
187+
messages.Add("Coder Connect stopped.");
188+
break;
189+
}
190+
}
191+
192+
if (messages.Count == 0) return;
193+
if (_aw.IsVisible) return;
194+
195+
var message = string.Join(" ", messages);
196+
_userNotifier.ShowActionNotification(message, string.Empty, null, null, CancellationToken.None);
197+
}
198+
145199
private void RpcController_StateChanged(object? _, RpcModel model)
146200
{
147201
SetPageByState(model, _credentialManager.GetCachedCredentials(), _syncSessionController.GetState());
202+
MaybeNotifyUser(model);
148203
}
149204

150205
private void CredentialManager_CredentialsChanged(object? _, CredentialModel model)
@@ -297,7 +352,7 @@ private void Window_Activated(object sender, WindowActivatedEventArgs e)
297352
}
298353

299354
[RelayCommand]
300-
private void Tray_Open()
355+
public void Tray_Open()
301356
{
302357
MoveResizeAndActivate();
303358
}

0 commit comments

Comments
 (0)
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