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)