11
11
using Microsoft . UI . Xaml . Controls ;
12
12
using Microsoft . UI . Xaml . Media . Animation ;
13
13
using System ;
14
+ using System . Collections . Generic ;
14
15
using System . Runtime . InteropServices ;
16
+ using System . Threading ;
15
17
using System . Threading . Tasks ;
16
18
using Windows . Graphics ;
17
19
using Windows . System ;
21
23
22
24
namespace Coder . Desktop . App . Views ;
23
25
24
- public sealed partial class TrayWindow : Window
26
+ public sealed partial class TrayWindow : Window , INotificationHandler
25
27
{
26
28
private const int WIDTH = 300 ;
27
29
@@ -33,17 +35,25 @@ public sealed partial class TrayWindow : Window
33
35
private int _lastWindowHeight ;
34
36
private Storyboard ? _currentSb ;
35
37
38
+ private VpnLifecycle prevVpnLifecycle = VpnLifecycle . Stopped ;
39
+ private RpcLifecycle prevRpcLifecycle = RpcLifecycle . Disconnected ;
40
+
41
+ private NativeApi . POINT ? _lastActivatePosition ;
42
+
36
43
private readonly IRpcController _rpcController ;
37
44
private readonly ICredentialManager _credentialManager ;
38
45
private readonly ISyncSessionController _syncSessionController ;
39
46
private readonly IUpdateController _updateController ;
47
+ private readonly IUserNotifier _userNotifier ;
40
48
private readonly TrayWindowLoadingPage _loadingPage ;
41
49
private readonly TrayWindowDisconnectedPage _disconnectedPage ;
42
50
private readonly TrayWindowLoginRequiredPage _loginRequiredPage ;
43
51
private readonly TrayWindowMainPage _mainPage ;
44
52
45
- public TrayWindow ( IRpcController rpcController , ICredentialManager credentialManager ,
53
+ public TrayWindow (
54
+ IRpcController rpcController , ICredentialManager credentialManager ,
46
55
ISyncSessionController syncSessionController , IUpdateController updateController ,
56
+ IUserNotifier userNotifier ,
47
57
TrayWindowLoadingPage loadingPage ,
48
58
TrayWindowDisconnectedPage disconnectedPage , TrayWindowLoginRequiredPage loginRequiredPage ,
49
59
TrayWindowMainPage mainPage )
@@ -52,12 +62,14 @@ public TrayWindow(IRpcController rpcController, ICredentialManager credentialMan
52
62
_credentialManager = credentialManager ;
53
63
_syncSessionController = syncSessionController ;
54
64
_updateController = updateController ;
65
+ _userNotifier = userNotifier ;
55
66
_loadingPage = loadingPage ;
56
67
_disconnectedPage = disconnectedPage ;
57
68
_loginRequiredPage = loginRequiredPage ;
58
69
_mainPage = mainPage ;
59
70
60
71
InitializeComponent ( ) ;
72
+ _userNotifier . RegisterHandler ( "TrayWindow" , this ) ;
61
73
AppWindow . Hide ( ) ;
62
74
Activated += Window_Activated ;
63
75
RootFrame . SizeChanged += RootFrame_SizeChanged ;
@@ -142,9 +154,55 @@ private void SetPageByState(RpcModel rpcModel, CredentialModel credentialModel,
142
154
}
143
155
}
144
156
157
+ private void NotifyUser ( RpcModel rpcModel )
158
+ {
159
+ // This method is called when the state changes, but we don't want to notify
160
+ // the user if the state hasn't changed.
161
+ var isRpcLifecycleChanged = rpcModel . RpcLifecycle != RpcLifecycle . Connecting && prevRpcLifecycle != rpcModel . RpcLifecycle ;
162
+ var isVpnLifecycleChanged = ( rpcModel . VpnLifecycle == VpnLifecycle . Started || rpcModel . VpnLifecycle == VpnLifecycle . Stopped ) && prevVpnLifecycle != rpcModel . VpnLifecycle ;
163
+
164
+ if ( ! isRpcLifecycleChanged && ! isVpnLifecycleChanged )
165
+ {
166
+ return ;
167
+ }
168
+ var message = string . Empty ;
169
+ // Compose the message based on the lifecycle changes
170
+ if ( isRpcLifecycleChanged )
171
+ message += rpcModel . RpcLifecycle switch
172
+ {
173
+ RpcLifecycle . Connected => "Connected to Coder vpn service." ,
174
+ RpcLifecycle . Disconnected => "Disconnected from Coder vpn service." ,
175
+ _=> "" // This will never be hit.
176
+ } ;
177
+
178
+ if ( message . Length > 0 && isVpnLifecycleChanged )
179
+ message += " " ;
180
+
181
+ if ( isVpnLifecycleChanged )
182
+ message += rpcModel . VpnLifecycle switch
183
+ {
184
+ VpnLifecycle . Started => "Coder Connect started." ,
185
+ VpnLifecycle . Stopped => "Coder Connect stopped." ,
186
+ _=> "" // This will never be hit.
187
+ } ;
188
+
189
+ // Save state for the next notification check
190
+ prevRpcLifecycle = rpcModel . RpcLifecycle ;
191
+ prevVpnLifecycle = rpcModel . VpnLifecycle ;
192
+
193
+ if ( _aw . IsVisible )
194
+ {
195
+ return ; // No need to notify if the window is not visible.
196
+ }
197
+
198
+ // Trigger notification
199
+ _userNotifier . ShowActionNotification ( message , string . Empty , nameof ( TrayWindow ) , null , CancellationToken . None ) ;
200
+ }
201
+
145
202
private void RpcController_StateChanged ( object ? _ , RpcModel model )
146
203
{
147
204
SetPageByState ( model , _credentialManager . GetCachedCredentials ( ) , _syncSessionController . GetState ( ) ) ;
205
+ NotifyUser ( model ) ;
148
206
}
149
207
150
208
private void CredentialManager_CredentialsChanged ( object ? _ , CredentialModel model )
@@ -316,6 +374,11 @@ private void Tray_Exit()
316
374
_ = ( ( App ) Application . Current ) . ExitApplication ( ) ;
317
375
}
318
376
377
+ public void HandleNotificationActivation ( IDictionary < string , string > args )
378
+ {
379
+ Tray_Open ( ) ;
380
+ }
381
+
319
382
public static class NativeApi
320
383
{
321
384
[ DllImport ( "dwmapi.dll" ) ]
@@ -336,7 +399,7 @@ internal enum TaskbarPosition { Left, Top, Right, Bottom }
336
399
internal readonly record struct TaskbarInfo ( TaskbarPosition Position , int Gap , bool AutoHide ) ;
337
400
338
401
// -----------------------------------------------------------------------------
339
- // Taskbar helpers– ABM_GETTASKBARPOS / ABM_GETSTATE via SHAppBarMessage
402
+ // Taskbar helpers� ABM_GETTASKBARPOS / ABM_GETSTATE via SHAppBarMessage
340
403
// -----------------------------------------------------------------------------
341
404
private static TaskbarInfo GetTaskbarInfo ( DisplayArea area )
342
405
{