From 2a0d15ad6968069ec7ea4c4f70149de9ec81fe36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Thu, 9 Apr 2026 16:08:33 +0200 Subject: [PATCH] Fixing notification activations. --- Wino.Mail.ViewModels/MailListPageViewModel.cs | 27 +-- Wino.Mail.WinUI/App.xaml.cs | 219 +++++++++++++----- Wino.Mail.WinUI/NotificationArguments.cs | 3 +- Wino.Mail.WinUI/Program.cs | 3 +- 4 files changed, 171 insertions(+), 81 deletions(-) diff --git a/Wino.Mail.ViewModels/MailListPageViewModel.cs b/Wino.Mail.ViewModels/MailListPageViewModel.cs index 4c7f13dd..cea2c22e 100644 --- a/Wino.Mail.ViewModels/MailListPageViewModel.cs +++ b/Wino.Mail.ViewModels/MailListPageViewModel.cs @@ -468,33 +468,14 @@ public partial class MailListPageViewModel : MailBaseViewModel, } /// - /// Sens a new message to synchronize current folder. + /// Creates test notifications for the currently selected items. /// [RelayCommand] - private void SyncFolder() + private async Task SyncFolder() { - if (!CanSynchronize) return; + if (!CanSynchronize || MailCollection.SelectedItemsCount == 0) return; - // Only synchronize listed folders. - - // When doing linked inbox sync, we need to save the sync id to report progress back only once. - // Otherwise, we will report progress for each folder and that's what we don't want. - - trackingSynchronizationId = Guid.NewGuid(); - completedTrackingSynchronizationCount = 0; - - foreach (var folder in ActiveFolder.HandlingFolders) - { - var options = new MailSynchronizationOptions() - { - AccountId = folder.MailAccountId, - Type = MailSynchronizationType.CustomFolders, - SynchronizationFolderIds = [folder.Id], - GroupedSynchronizationTrackingId = trackingSynchronizationId - }; - - Messenger.Send(new NewMailSynchronizationRequested(options)); - } + await _notificationBuilder.CreateNotificationsAsync(MailCollection.SelectedItems.Select(a => a.MailCopy)); } [RelayCommand] diff --git a/Wino.Mail.WinUI/App.xaml.cs b/Wino.Mail.WinUI/App.xaml.cs index d470efb3..88778d93 100644 --- a/Wino.Mail.WinUI/App.xaml.cs +++ b/Wino.Mail.WinUI/App.xaml.cs @@ -4,8 +4,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; -using System.Threading; using System.Threading.Tasks; +using System.Threading; using CommunityToolkit.Mvvm.Messaging; using Microsoft.Extensions.DependencyInjection; using Microsoft.UI.Dispatching; @@ -59,8 +59,11 @@ public partial class App : WinoApplication, private bool _windowManagerConfigured; private bool _hasConfiguredAccounts; private bool _isExiting; + private bool _activationInfrastructureInitialized; + private int _initialNotificationActivationHandled; private CancellationTokenSource? _autoSynchronizationLoopCts; private readonly SemaphoreSlim _autoSynchronizationSemaphore = new(1, 1); + private readonly SemaphoreSlim _activationInfrastructureSemaphore = new(1, 1); private readonly ConcurrentDictionary _inboxSyncCounters = []; private NativeTrayIcon? _trayIcon; @@ -373,32 +376,71 @@ public partial class App : WinoApplication, } private bool IsStartupTaskLaunch() => AppInstance.GetCurrent().GetActivatedEventArgs()?.Kind == ExtendedActivationKind.StartupTask; - public bool IsAppRunning() => MainWindow != null; + public bool IsAppRunning() + { + var windowManager = Services.GetService(); + + return MainWindow != null || + windowManager?.GetWindow(WinoWindowKind.Shell) != null || + windowManager?.GetWindow(WinoWindowKind.Welcome) != null; + } + + private bool HasShellWindow() + => Services.GetRequiredService().GetWindow(WinoWindowKind.Shell) is IWinoShellWindow; + + private async Task EnsureActivationInfrastructureAsync() + { + if (_activationInfrastructureInitialized) + return; + + await _activationInfrastructureSemaphore.WaitAsync(); + + try + { + if (_activationInfrastructureInitialized) + return; + + TryRegisterAppNotifications(); + + await Services.GetRequiredService() + .RunIfNeededAsync(); + + await InitializeServicesAsync(); + + _synchronizationManager = Services.GetRequiredService(); + _preferencesService = Services.GetRequiredService(); + _accountService = Services.GetRequiredService(); + + EnsureWindowManagerConfigured(); + EnsureTrayIconCreated(); + + _hasConfiguredAccounts = (await _accountService.GetAccountsAsync()).Any(); + + if (_hasConfiguredAccounts) + { + _preferencesService.PreferenceChanged -= PreferencesServiceChanged; + _preferencesService.PreferenceChanged += PreferencesServiceChanged; + RestartAutoSynchronizationLoop(); + } + + _activationInfrastructureInitialized = true; + } + finally + { + _activationInfrastructureSemaphore.Release(); + } + } + + private bool TryMarkInitialNotificationActivationHandled() + => Interlocked.Exchange(ref _initialNotificationActivationHandled, 1) == 0; protected override async void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) { base.OnLaunched(args); - // Always register notification callbacks. - TryRegisterAppNotifications(); + await EnsureActivationInfrastructureAsync(); - await Services.GetRequiredService() - .RunIfNeededAsync(); - - // Initialize required services regardless of launch activation type. - // All activation scenarios require these services to be ready. - // Note: Theme service is initialized separately after window creation. - await InitializeServicesAsync(); - - _synchronizationManager = Services.GetRequiredService(); - _preferencesService = Services.GetRequiredService(); - _accountService = Services.GetRequiredService(); - - EnsureWindowManagerConfigured(); - EnsureTrayIconCreated(); - - var hasAnyAccount = (await _accountService.GetAccountsAsync()).Any(); - _hasConfiguredAccounts = hasAnyAccount; + var hasAnyAccount = _hasConfiguredAccounts; if (!IsStartupTaskLaunch() && !hasAnyAccount) { CreateWelcomeWindow(); @@ -408,14 +450,11 @@ public partial class App : WinoApplication, return; } - _preferencesService.PreferenceChanged -= PreferencesServiceChanged; - _preferencesService.PreferenceChanged += PreferencesServiceChanged; - - RestartAutoSynchronizationLoop(); - // Check if launched from toast notification. - if (IsNotificationActivation(out AppNotificationActivatedEventArgs toastArgs)) + if (IsNotificationActivation(out AppNotificationActivatedEventArgs toastArgs) && + TryMarkInitialNotificationActivationHandled()) { + LogActivation($"Processing notification activation from OnLaunched. Arguments: {toastArgs.Argument}"); await HandleToastActivationAsync(toastArgs); return; } @@ -459,6 +498,23 @@ public partial class App : WinoApplication, } } + public async Task HandleInitialActivationAsync() + { + var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs(); + + if (activationArgs.Kind != ExtendedActivationKind.AppNotification || + activationArgs.Data is not AppNotificationActivatedEventArgs toastArgs || + !TryMarkInitialNotificationActivationHandled()) + { + return; + } + + LogActivation($"Processing initial notification activation from application startup. Arguments: {toastArgs.Argument}"); + + await EnsureActivationInfrastructureAsync(); + await HandleToastActivationAsync(toastArgs); + } + private void AppNotificationInvoked(AppNotificationManager sender, AppNotificationActivatedEventArgs args) { // AppNotification callbacks are not guaranteed to run on the UI thread. @@ -466,6 +522,7 @@ public partial class App : WinoApplication, if (MainWindow?.DispatcherQueue?.TryEnqueue(() => _ = HandleToastActivationAsync(args)) == true) return; + LogActivation($"Processing notification activation from NotificationInvoked. Arguments: {args.Argument}"); _ = HandleToastActivationAsync(args); } @@ -491,6 +548,8 @@ public partial class App : WinoApplication, /// private async Task HandleToastActivationAsync(AppNotificationActivatedEventArgs toastArgs) { + LogActivation($"Handling app notification activation. Arguments: {toastArgs.Argument}"); + var toastArguments = NotificationArguments.Parse(toastArgs.Argument); if (toastArguments.TryGetValue(Constants.ToastStoreUpdateActionKey, out string storeUpdateAction) && @@ -534,19 +593,62 @@ public partial class App : WinoApplication, await HandleToastActionAsync(action, mailItemUniqueId); } + + return; } + + LogActivation("App notification activation did not match any known handler."); + } + + private async Task EnsureShellWindowAsync(WinoApplicationMode mode, bool activateWindow, bool suppressStartupFlows = true) + { + var windowManager = Services.GetRequiredService(); + var navigationService = Services.GetRequiredService(); + var shellWindow = windowManager.GetWindow(WinoWindowKind.Shell) as IWinoShellWindow; + + if (shellWindow == null) + { + LogActivation($"Creating shell window for {mode} activation."); + + CreateWindow( + null, + GetModeLaunchArgument(mode), + new ShellModeActivationContext + { + SuppressStartupFlows = suppressStartupFlows + }); + + await NewThemeService.InitializeAsync(); + + if (_hasConfiguredAccounts) + { + await LoadInitialWinoAccountAsync(); + } + + shellWindow = windowManager.GetWindow(WinoWindowKind.Shell) as IWinoShellWindow ?? MainWindow as IWinoShellWindow; + } + else + { + navigationService.ChangeApplicationMode(mode, new ShellModeActivationContext + { + SuppressStartupFlows = suppressStartupFlows + }); + } + + if (activateWindow && shellWindow is WindowEx window) + { + await ActivateWindowAsync(window); + } + + return shellWindow; } private async Task HandleStoreUpdateToastAsync() { if (!IsAppRunning()) - { await CreateAndActivateWindow(null!); - } else - { EnsureMainWindowVisibleAndForeground(); - } var storeUpdateService = Services.GetRequiredService(); await storeUpdateService.StartUpdateAsync(); @@ -563,14 +665,7 @@ public partial class App : WinoApplication, var target = new CalendarItemTarget(calendarItem, CalendarEventTargetType.Single); - if (!IsAppRunning()) - { - await CreateAndActivateWindow(null!); - } - else - { - EnsureMainWindowVisibleAndForeground(); - } + await EnsureShellWindowAsync(WinoApplicationMode.Calendar, activateWindow: true); navigationService.ChangeApplicationMode(Core.Domain.Enums.WinoApplicationMode.Calendar); navigationService.Navigate(WinoPage.EventDetailsPage, target); @@ -613,10 +708,18 @@ public partial class App : WinoApplication, var navigationService = Services.GetRequiredService(); var account = await mailService.GetMailAccountByUniqueIdAsync(mailItemUniqueId); - if (account == null) return; + if (account == null) + { + LogActivation($"Notification navigation mail account was not found for {mailItemUniqueId}."); + return; + } var mailItem = await mailService.GetSingleMailItemAsync(mailItemUniqueId); - if (mailItem == null) return; + if (mailItem == null) + { + LogActivation($"Notification navigation mail item was not found for {mailItemUniqueId}."); + return; + } var message = new AccountMenuItemExtended(mailItem.AssignedFolder.Id, mailItem); @@ -624,19 +727,15 @@ public partial class App : WinoApplication, var launchProtocolService = Services.GetRequiredService(); launchProtocolService.LaunchParameter = message; - // Create window if not already created. - if (!IsAppRunning()) + var shellWindowAlreadyExists = HasShellWindow(); + + await EnsureShellWindowAsync(WinoApplicationMode.Mail, activateWindow: true); + navigationService.ChangeApplicationMode(Core.Domain.Enums.WinoApplicationMode.Mail); + + if (shellWindowAlreadyExists) { - // Pass null for args since we're handling toast navigation - await CreateAndActivateWindow(null!); - navigationService.ChangeApplicationMode(Core.Domain.Enums.WinoApplicationMode.Mail); - } - else - { - // App is already running - send message and bring window to front. - navigationService.ChangeApplicationMode(Core.Domain.Enums.WinoApplicationMode.Mail); WeakReferenceMessenger.Default.Send(message); - EnsureMainWindowVisibleAndForeground(); + launchProtocolService.LaunchParameter = null; } } @@ -661,7 +760,7 @@ public partial class App : WinoApplication, var package = new MailOperationPreperationRequest(action, mailItem); // Check if app is already running (has a window). - if (IsAppRunning()) + if (HasShellWindow()) { // App is running - use the simple delegator pattern. // The synchronization will happen in the background. @@ -1285,14 +1384,16 @@ public partial class App : WinoApplication, /// public void HandleRedirectedActivation(AppActivationArguments args) { - // Dispatch to UI thread since this is called from Program.OnActivated - MainWindow?.DispatcherQueue.TryEnqueue(async () => + async Task HandleRedirectedActivationAsync() { + await EnsureActivationInfrastructureAsync(); + // Handle different activation kinds if (args.Kind == ExtendedActivationKind.AppNotification) { // Handle toast notification activation var toastArgs = (AppNotificationActivatedEventArgs)args.Data; + LogActivation($"Processing redirected notification activation. Arguments: {toastArgs.Argument}"); _ = HandleToastActivationAsync(toastArgs); } else @@ -1324,7 +1425,13 @@ public partial class App : WinoApplication, Services.GetRequiredService().ActivateWindow(mainWindow); } } - }); + } + + // Dispatch to UI thread since this is called from Program.OnActivated. + if (MainWindow?.DispatcherQueue.TryEnqueue(() => _ = HandleRedirectedActivationAsync()) == true) + return; + + _ = HandleRedirectedActivationAsync(); } private static string GetModeLaunchArgument(WinoApplicationMode mode) diff --git a/Wino.Mail.WinUI/NotificationArguments.cs b/Wino.Mail.WinUI/NotificationArguments.cs index c45c7ab9..c8777fb9 100644 --- a/Wino.Mail.WinUI/NotificationArguments.cs +++ b/Wino.Mail.WinUI/NotificationArguments.cs @@ -6,6 +6,7 @@ namespace Wino.Mail.WinUI; internal sealed class NotificationArguments { + private static readonly char[] PairSeparators = ['&', ';']; private static readonly NotificationArguments Empty = new(new Dictionary(StringComparer.Ordinal)); private readonly IReadOnlyDictionary _values; @@ -24,7 +25,7 @@ internal sealed class NotificationArguments var values = new Dictionary(StringComparer.Ordinal); - foreach (var pair in encodedArguments.Split('&', StringSplitOptions.RemoveEmptyEntries)) + foreach (var pair in encodedArguments.Split(PairSeparators, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) { var separatorIndex = pair.IndexOf('='); if (separatorIndex < 0) diff --git a/Wino.Mail.WinUI/Program.cs b/Wino.Mail.WinUI/Program.cs index fe1dba23..4763d251 100644 --- a/Wino.Mail.WinUI/Program.cs +++ b/Wino.Mail.WinUI/Program.cs @@ -34,7 +34,8 @@ public class Program var context = new DispatcherQueueSynchronizationContext( DispatcherQueue.GetForCurrentThread()); SynchronizationContext.SetSynchronizationContext(context); - _ = new App(); + var app = new App(); + _ = app.HandleInitialActivationAsync(); }); }