1111using Microsoft . UI . Xaml . Controls ;
1212using Microsoft . UI . Xaml . Media . Animation ;
1313using System ;
14+ using System . Collections . Generic ;
1415using System . Runtime . InteropServices ;
16+ using System . Threading ;
1517using System . Threading . Tasks ;
1618using Windows . Graphics ;
1719using Windows . System ;
2123
2224namespace Coder . Desktop . App . Views ;
2325
24- public sealed partial class TrayWindow : Window
26+ public sealed partial class TrayWindow : Window , INotificationHandler
2527{
2628 private const int WIDTH = 300 ;
2729
@@ -33,17 +35,25 @@ public sealed partial class TrayWindow : Window
3335 private int _lastWindowHeight ;
3436 private Storyboard ? _currentSb ;
3537
38+ private VpnLifecycle prevVpnLifecycle = VpnLifecycle . Stopped ;
39+ private RpcLifecycle prevRpcLifecycle = RpcLifecycle . Disconnected ;
40+
41+ private NativeApi . POINT ? _lastActivatePosition ;
42+
3643 private readonly IRpcController _rpcController ;
3744 private readonly ICredentialManager _credentialManager ;
3845 private readonly ISyncSessionController _syncSessionController ;
3946 private readonly IUpdateController _updateController ;
47+ private readonly IUserNotifier _userNotifier ;
4048 private readonly TrayWindowLoadingPage _loadingPage ;
4149 private readonly TrayWindowDisconnectedPage _disconnectedPage ;
4250 private readonly TrayWindowLoginRequiredPage _loginRequiredPage ;
4351 private readonly TrayWindowMainPage _mainPage ;
4452
45- public TrayWindow ( IRpcController rpcController , ICredentialManager credentialManager ,
53+ public TrayWindow (
54+ IRpcController rpcController , ICredentialManager credentialManager ,
4655 ISyncSessionController syncSessionController , IUpdateController updateController ,
56+ IUserNotifier userNotifier ,
4757 TrayWindowLoadingPage loadingPage ,
4858 TrayWindowDisconnectedPage disconnectedPage , TrayWindowLoginRequiredPage loginRequiredPage ,
4959 TrayWindowMainPage mainPage )
@@ -52,12 +62,14 @@ public TrayWindow(IRpcController rpcController, ICredentialManager credentialMan
5262 _credentialManager = credentialManager ;
5363 _syncSessionController = syncSessionController ;
5464 _updateController = updateController ;
65+ _userNotifier = userNotifier ;
5566 _loadingPage = loadingPage ;
5667 _disconnectedPage = disconnectedPage ;
5768 _loginRequiredPage = loginRequiredPage ;
5869 _mainPage = mainPage ;
5970
6071 InitializeComponent ( ) ;
72+ _userNotifier . RegisterHandler ( "TrayWindow" , this ) ;
6173 AppWindow . Hide ( ) ;
6274 Activated += Window_Activated ;
6375 RootFrame . SizeChanged += RootFrame_SizeChanged ;
@@ -142,9 +154,55 @@ private void SetPageByState(RpcModel rpcModel, CredentialModel credentialModel,
142154 }
143155 }
144156
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+
145202 private void RpcController_StateChanged ( object ? _ , RpcModel model )
146203 {
147204 SetPageByState ( model , _credentialManager . GetCachedCredentials ( ) , _syncSessionController . GetState ( ) ) ;
205+ NotifyUser ( model ) ;
148206 }
149207
150208 private void CredentialManager_CredentialsChanged ( object ? _ , CredentialModel model )
@@ -316,6 +374,11 @@ private void Tray_Exit()
316374 _ = ( ( App ) Application . Current ) . ExitApplication ( ) ;
317375 }
318376
377+ public void HandleNotificationActivation ( IDictionary < string , string > args )
378+ {
379+ Tray_Open ( ) ;
380+ }
381+
319382 public static class NativeApi
320383 {
321384 [ DllImport ( "dwmapi.dll" ) ]
@@ -336,7 +399,7 @@ internal enum TaskbarPosition { Left, Top, Right, Bottom }
336399 internal readonly record struct TaskbarInfo ( TaskbarPosition Position , int Gap , bool AutoHide ) ;
337400
338401 // -----------------------------------------------------------------------------
339- // Taskbar helpers – ABM_GETTASKBARPOS / ABM_GETSTATE via SHAppBarMessage
402+ // Taskbar helpers � ABM_GETTASKBARPOS / ABM_GETSTATE via SHAppBarMessage
340403 // -----------------------------------------------------------------------------
341404 private static TaskbarInfo GetTaskbarInfo ( DisplayArea area )
342405 {
0 commit comments