diff --git a/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs b/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs index 43cf07b7..15c6931d 100644 --- a/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs +++ b/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs @@ -169,6 +169,7 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel, var activationContext = parameters as ShellModeActivationContext; var shouldRunStartupFlows = activationContext?.IsInitialActivation ?? true; + var navigationArgs = activationContext?.Parameter as CalendarPageNavigationArgs; PreferencesService.PreferenceChanged -= PreferencesServiceChanged; PreferencesService.PreferenceChanged += PreferencesServiceChanged; @@ -178,7 +179,14 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel, await InitializeAccountCalendarsAsync(); ValidateConfiguredNewEventCalendar(); - TodayClicked(); + if (navigationArgs != null) + { + NavigationService.Navigate(WinoPage.CalendarPage, navigationArgs); + } + else if (shouldRunStartupFlows || _calendarPageViewModel.CurrentVisibleRange == null) + { + TodayClicked(); + } } public override void OnNavigatedFrom(NavigationMode mode, object parameters) diff --git a/Wino.Calendar.ViewModels/CalendarPageViewModel.cs b/Wino.Calendar.ViewModels/CalendarPageViewModel.cs index 99664814..63aab5d2 100644 --- a/Wino.Calendar.ViewModels/CalendarPageViewModel.cs +++ b/Wino.Calendar.ViewModels/CalendarPageViewModel.cs @@ -634,7 +634,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, } } - public async Task ApplyDisplayRequestAsync(CalendarDisplayRequest request, bool forceReload = false) + public async Task ApplyDisplayRequestAsync(CalendarDisplayRequest request, bool forceReload = false, CalendarItemTarget pendingTarget = null) { var lifetimeVersion = CurrentPageLifetimeVersion; var hasLoadingLock = await WaitForCalendarLoadingLockAsync(lifetimeVersion).ConfigureAwait(false); @@ -718,6 +718,11 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, await _notificationBuilder.ClearCalendarTaskbarBadgeAsync().ConfigureAwait(false); _isCalendarBadgeClearedForPageLifetime = true; } + + if (loadSucceeded && pendingTarget != null && IsPageActive(lifetimeVersion)) + { + await NavigateToPendingCalendarTargetAsync(pendingTarget).ConfigureAwait(false); + } } public Task ReloadCurrentVisibleRangeAsync() @@ -745,6 +750,31 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, NavigateEvent(new CalendarItemViewModel(calendarItem), CalendarEventTargetType.Single); } + private async Task NavigateToPendingCalendarTargetAsync(CalendarItemTarget target) + { + CalendarItemViewModel calendarItemViewModel = null; + + if (_loadedCalendarItems.TryGetValue(target.Item.Id, out var loadedCalendarItemViewModel)) + { + calendarItemViewModel = loadedCalendarItemViewModel; + } + else + { + var targetItem = await _calendarService.GetCalendarItemTargetAsync(target).ConfigureAwait(false); + if (targetItem == null) + return; + + targetItem.AssignedCalendar ??= AccountCalendarStateService.ActiveCalendars.FirstOrDefault(calendar => calendar.Id == targetItem.CalendarId); + calendarItemViewModel = new CalendarItemViewModel(targetItem); + } + + await ExecuteUIThread(() => + { + DisplayDetailsCalendarItemViewModel = calendarItemViewModel; + NavigateEvent(calendarItemViewModel, target.TargetType); + }).ConfigureAwait(false); + } + private async Task> LoadCalendarItemsAsync(DateRange loadedDateWindow, long lifetimeVersion) { var loadedItems = new Dictionary(); @@ -819,7 +849,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, } public async void Receive(LoadCalendarMessage message) - => await ApplyDisplayRequestAsync(message.DisplayRequest, message.ForceReload); + => await ApplyDisplayRequestAsync(message.DisplayRequest, message.ForceReload, message.PendingTarget); public void Receive(CalendarSettingsUpdatedMessage message) { diff --git a/Wino.Core.Domain/Models/Calendar/CalendarPageNavigationArgs.cs b/Wino.Core.Domain/Models/Calendar/CalendarPageNavigationArgs.cs index 6e1c0ba7..40ff31b5 100644 --- a/Wino.Core.Domain/Models/Calendar/CalendarPageNavigationArgs.cs +++ b/Wino.Core.Domain/Models/Calendar/CalendarPageNavigationArgs.cs @@ -1,4 +1,5 @@ -using System; +#nullable enable +using System; namespace Wino.Core.Domain.Models.Calendar; @@ -18,4 +19,9 @@ public class CalendarPageNavigationArgs /// Force reloading the calendar data even when the target range does not change. /// public bool ForceReload { get; set; } + + /// + /// Optional event target to navigate to after the calendar page loads the requested range. + /// + public CalendarItemTarget? PendingTarget { get; set; } } diff --git a/Wino.Mail.ViewModels/MailListPageViewModel.cs b/Wino.Mail.ViewModels/MailListPageViewModel.cs index 2631bbc8..add078b1 100644 --- a/Wino.Mail.ViewModels/MailListPageViewModel.cs +++ b/Wino.Mail.ViewModels/MailListPageViewModel.cs @@ -23,7 +23,6 @@ using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.Menus; using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Reader; -using Wino.Core.Domain.Models.Synchronization; using Wino.Core.Requests.Mail; using Wino.Core.Services; using Wino.Mail.ViewModels.Collections; @@ -486,26 +485,29 @@ public partial class MailListPageViewModel : MailBaseViewModel, { if (!CanSynchronize) return; + _notificationBuilder.CreateNotificationsAsync(MailCollection.SelectedItems.Select(a => a.MailCopy)); + 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; + //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 - }; + //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)); - } + // Messenger.Send(new NewMailSynchronizationRequested(options)); + //} } [RelayCommand] diff --git a/Wino.Mail.WinUI/Activation/AppNotificationActivationBuffer.cs b/Wino.Mail.WinUI/Activation/AppNotificationActivationBuffer.cs new file mode 100644 index 00000000..e41688d1 --- /dev/null +++ b/Wino.Mail.WinUI/Activation/AppNotificationActivationBuffer.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Windows.AppNotifications; + +namespace Wino.Mail.WinUI.Activation; + +internal sealed record BufferedAppNotificationActivation(string Argument, IReadOnlyDictionary? UserInput); + +internal sealed class AppNotificationActivationBuffer +{ + private readonly ConcurrentQueue _pendingActivations = new(); + private readonly SemaphoreSlim _pendingSignal = new(0); + + public void Enqueue(AppNotificationActivatedEventArgs args) + { + var copiedUserInput = args.UserInput == null + ? null + : new Dictionary(args.UserInput, StringComparer.Ordinal); + + _pendingActivations.Enqueue(new BufferedAppNotificationActivation(args.Argument, copiedUserInput)); + _pendingSignal.Release(); + } + + public bool TryDequeue(out BufferedAppNotificationActivation activation) + => _pendingActivations.TryDequeue(out activation!); + + public async Task WaitAsync(TimeSpan timeout, CancellationToken cancellationToken = default) + { + if (!_pendingActivations.IsEmpty && TryDequeue(out var queuedActivation)) + return queuedActivation; + + if (!await _pendingSignal.WaitAsync(timeout, cancellationToken)) + return null; + + return TryDequeue(out var activation) ? activation : null; + } +} diff --git a/Wino.Mail.WinUI/Activation/CalendarEntryBootstrapActivation.cs b/Wino.Mail.WinUI/Activation/CalendarEntryBootstrapActivation.cs new file mode 100644 index 00000000..2cdcb4b7 --- /dev/null +++ b/Wino.Mail.WinUI/Activation/CalendarEntryBootstrapActivation.cs @@ -0,0 +1,207 @@ +using System; +using System.IO; +using System.Linq; +using Microsoft.Windows.AppLifecycle; +using Windows.ApplicationModel; +using Windows.ApplicationModel.Activation; +using Windows.Storage; +using Wino.Core.Domain.Enums; + +namespace Wino.Mail.WinUI.Activation; + +internal enum PendingBootstrapActivationKind +{ + Launch, + Protocol, + File +} + +internal sealed class PendingBootstrapActivation +{ + public PendingBootstrapActivationKind Kind { get; init; } + public WinoApplicationMode Mode { get; init; } = WinoApplicationMode.Mail; + public string? LaunchArguments { get; init; } + public string? TileId { get; init; } + public string? ProtocolUri { get; init; } + public string[] FilePaths { get; init; } = []; + public DateTimeOffset CreatedAtUtc { get; init; } = DateTimeOffset.UtcNow; +} + +internal static class CalendarEntryBootstrapActivation +{ + private const string PendingActivationKey = "PendingCalendarEntryBootstrapActivation"; + private const string KindKey = "Kind"; + private const string ModeKey = "Mode"; + private const string LaunchArgumentsKey = "LaunchArguments"; + private const string TileIdKey = "TileId"; + private const string ProtocolUriKey = "ProtocolUri"; + private const string FilePathsKey = "FilePaths"; + private const string CreatedAtUtcKey = "CreatedAtUtc"; + private static readonly TimeSpan PendingActivationLifetime = TimeSpan.FromMinutes(1); + + public static bool ShouldBootstrapToMailHost(AppActivationArguments activationArgs) + => TryCreatePendingActivation(activationArgs, out _); + + public static bool QueuePendingActivation(AppActivationArguments activationArgs) + { + if (!TryCreatePendingActivation(activationArgs, out var pendingActivation)) + return false; + + ApplicationData.Current.LocalSettings.Values[PendingActivationKey] = CreateCompositeValue(pendingActivation!); + return true; + } + + public static void ClearPendingActivation() + => ApplicationData.Current.LocalSettings.Values.Remove(PendingActivationKey); + + public static PendingBootstrapActivation? ConsumePendingActivation() + { + if (!ApplicationData.Current.LocalSettings.Values.TryGetValue(PendingActivationKey, out var pendingActivationValue) || + pendingActivationValue is not ApplicationDataCompositeValue compositeValue) + { + return null; + } + + ClearPendingActivation(); + + try + { + var pendingActivation = ParseCompositeValue(compositeValue); + if (pendingActivation == null) + return null; + + if (DateTimeOffset.UtcNow - pendingActivation.CreatedAtUtc > PendingActivationLifetime) + return null; + + return pendingActivation; + } + catch + { + return null; + } + } + + private static ApplicationDataCompositeValue CreateCompositeValue(PendingBootstrapActivation pendingActivation) + { + var compositeValue = new ApplicationDataCompositeValue + { + [KindKey] = pendingActivation.Kind.ToString(), + [ModeKey] = pendingActivation.Mode.ToString(), + [LaunchArgumentsKey] = pendingActivation.LaunchArguments ?? string.Empty, + [TileIdKey] = pendingActivation.TileId ?? string.Empty, + [ProtocolUriKey] = pendingActivation.ProtocolUri ?? string.Empty, + [FilePathsKey] = string.Join("\n", pendingActivation.FilePaths), + [CreatedAtUtcKey] = pendingActivation.CreatedAtUtc.ToString("o") + }; + + return compositeValue; + } + + private static PendingBootstrapActivation? ParseCompositeValue(ApplicationDataCompositeValue compositeValue) + { + if (!Enum.TryParse(compositeValue[KindKey]?.ToString(), ignoreCase: true, out PendingBootstrapActivationKind kind) || + !Enum.TryParse(compositeValue[ModeKey]?.ToString(), ignoreCase: true, out WinoApplicationMode mode) || + !DateTimeOffset.TryParse(compositeValue[CreatedAtUtcKey]?.ToString(), out var createdAtUtc)) + { + return null; + } + + return new PendingBootstrapActivation + { + Kind = kind, + Mode = mode, + LaunchArguments = GetOptionalCompositeString(compositeValue, LaunchArgumentsKey), + TileId = GetOptionalCompositeString(compositeValue, TileIdKey), + ProtocolUri = GetOptionalCompositeString(compositeValue, ProtocolUriKey), + FilePaths = GetOptionalCompositeString(compositeValue, FilePathsKey)? + .Split(['\n'], StringSplitOptions.RemoveEmptyEntries) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray() ?? [], + CreatedAtUtc = createdAtUtc + }; + } + + private static string? GetOptionalCompositeString(ApplicationDataCompositeValue compositeValue, string key) + { + if (!compositeValue.TryGetValue(key, out var value)) + return null; + + var stringValue = value?.ToString(); + return string.IsNullOrWhiteSpace(stringValue) ? null : stringValue; + } + + public static bool LaunchMailHost() + { + var mailAppUserModelId = AppEntryConstants.GetAppUserModelId(WinoApplicationMode.Mail); + var appEntries = Package.Current.GetAppListEntriesAsync().AsTask().GetAwaiter().GetResult(); + var mailEntry = appEntries.FirstOrDefault(entry => + string.Equals(entry.AppUserModelId, mailAppUserModelId, StringComparison.OrdinalIgnoreCase)); + + return mailEntry != null && mailEntry.LaunchAsync().AsTask().GetAwaiter().GetResult(); + } + + private static bool TryCreatePendingActivation(AppActivationArguments activationArgs, out PendingBootstrapActivation? pendingActivation) + { + pendingActivation = null; + + if (activationArgs.Kind == ExtendedActivationKind.Launch && + activationArgs.Data is ILaunchActivatedEventArgs launchArgs) + { + var resolvedMode = AppModeActivationResolver.Resolve(launchArgs.Arguments, launchArgs.TileId, Environment.CommandLine); + if (resolvedMode != WinoApplicationMode.Calendar) + return false; + + pendingActivation = new PendingBootstrapActivation + { + Kind = PendingBootstrapActivationKind.Launch, + Mode = resolvedMode, + LaunchArguments = launchArgs.Arguments, + TileId = launchArgs.TileId + }; + + return true; + } + + if (activationArgs.Kind == ExtendedActivationKind.Protocol && + activationArgs.Data is IProtocolActivatedEventArgs protocolArgs && + protocolArgs.Uri != null && + (string.Equals(protocolArgs.Uri.Scheme, "webcal", StringComparison.OrdinalIgnoreCase) || + string.Equals(protocolArgs.Uri.Scheme, "webcals", StringComparison.OrdinalIgnoreCase))) + { + pendingActivation = new PendingBootstrapActivation + { + Kind = PendingBootstrapActivationKind.Protocol, + Mode = WinoApplicationMode.Calendar, + ProtocolUri = protocolArgs.Uri.AbsoluteUri + }; + + return true; + } + + if (activationArgs.Kind == ExtendedActivationKind.File && + activationArgs.Data is IFileActivatedEventArgs fileArgs) + { + var filePaths = fileArgs.Files? + .OfType() + .Where(item => string.Equals(Path.GetExtension(item.Path), ".ics", StringComparison.OrdinalIgnoreCase)) + .Select(item => item.Path) + .Where(path => !string.IsNullOrWhiteSpace(path)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + + if (filePaths == null || filePaths.Length == 0) + return false; + + pendingActivation = new PendingBootstrapActivation + { + Kind = PendingBootstrapActivationKind.File, + Mode = WinoApplicationMode.Calendar, + FilePaths = filePaths + }; + + return true; + } + + return false; + } +} diff --git a/Wino.Mail.WinUI/App.xaml.cs b/Wino.Mail.WinUI/App.xaml.cs index 893990b5..9cf6ec5b 100644 --- a/Wino.Mail.WinUI/App.xaml.cs +++ b/Wino.Mail.WinUI/App.xaml.cs @@ -36,6 +36,7 @@ using Wino.Mail.ViewModels; using Wino.Mail.ViewModels.Data; using Wino.Mail.WinUI.Activation; using Wino.Mail.WinUI.Extensions; +using Wino.Mail.WinUI.Helpers; using Wino.Mail.WinUI.Interfaces; using Wino.Mail.WinUI.Models; using Wino.Mail.WinUI.Services; @@ -66,13 +67,19 @@ public partial class App : WinoApplication, private bool _hasConfiguredAccounts; private bool _isExiting; private bool _activationInfrastructureInitialized; + private bool _appHostInfrastructureInitialized; + private bool _appNotificationsRegistered; private int _initialNotificationActivationHandled; private int _initialShareActivationHandled; private CancellationTokenSource? _autoSynchronizationLoopCts; private readonly SemaphoreSlim _autoSynchronizationSemaphore = new(1, 1); private readonly SemaphoreSlim _activationInfrastructureSemaphore = new(1, 1); + private readonly SemaphoreSlim _appHostInfrastructureSemaphore = new(1, 1); private readonly ConcurrentDictionary _inboxSyncCounters = []; + private readonly AppNotificationActivationBuffer _bufferedAppNotificationActivations = new(); private NativeTrayIcon? _trayIcon; + private readonly record struct NotificationActivationRoute(bool RequiresForegroundWindow, Func? ExecuteAsync); + private readonly record struct ShellWindowActivationResult(IWinoShellWindow? ShellWindow, bool WasCreated); internal bool IsExiting => _isExiting; @@ -83,6 +90,7 @@ public partial class App : WinoApplication, Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); CryptographyContext.Register(typeof(WindowsSecureMimeContext)); + EnsureAppNotificationRegistration(); RegisterRecipients(); } @@ -271,12 +279,16 @@ public partial class App : WinoApplication, shellWindow.Close(); } - private async Task ActivateWindowAsync(WindowEx window) + private async Task ActivateWindowAsync(WindowEx window, bool applyThemeToWindow = true) { var windowManager = Services.GetRequiredService(); MainWindow = window; windowManager.ActivateWindow(window); - await NewThemeService.ApplyThemeToActiveWindowAsync(); + + if (applyThemeToWindow) + { + await NewThemeService.ApplyThemeToActiveWindowAsync(); + } } private Task ExitApplicationAsync() @@ -410,6 +422,12 @@ public partial class App : WinoApplication, => Services.GetRequiredService().GetWindow(WinoWindowKind.Shell) is IWinoShellWindow; private async Task EnsureActivationInfrastructureAsync() + { + await EnsureCoreActivationInfrastructureAsync(); + await EnsureAppHostInfrastructureAsync(); + } + + private async Task EnsureCoreActivationInfrastructureAsync() { if (_activationInfrastructureInitialized) return; @@ -421,7 +439,7 @@ public partial class App : WinoApplication, if (_activationInfrastructureInitialized) return; - TryRegisterAppNotifications(); + EnsureAppNotificationRegistration(); await Services.GetRequiredService() .RunIfNeededAsync(); @@ -432,18 +450,8 @@ public partial class App : WinoApplication, _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 @@ -452,6 +460,38 @@ public partial class App : WinoApplication, } } + private async Task EnsureAppHostInfrastructureAsync() + { + await EnsureCoreActivationInfrastructureAsync(); + + if (_appHostInfrastructureInitialized) + return; + + await _appHostInfrastructureSemaphore.WaitAsync(); + + try + { + if (_appHostInfrastructureInitialized) + return; + + EnsureWindowManagerConfigured(); + EnsureTrayIconCreated(); + + if (_hasConfiguredAccounts) + { + _preferencesService!.PreferenceChanged -= PreferencesServiceChanged; + _preferencesService.PreferenceChanged += PreferencesServiceChanged; + RestartAutoSynchronizationLoop(); + } + + _appHostInfrastructureInitialized = true; + } + finally + { + _appHostInfrastructureSemaphore.Release(); + } + } + private bool TryMarkInitialNotificationActivationHandled() => Interlocked.Exchange(ref _initialNotificationActivationHandled, 1) == 0; @@ -462,115 +502,123 @@ public partial class App : WinoApplication, { base.OnLaunched(args); - await EnsureActivationInfrastructureAsync(); + await EnsureCoreActivationInfrastructureAsync(); - var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs(); + var activationArgs = ResolveStartupActivation(); - if (activationArgs.Kind == ExtendedActivationKind.ShareTarget && - TryMarkInitialShareActivationHandled()) - { - LogActivation("Processing share target activation from OnLaunched."); + if (await TryHandleStartupAppNotificationActivationAsync(activationArgs)) + return; - if (await HandleShareTargetActivationAsync(activationArgs, activateWindow: true)) - return; - } + await EnsureAppHostInfrastructureAsync(); var hasAnyAccount = _hasConfiguredAccounts; - if (!IsStartupTaskLaunch() && !hasAnyAccount) + var isStartupTaskLaunch = activationArgs.Kind == ExtendedActivationKind.StartupTask; + + if (!hasAnyAccount && !isStartupTaskLaunch) { - CreateWelcomeWindow(); - await NewThemeService.InitializeAsync(); - MainWindow?.Activate(); - LogActivation("Welcome window created and activated."); + await LaunchWelcomeWindowAsync(); return; } - // Check if launched from toast notification. - if (IsNotificationActivation(out AppNotificationActivatedEventArgs toastArgs) && - TryMarkInitialNotificationActivationHandled()) - { - LogActivation($"Processing notification activation from OnLaunched. Arguments: {toastArgs.Argument}"); - await HandleToastActivationAsync(toastArgs.Argument, toastArgs.UserInput); + if (await TryHandleLaunchActivationAsync(args, activationArgs)) return; - } - if (ToastActivationResolver.TryParse(args.Arguments, out var launchToastArguments) && - TryMarkInitialNotificationActivationHandled()) - { - LogActivation($"Processing toast launch activation from OnLaunched. Arguments: {args.Arguments}"); - await HandleToastActivationAsync(launchToastArguments); - return; - } - - // Check if launched by startup task. - bool isStartupTaskLaunch = IsStartupTaskLaunch(); - - if (isStartupTaskLaunch && !hasAnyAccount) - { - CreateWelcomeWindow(); - } - else - { - CreateWindow(args); - } - - // Initialize theme service after window creation. - // Theme service requires the window to exist to properly load and apply themes. - await NewThemeService.InitializeAsync(); - - if (hasAnyAccount) - { - // Wino account loading and activation. - await LoadInitialWinoAccountAsync(); - } - - LogActivation("Theme service initialized."); - - // If startup task launch, keep window hidden (system tray only). - // Otherwise, activate the window normally. - if (isStartupTaskLaunch) - { - LogActivation("Launched by startup task. Window created but hidden (system tray only)."); - } - else - { - // Normal launch - show and activate the window. - // The What's New dialog is shown from MailAppShellViewModel.OnNavigatedTo once XamlRoot is ready. - MainWindow?.Activate(); - LogActivation("Window created and activated."); - } + await CompleteStandardLaunchAsync(args, hasAnyAccount, isStartupTaskLaunch); } - public async Task HandleInitialActivationAsync() + private AppActivationArguments ResolveStartupActivation() { var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs(); + if (Program.TryConsumeDeferredAppNotificationStartup()) + { + LogActivation($"Resolved deferred COM activation after notification registration. Kind: {activationArgs.Kind}"); + } + return activationArgs; + } + + private async Task TryHandleStartupAppNotificationActivationAsync(AppActivationArguments activationArgs) + { if (activationArgs.Kind != ExtendedActivationKind.AppNotification || activationArgs.Data is not AppNotificationActivatedEventArgs toastArgs || !TryMarkInitialNotificationActivationHandled()) { - return; + return false; } - LogActivation($"Processing initial notification activation from application startup. Arguments: {toastArgs.Argument}"); + LogActivation($"Processing app-notification activation from startup. Arguments: {toastArgs.Argument}"); - await EnsureActivationInfrastructureAsync(); - await HandleToastActivationAsync(toastArgs.Argument, toastArgs.UserInput); + if (!TryResolveNotificationActivationRoute(toastArgs, out var route)) + { + await HandleToastActivationAsync(toastArgs.Argument, toastArgs.UserInput); + + if (!IsAppRunning()) + { + LogActivation("Startup app-notification activation completed without a window. Exiting transient process."); + ExitApplication(); + } + + return true; + } + + if (route.RequiresForegroundWindow) + { + await EnsureAppHostInfrastructureAsync(); + await route.ExecuteAsync!.Invoke(); + return true; + } + + await route.ExecuteAsync!.Invoke(); + + if (!IsAppRunning()) + { + LogActivation("Background startup app-notification activation completed. Exiting without creating app host."); + ExitApplication(); + } + + return true; } private void AppNotificationInvoked(AppNotificationManager sender, AppNotificationActivatedEventArgs args) { + if (!_activationInfrastructureInitialized) + { + LogActivation($"Buffering app notification activation until infrastructure is ready. Arguments: {args.Argument}"); + _bufferedAppNotificationActivations.Enqueue(args); + return; + } + // AppNotification callbacks are not guaranteed to run on the UI thread. // Marshal toast handling to the window dispatcher before touching window APIs. - if (MainWindow?.DispatcherQueue?.TryEnqueue(() => _ = HandleToastActivationAsync(args.Argument, args.UserInput)) == true) + if (TryEnqueueActivationOnUiThread(() => _ = HandleToastActivationAsync(args.Argument, args.UserInput))) return; LogActivation($"Processing notification activation from NotificationInvoked. Arguments: {args.Argument}"); _ = HandleToastActivationAsync(args.Argument, args.UserInput); } - private void TryRegisterAppNotifications() + private bool TryResolveNotificationActivationRoute(AppNotificationActivatedEventArgs notificationArgs, + out NotificationActivationRoute route) { + route = default; + + if (!ToastActivationResolver.TryParse(notificationArgs.Argument, out var toastArguments)) + return false; + + return TryCreateNotificationActivationRoute(toastArguments, notificationArgs.UserInput, out route); + } + + private void EnsureAppNotificationRegistration() + { + if (!Program.ShouldRegisterAppNotifications()) + { + LogActivation("Skipping app notification registration for non-host entry activation."); + return; + } + + if (_appNotificationsRegistered) + return; + var notificationManager = AppNotificationManager.Default; notificationManager.NotificationInvoked -= AppNotificationInvoked; @@ -579,6 +627,7 @@ public partial class App : WinoApplication, try { notificationManager.Register(); + _appNotificationsRegistered = true; } catch (Exception ex) { @@ -591,70 +640,17 @@ public partial class App : WinoApplication, /// private async Task HandleToastActivationAsync(NotificationArguments toastArguments, IDictionary? userInput = null) { - LogActivation("Handling app notification activation."); - - if (toastArguments.TryGetValue(Constants.ToastStoreUpdateActionKey, out string storeUpdateAction) && - storeUpdateAction == Constants.ToastStoreUpdateActionInstall) + if (!TryCreateNotificationActivationRoute(toastArguments, userInput, out var route)) { - await HandleStoreUpdateToastAsync(); + LogActivation("App notification activation did not match any known handler."); return; } - if (toastArguments.TryGetValue(Constants.ToastDismissActionKey, out string _)) - { - LogActivation("Handling notification dismiss action."); - return; - } + LogActivation(route.RequiresForegroundWindow + ? "Handling foreground app notification activation." + : "Handling background app notification activation."); - // Check calendar reminder toast activation first. - if (toastArguments.TryGetValue(Constants.ToastCalendarActionKey, out string calendarAction) && - toastArguments.TryGetValue(Constants.ToastCalendarItemIdKey, out string calendarItemIdString) && - Guid.TryParse(calendarItemIdString, out Guid calendarItemId)) - { - if (calendarAction == Constants.ToastCalendarNavigateAction) - { - await HandleCalendarToastNavigationAsync(calendarItemId); - return; - } - - if (calendarAction == Constants.ToastCalendarSnoozeAction) - { - await HandleCalendarToastSnoozeAsync(userInput, calendarItemId); - return; - } - - if (calendarAction == Constants.ToastCalendarJoinOnlineAction) - { - await HandleCalendarToastJoinOnlineAsync(calendarItemId); - return; - } - } - - // Check if this is a navigation toast (user clicked the notification). - if (toastArguments.TryGetValue(Constants.ToastActionKey, out MailOperation action) && - Guid.TryParse(toastArguments[Constants.ToastMailUniqueIdKey], out Guid mailItemUniqueId)) - { - if (action == MailOperation.Navigate) - { - // User clicked notification - create window if needed and navigate. - await HandleToastNavigationAsync(mailItemUniqueId); - } - else if (IsComposeToastAction(action)) - { - await HandleToastComposeActionAsync(action, mailItemUniqueId); - } - else - { - // User clicked action button (Mark as Read, Delete, etc.) - // Execute action without window and exit. - - await HandleToastActionAsync(action, mailItemUniqueId); - } - - return; - } - - LogActivation("App notification activation did not match any known handler."); + await route.ExecuteAsync!.Invoke(); } private Task HandleToastActivationAsync(string toastArgument, IDictionary? userInput = null) @@ -705,6 +701,37 @@ public partial class App : WinoApplication, return true; } + private async Task TryHandleLaunchActivationAsync(Microsoft.UI.Xaml.LaunchActivatedEventArgs args, + AppActivationArguments activationArgs) + { + if (activationArgs.Kind == ExtendedActivationKind.ShareTarget && + TryMarkInitialShareActivationHandled()) + { + LogActivation("Processing share target activation from OnLaunched."); + + if (await HandleShareTargetActivationAsync(activationArgs, activateWindow: true)) + return true; + } + + if (Program.TryConsumePendingBootstrapActivation(out var pendingBootstrapActivation)) + { + LogActivation($"Processing pending bootstrap activation. Kind: {pendingBootstrapActivation.Kind}, Mode: {pendingBootstrapActivation.Mode}"); + + if (await HandlePendingBootstrapActivationAsync(pendingBootstrapActivation)) + return true; + } + + if (ToastActivationResolver.TryParse(args.Arguments, out var launchToastArguments) && + TryMarkInitialNotificationActivationHandled()) + { + LogActivation($"Processing toast launch activation from OnLaunched. Arguments: {args.Arguments}"); + await HandleToastActivationAsync(launchToastArguments); + return true; + } + + return false; + } + private async Task ExtractMailShareRequestAsync(ShareTargetActivatedEventArgs shareTargetArgs) { var shareOperation = shareTargetArgs.ShareOperation; @@ -751,22 +778,36 @@ public partial class App : WinoApplication, } } - private async Task EnsureShellWindowAsync(WinoApplicationMode mode, bool activateWindow, bool suppressStartupFlows = true) + private async Task LaunchWelcomeWindowAsync() + { + CreateWelcomeWindow(); + await NewThemeService.InitializeAsync(); + MainWindow?.Activate(); + LogActivation("Welcome window created and activated."); + } + + private async Task EnsureShellWindowAsync(WinoApplicationMode mode, + bool activateWindow, + bool suppressStartupFlows = true, + object? activationParameter = null) { var windowManager = Services.GetRequiredService(); var navigationService = Services.GetRequiredService(); var shellWindow = windowManager.GetWindow(WinoWindowKind.Shell) as IWinoShellWindow; + var wasCreated = false; if (shellWindow == null) { LogActivation($"Creating shell window for {mode} activation."); + wasCreated = true; CreateWindow( null, AppEntryConstants.GetModeLaunchArgument(mode), new ShellModeActivationContext { - SuppressStartupFlows = suppressStartupFlows + SuppressStartupFlows = suppressStartupFlows, + Parameter = activationParameter }); await NewThemeService.InitializeAsync(); @@ -780,18 +821,22 @@ public partial class App : WinoApplication, } else { + ApplyShellWindowTaskbarIdentity(shellWindow, mode); navigationService.ChangeApplicationMode(mode, new ShellModeActivationContext { - SuppressStartupFlows = suppressStartupFlows + SuppressStartupFlows = suppressStartupFlows, + Parameter = activationParameter }); } + ApplyShellWindowTaskbarIdentity(shellWindow, mode); + if (activateWindow && shellWindow is WindowEx window) { - await ActivateWindowAsync(window); + await ActivateWindowAsync(window, applyThemeToWindow: wasCreated); } - return shellWindow; + return new ShellWindowActivationResult(shellWindow, wasCreated); } private async Task HandleStoreUpdateToastAsync() @@ -808,18 +853,42 @@ public partial class App : WinoApplication, private async Task HandleCalendarToastNavigationAsync(Guid calendarItemId) { var calendarService = Services.GetRequiredService(); - var navigationService = Services.GetRequiredService(); + var fallbackNavigationArgs = new CalendarPageNavigationArgs + { + RequestDefaultNavigation = true + }; + + if (!HasShellWindow()) + { + await EnsureShellWindowAsync( + WinoApplicationMode.Calendar, + activateWindow: true, + activationParameter: fallbackNavigationArgs); + } var calendarItem = await calendarService.GetCalendarItemAsync(calendarItemId); if (calendarItem == null) + { + LogActivation($"Calendar notification navigation item was not found for {calendarItemId}. Opening calendar shell only."); + + await EnsureShellWindowAsync( + WinoApplicationMode.Calendar, + activateWindow: true, + activationParameter: fallbackNavigationArgs); return; + } var target = new CalendarItemTarget(calendarItem, CalendarEventTargetType.Single); + var navigationArgs = new CalendarPageNavigationArgs + { + NavigationDate = calendarItem.LocalStartDate, + PendingTarget = target + }; - await EnsureShellWindowAsync(WinoApplicationMode.Calendar, activateWindow: true); - - navigationService.ChangeApplicationMode(Core.Domain.Enums.WinoApplicationMode.Calendar); - navigationService.Navigate(WinoPage.EventDetailsPage, target); + await EnsureShellWindowAsync( + WinoApplicationMode.Calendar, + activateWindow: true, + activationParameter: navigationArgs); } private async Task HandleCalendarToastSnoozeAsync(IDictionary? userInput, Guid calendarItemId) @@ -848,6 +917,38 @@ public partial class App : WinoApplication, await nativeAppService.LaunchUriAsync(joinUri); } + private async Task CompleteStandardLaunchAsync(Microsoft.UI.Xaml.LaunchActivatedEventArgs args, + bool hasAnyAccount, + bool isStartupTaskLaunch) + { + if (isStartupTaskLaunch && !hasAnyAccount) + { + CreateWelcomeWindow(); + } + else + { + CreateWindow(args); + } + + await NewThemeService.InitializeAsync(); + + if (hasAnyAccount) + { + await LoadInitialWinoAccountAsync(); + } + + LogActivation("Theme service initialized."); + + if (isStartupTaskLaunch) + { + LogActivation("Launched by startup task. Window created but hidden (system tray only)."); + return; + } + + MainWindow?.Activate(); + LogActivation("Window created and activated."); + } + private bool TryGetSnoozeDurationMinutes(IDictionary? userInput, out int snoozeDurationMinutes) { snoozeDurationMinutes = _preferencesService?.DefaultSnoozeDurationInMinutes ?? 0; @@ -871,7 +972,6 @@ public partial class App : WinoApplication, private async Task HandleToastNavigationAsync(Guid mailItemUniqueId) { var mailService = Services.GetRequiredService(); - var navigationService = Services.GetRequiredService(); var account = await mailService.GetMailAccountByUniqueIdAsync(mailItemUniqueId); if (account == null) @@ -896,7 +996,6 @@ public partial class App : WinoApplication, var shellWindowAlreadyExists = HasShellWindow(); await EnsureShellWindowAsync(WinoApplicationMode.Mail, activateWindow: true); - navigationService.ChangeApplicationMode(Core.Domain.Enums.WinoApplicationMode.Mail); if (shellWindowAlreadyExists) { @@ -1036,7 +1135,6 @@ public partial class App : WinoApplication, } await EnsureShellWindowAsync(WinoApplicationMode.Mail, activateWindow: true); - navigationService.ChangeApplicationMode(WinoApplicationMode.Mail); if (mailShellViewModel.MenuItems.TryGetAccountMenuItem(account.Id, out IAccountMenuItem accountMenuItem)) { @@ -1131,6 +1229,7 @@ public partial class App : WinoApplication, ? resolvedActivationMode : AppModeActivationResolver.Resolve(args?.Arguments, GetCurrentLaunchTileId(), Environment.CommandLine, defaultMode); + ApplyShellWindowTaskbarIdentity(shellWindow, targetMode); navigationService.ChangeApplicationMode(targetMode, activationContextOverride); return; } @@ -1164,6 +1263,18 @@ public partial class App : WinoApplication, shellWindow.HandleAppActivation(args?.Arguments, GetCurrentLaunchTileId(), Environment.CommandLine); } + private static void ApplyShellWindowTaskbarIdentity(IWinoShellWindow? shellWindow, WinoApplicationMode mode) + { + if (shellWindow is not WindowEx window) + return; + + var packagedApplicationId = AppEntryConstants.GetPackagedApplicationId(mode); + if (packagedApplicationId == null) + return; + + WindowAppUserModelIdHelper.TrySet(window, AppEntryConstants.GetAppUserModelId(mode)); + } + private void CreateWelcomeWindow() { LogActivation("Creating welcome window."); @@ -1642,7 +1753,7 @@ public partial class App : WinoApplication, // Handle toast notification activation var toastArgs = (AppNotificationActivatedEventArgs)args.Data; LogActivation($"Processing redirected notification activation. Arguments: {toastArgs.Argument}"); - _ = HandleToastActivationAsync(toastArgs.Argument, toastArgs.UserInput); + await HandleToastActivationAsync(toastArgs.Argument, toastArgs.UserInput); } else if (args.Kind == ExtendedActivationKind.ShareTarget) { @@ -1662,7 +1773,7 @@ public partial class App : WinoApplication, { shouldActivateWindow = ToastActivationResolver.ShouldBringToForeground(launchToastArguments); LogActivation($"Processing redirected toast launch activation. Arguments: {launchArgs.Arguments}"); - _ = HandleToastActivationAsync(launchToastArguments); + await HandleToastActivationAsync(launchToastArguments); } else { @@ -1692,12 +1803,89 @@ public partial class App : WinoApplication, } // Dispatch to UI thread since this is called from Program.OnActivated. - if (MainWindow?.DispatcherQueue.TryEnqueue(() => _ = HandleRedirectedActivationAsync()) == true) + if (TryEnqueueActivationOnUiThread(() => _ = HandleRedirectedActivationAsync())) return; _ = HandleRedirectedActivationAsync(); } + private bool TryCreateNotificationActivationRoute(NotificationArguments toastArguments, + IDictionary? userInput, + out NotificationActivationRoute route) + { + if (toastArguments.TryGetValue(Constants.ToastStoreUpdateActionKey, out string storeUpdateAction) && + storeUpdateAction == Constants.ToastStoreUpdateActionInstall) + { + route = new NotificationActivationRoute(true, HandleStoreUpdateToastAsync); + return true; + } + + if (toastArguments.TryGetValue(Constants.ToastDismissActionKey, out string _)) + { + route = new NotificationActivationRoute(false, () => + { + LogActivation("Handling notification dismiss action."); + return Task.CompletedTask; + }); + return true; + } + + if (toastArguments.TryGetValue(Constants.ToastCalendarActionKey, out string calendarAction) && + toastArguments.TryGetValue(Constants.ToastCalendarItemIdKey, out string calendarItemIdString) && + Guid.TryParse(calendarItemIdString, out Guid calendarItemId)) + { + route = calendarAction switch + { + Constants.ToastCalendarNavigateAction => new NotificationActivationRoute(true, () => HandleCalendarToastNavigationAsync(calendarItemId)), + Constants.ToastCalendarSnoozeAction => new NotificationActivationRoute(false, () => HandleCalendarToastSnoozeAsync(userInput, calendarItemId)), + Constants.ToastCalendarJoinOnlineAction => new NotificationActivationRoute(false, () => HandleCalendarToastJoinOnlineAsync(calendarItemId)), + _ => default + }; + + return route.ExecuteAsync != null; + } + + if (toastArguments.TryGetValue(Constants.ToastActionKey, out MailOperation action) && + Guid.TryParse(toastArguments[Constants.ToastMailUniqueIdKey], out Guid mailItemUniqueId)) + { + if (action == MailOperation.Navigate) + { + route = new NotificationActivationRoute(true, () => HandleToastNavigationAsync(mailItemUniqueId)); + return true; + } + + if (IsComposeToastAction(action)) + { + route = new NotificationActivationRoute(true, () => HandleToastComposeActionAsync(action, mailItemUniqueId)); + return true; + } + + route = new NotificationActivationRoute(false, () => HandleToastActionAsync(action, mailItemUniqueId)); + return true; + } + + route = default; + return false; + } + + private async Task HandlePendingBootstrapActivationAsync(PendingBootstrapActivation pendingBootstrapActivation) + { + if (pendingBootstrapActivation.Mode != WinoApplicationMode.Calendar) + return false; + + var navigationArgs = new CalendarPageNavigationArgs + { + RequestDefaultNavigation = true + }; + + await EnsureShellWindowAsync( + WinoApplicationMode.Calendar, + activateWindow: true, + activationParameter: navigationArgs); + + return true; + } + private static string AppendLaunchArgument(string? launchArguments, string launchArgument) { return string.IsNullOrWhiteSpace(launchArguments) @@ -1778,6 +1966,32 @@ public partial class App : WinoApplication, return null; } + private bool TryEnqueueActivationOnUiThread(Action action) + { + var dispatcherQueue = MainWindow?.DispatcherQueue; + + if (dispatcherQueue == null) + { + var windowManager = Services.GetService(); + var currentWindow = windowManager?.ActiveWindow + ?? windowManager?.GetWindow(WinoWindowKind.Shell) + ?? windowManager?.GetWindow(WinoWindowKind.Welcome); + + dispatcherQueue = currentWindow?.DispatcherQueue; + } + + if (dispatcherQueue == null) + return false; + + if (dispatcherQueue.HasThreadAccess) + { + action(); + return true; + } + + return dispatcherQueue.TryEnqueue(() => action()); + } + } diff --git a/Wino.Mail.WinUI/Controls/Calendar/CalendarItemControl.xaml b/Wino.Mail.WinUI/Controls/Calendar/CalendarItemControl.xaml index 51b113d0..305b126b 100644 --- a/Wino.Mail.WinUI/Controls/Calendar/CalendarItemControl.xaml +++ b/Wino.Mail.WinUI/Controls/Calendar/CalendarItemControl.xaml @@ -20,8 +20,7 @@ CornerRadius="4" DoubleTapped="ControlDoubleTapped" RightTapped="ControlRightTapped" - Tapped="ControlTapped" - ToolTipService.ToolTip="{x:Bind CalendarItem.DisplayTitle, Mode=OneWay}"> + Tapped="ControlTapped"> @@ -36,6 +35,9 @@ Placement="BottomEdgeAlignedLeft" ShowMode="Auto" /> + + + + { + calendarItemViewModel.DisplayingPeriod = new TimeRange( date.ToDateTime(TimeOnly.MinValue), date.AddDays(1).ToDateTime(TimeOnly.MinValue)); - calendarItemViewModel.CalendarSettings = CalendarSettings; + calendarItemViewModel.CalendarSettings = CalendarSettings; + }); } private static VisibleDateRange CreateLayoutRange(VisibleDateRange visibleRange, CalendarSettings calendarSettings) diff --git a/Wino.Mail.WinUI/Package.appxmanifest b/Wino.Mail.WinUI/Package.appxmanifest index dfe45eb0..72eab8cd 100644 --- a/Wino.Mail.WinUI/Package.appxmanifest +++ b/Wino.Mail.WinUI/Package.appxmanifest @@ -133,18 +133,6 @@ - - - - - - - - - - - - Calendar Protocol diff --git a/Wino.Mail.WinUI/Program.cs b/Wino.Mail.WinUI/Program.cs index 5444cb3c..f57a26fd 100644 --- a/Wino.Mail.WinUI/Program.cs +++ b/Wino.Mail.WinUI/Program.cs @@ -13,38 +13,87 @@ namespace Wino.Mail.WinUI; public class Program { + private const string AppNotificationActivatedCommandLinePrefix = "----AppNotificationActivated:"; private const string SingleInstanceKey = "WinoMailSingleInstance"; private const string ForceAlternateModeSignalEventName = "Local\\WinoMailForceAlternateMode"; + private const string MailHostRunningMutexName = "Local\\WinoMailMailHostRunning"; private const int VkControl = 0x11; private static bool _forceAlternateModeOnLaunch; private static EventWaitHandle? _forceAlternateModeSignalHandle; + private static Mutex? _mailHostRunningMutex; + private static PendingBootstrapActivation? _pendingBootstrapActivation; + private static bool _hasDeferredAppNotificationStartup; + private static bool _shouldRegisterAppNotifications; [STAThread] static int Main(string[] args) { WinRT.ComWrappersSupport.InitializeComWrappers(); - bool isRedirect = DecideRedirection(); + + if (TryCaptureCommandLineToastActivation(args)) + { + _shouldRegisterAppNotifications = true; + EnsureMailHostRunningMutex(); + StartApplication(); + return 0; + } + + var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs(); + var shouldBootstrapCalendarEntry = CalendarEntryBootstrapActivation.ShouldBootstrapToMailHost(activationArgs); + _shouldRegisterAppNotifications = !shouldBootstrapCalendarEntry; + + if (shouldBootstrapCalendarEntry && !IsMailHostRunning()) + { + if (CalendarEntryBootstrapActivation.QueuePendingActivation(activationArgs) && + CalendarEntryBootstrapActivation.LaunchMailHost()) + { + return 0; + } + + CalendarEntryBootstrapActivation.ClearPendingActivation(); + } + + _pendingBootstrapActivation = CalendarEntryBootstrapActivation.ConsumePendingActivation(); + bool isRedirect = DecideRedirection(activationArgs); if (!isRedirect) { - Application.Start((p) => - { - var context = new DispatcherQueueSynchronizationContext( - DispatcherQueue.GetForCurrentThread()); - SynchronizationContext.SetSynchronizationContext(context); - var app = new App(); - _ = app.HandleInitialActivationAsync(); - }); + EnsureMailHostRunningMutex(); + StartApplication(); } return 0; } - private static bool DecideRedirection() + public static bool ShouldRegisterAppNotifications() + => _shouldRegisterAppNotifications; + + internal static bool TryConsumeDeferredAppNotificationStartup() + { + if (!_hasDeferredAppNotificationStartup) + return false; + + _hasDeferredAppNotificationStartup = false; + return true; + } + + internal static bool TryConsumePendingBootstrapActivation(out PendingBootstrapActivation activation) + { + if (_pendingBootstrapActivation == null) + { + activation = null!; + return false; + } + + activation = _pendingBootstrapActivation; + _pendingBootstrapActivation = null; + return true; + } + + private static bool DecideRedirection(AppActivationArguments args) { bool isRedirect = false; - AppActivationArguments args = AppInstance.GetCurrent().GetActivatedEventArgs(); _forceAlternateModeOnLaunch = args.Kind == ExtendedActivationKind.Launch && IsCtrlKeyDown(); AppInstance keyInstance = AppInstance.FindOrRegisterForKey(SingleInstanceKey); @@ -69,6 +118,60 @@ public class Program return isRedirect; } + private static bool TryCaptureCommandLineToastActivation(string[] args) + { + var commandLine = Environment.CommandLine; + var prefixIndex = commandLine.IndexOf(AppNotificationActivatedCommandLinePrefix, StringComparison.OrdinalIgnoreCase); + + if (prefixIndex < 0) + return false; + + // Do not touch AppInstance.GetActivatedEventArgs here. For app-notification cold starts, + // Windows App SDK expects the app to register AppNotificationManager first and then + // resolve the activation inside App.OnLaunched. + _hasDeferredAppNotificationStartup = true; + return true; + } + + private static void StartApplication() + { + Application.Start((p) => + { + var context = new DispatcherQueueSynchronizationContext( + DispatcherQueue.GetForCurrentThread()); + SynchronizationContext.SetSynchronizationContext(context); + _ = new App(); + }); + } + + private static bool IsMailHostRunning() + { + try + { + if (!Mutex.TryOpenExisting(MailHostRunningMutexName, out var existingMutex)) + return false; + + existingMutex.Dispose(); + return true; + } + catch + { + return false; + } + } + + private static void EnsureMailHostRunningMutex() + { + try + { + _mailHostRunningMutex ??= new Mutex(false, MailHostRunningMutexName); + } + catch + { + _mailHostRunningMutex = null; + } + } + [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr CreateEvent( IntPtr lpEventAttributes, bool bManualReset, diff --git a/Wino.Mail.WinUI/Services/NavigationService.cs b/Wino.Mail.WinUI/Services/NavigationService.cs index 233df6e3..724babac 100644 --- a/Wino.Mail.WinUI/Services/NavigationService.cs +++ b/Wino.Mail.WinUI/Services/NavigationService.cs @@ -243,7 +243,19 @@ public class NavigationService : NavigationServiceBase, INavigationService _statePersistanceService.AppModeTitle = GetApplicationModeTitle(mode); if (coreFrame.Content is IShellHost activeShell && activeShell.HasShellContent && currentMode == mode) + { + if (activationContext?.Parameter != null) + { + activeShell.ActivateMode(mode, new ShellModeActivationContext + { + IsInitialActivation = false, + SuppressStartupFlows = activationContext.SuppressStartupFlows, + Parameter = activationContext.Parameter + }); + } + return true; + } _pendingInnerShellTransition = isInitialShellNavigation ? null @@ -504,7 +516,7 @@ public class NavigationService : NavigationServiceBase, INavigationService : DateOnly.FromDateTime(args.NavigationDate.Date); var displayRequest = new CalendarDisplayRequest(_statePersistanceService.CalendarDisplayType, targetDate); - return new LoadCalendarMessage(displayRequest, args.ForceReload); + return new LoadCalendarMessage(displayRequest, args.ForceReload, args.PendingTarget); } private bool NavigateInnerShellFrame(Frame frame, Type pageType, object? parameter, NavigationTransitionType transition) diff --git a/Wino.Mail.WinUI/Views/Calendar/CalendarPage.xaml.cs b/Wino.Mail.WinUI/Views/Calendar/CalendarPage.xaml.cs index 5cce241e..1d859cae 100644 --- a/Wino.Mail.WinUI/Views/Calendar/CalendarPage.xaml.cs +++ b/Wino.Mail.WinUI/Views/Calendar/CalendarPage.xaml.cs @@ -71,14 +71,20 @@ public sealed partial class CalendarPage : CalendarPageAbstract, ITitleBarSearch } var anchorDate = DateOnly.FromDateTime(DateTime.Now.Date); + CalendarItemTarget? pendingTarget = null; if (e.Parameter is CalendarPageNavigationArgs args && !args.RequestDefaultNavigation) { anchorDate = DateOnly.FromDateTime(args.NavigationDate.Date); + pendingTarget = args.PendingTarget; + } + else if (e.Parameter is CalendarPageNavigationArgs pendingArgs) + { + pendingTarget = pendingArgs.PendingTarget; } var request = new CalendarDisplayRequest(ViewModel.StatePersistanceService.CalendarDisplayType, anchorDate); - WeakReferenceMessenger.Default.Send(new LoadCalendarMessage(request)); + WeakReferenceMessenger.Default.Send(new LoadCalendarMessage(request, PendingTarget: pendingTarget)); } protected override void OnNavigatedFrom(NavigationEventArgs e) diff --git a/Wino.Mail.WinUI/Views/WinoAppShell.xaml.cs b/Wino.Mail.WinUI/Views/WinoAppShell.xaml.cs index da2bcc06..a1f7c629 100644 --- a/Wino.Mail.WinUI/Views/WinoAppShell.xaml.cs +++ b/Wino.Mail.WinUI/Views/WinoAppShell.xaml.cs @@ -89,7 +89,16 @@ public sealed partial class WinoAppShell : Views.Abstract.WinoAppShellAbstract, public void ActivateMode(WinoApplicationMode mode, ShellModeActivationContext activationContext) { if (_activeMode == mode && InnerShellFrame.Content != null) + { + if (activationContext.Parameter != null) + { + ViewModel.SetCurrentMode(mode); + ViewModel.CurrentClient.Activate(activationContext); + NotifyTitleBarContentChanged(); + } + return; + } DeactivateCurrentMode(); ResetShellModeNavigationState(); diff --git a/Wino.Messages/Client/Calendar/LoadCalendarMessage.cs b/Wino.Messages/Client/Calendar/LoadCalendarMessage.cs index 851ba3ba..00918586 100644 --- a/Wino.Messages/Client/Calendar/LoadCalendarMessage.cs +++ b/Wino.Messages/Client/Calendar/LoadCalendarMessage.cs @@ -1,3 +1,4 @@ +#nullable enable using Wino.Core.Domain.Models.Calendar; namespace Wino.Messaging.Client.Calendar; @@ -7,4 +8,8 @@ namespace Wino.Messaging.Client.Calendar; /// /// Display type and anchor date to resolve. /// Force a reload even if the resolved range did not change. -public record LoadCalendarMessage(CalendarDisplayRequest DisplayRequest, bool ForceReload = false); +/// Optional event target to open after the requested range is loaded. +public record LoadCalendarMessage( + CalendarDisplayRequest DisplayRequest, + bool ForceReload = false, + CalendarItemTarget? PendingTarget = null);