Fix notification activation and calendar bootstrap flow

This commit is contained in:
Burak Kaan Köse
2026-04-16 01:32:48 +02:00
parent 94675eee9a
commit e13aaadc78
15 changed files with 844 additions and 209 deletions
@@ -169,6 +169,7 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
var activationContext = parameters as ShellModeActivationContext; var activationContext = parameters as ShellModeActivationContext;
var shouldRunStartupFlows = activationContext?.IsInitialActivation ?? true; var shouldRunStartupFlows = activationContext?.IsInitialActivation ?? true;
var navigationArgs = activationContext?.Parameter as CalendarPageNavigationArgs;
PreferencesService.PreferenceChanged -= PreferencesServiceChanged; PreferencesService.PreferenceChanged -= PreferencesServiceChanged;
PreferencesService.PreferenceChanged += PreferencesServiceChanged; PreferencesService.PreferenceChanged += PreferencesServiceChanged;
@@ -178,7 +179,14 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
await InitializeAccountCalendarsAsync(); await InitializeAccountCalendarsAsync();
ValidateConfiguredNewEventCalendar(); 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) public override void OnNavigatedFrom(NavigationMode mode, object parameters)
@@ -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 lifetimeVersion = CurrentPageLifetimeVersion;
var hasLoadingLock = await WaitForCalendarLoadingLockAsync(lifetimeVersion).ConfigureAwait(false); var hasLoadingLock = await WaitForCalendarLoadingLockAsync(lifetimeVersion).ConfigureAwait(false);
@@ -718,6 +718,11 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
await _notificationBuilder.ClearCalendarTaskbarBadgeAsync().ConfigureAwait(false); await _notificationBuilder.ClearCalendarTaskbarBadgeAsync().ConfigureAwait(false);
_isCalendarBadgeClearedForPageLifetime = true; _isCalendarBadgeClearedForPageLifetime = true;
} }
if (loadSucceeded && pendingTarget != null && IsPageActive(lifetimeVersion))
{
await NavigateToPendingCalendarTargetAsync(pendingTarget).ConfigureAwait(false);
}
} }
public Task ReloadCurrentVisibleRangeAsync() public Task ReloadCurrentVisibleRangeAsync()
@@ -745,6 +750,31 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
NavigateEvent(new CalendarItemViewModel(calendarItem), CalendarEventTargetType.Single); 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<List<CalendarItemViewModel>> LoadCalendarItemsAsync(DateRange loadedDateWindow, long lifetimeVersion) private async Task<List<CalendarItemViewModel>> LoadCalendarItemsAsync(DateRange loadedDateWindow, long lifetimeVersion)
{ {
var loadedItems = new Dictionary<Guid, CalendarItemViewModel>(); var loadedItems = new Dictionary<Guid, CalendarItemViewModel>();
@@ -819,7 +849,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
} }
public async void Receive(LoadCalendarMessage message) 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) public void Receive(CalendarSettingsUpdatedMessage message)
{ {
@@ -1,4 +1,5 @@
using System; #nullable enable
using System;
namespace Wino.Core.Domain.Models.Calendar; 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. /// Force reloading the calendar data even when the target range does not change.
/// </summary> /// </summary>
public bool ForceReload { get; set; } public bool ForceReload { get; set; }
/// <summary>
/// Optional event target to navigate to after the calendar page loads the requested range.
/// </summary>
public CalendarItemTarget? PendingTarget { get; set; }
} }
+16 -14
View File
@@ -23,7 +23,6 @@ using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Menus; using Wino.Core.Domain.Models.Menus;
using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Domain.Models.Reader; using Wino.Core.Domain.Models.Reader;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Requests.Mail; using Wino.Core.Requests.Mail;
using Wino.Core.Services; using Wino.Core.Services;
using Wino.Mail.ViewModels.Collections; using Wino.Mail.ViewModels.Collections;
@@ -486,26 +485,29 @@ public partial class MailListPageViewModel : MailBaseViewModel,
{ {
if (!CanSynchronize) return; if (!CanSynchronize) return;
_notificationBuilder.CreateNotificationsAsync(MailCollection.SelectedItems.Select(a => a.MailCopy));
return;
// Only synchronize listed folders. // Only synchronize listed folders.
// When doing linked inbox sync, we need to save the sync id to report progress back only once. // 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. // Otherwise, we will report progress for each folder and that's what we don't want.
trackingSynchronizationId = Guid.NewGuid(); //trackingSynchronizationId = Guid.NewGuid();
completedTrackingSynchronizationCount = 0; //completedTrackingSynchronizationCount = 0;
foreach (var folder in ActiveFolder.HandlingFolders) //foreach (var folder in ActiveFolder.HandlingFolders)
{ //{
var options = new MailSynchronizationOptions() // var options = new MailSynchronizationOptions()
{ // {
AccountId = folder.MailAccountId, // AccountId = folder.MailAccountId,
Type = MailSynchronizationType.CustomFolders, // Type = MailSynchronizationType.CustomFolders,
SynchronizationFolderIds = [folder.Id], // SynchronizationFolderIds = [folder.Id],
GroupedSynchronizationTrackingId = trackingSynchronizationId // GroupedSynchronizationTrackingId = trackingSynchronizationId
}; // };
Messenger.Send(new NewMailSynchronizationRequested(options)); // Messenger.Send(new NewMailSynchronizationRequested(options));
} //}
} }
[RelayCommand] [RelayCommand]
@@ -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<string, string>? UserInput);
internal sealed class AppNotificationActivationBuffer
{
private readonly ConcurrentQueue<BufferedAppNotificationActivation> _pendingActivations = new();
private readonly SemaphoreSlim _pendingSignal = new(0);
public void Enqueue(AppNotificationActivatedEventArgs args)
{
var copiedUserInput = args.UserInput == null
? null
: new Dictionary<string, string>(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<BufferedAppNotificationActivation?> 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;
}
}
@@ -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<IStorageItem>()
.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;
}
}
+375 -161
View File
@@ -36,6 +36,7 @@ using Wino.Mail.ViewModels;
using Wino.Mail.ViewModels.Data; using Wino.Mail.ViewModels.Data;
using Wino.Mail.WinUI.Activation; using Wino.Mail.WinUI.Activation;
using Wino.Mail.WinUI.Extensions; using Wino.Mail.WinUI.Extensions;
using Wino.Mail.WinUI.Helpers;
using Wino.Mail.WinUI.Interfaces; using Wino.Mail.WinUI.Interfaces;
using Wino.Mail.WinUI.Models; using Wino.Mail.WinUI.Models;
using Wino.Mail.WinUI.Services; using Wino.Mail.WinUI.Services;
@@ -66,13 +67,19 @@ public partial class App : WinoApplication,
private bool _hasConfiguredAccounts; private bool _hasConfiguredAccounts;
private bool _isExiting; private bool _isExiting;
private bool _activationInfrastructureInitialized; private bool _activationInfrastructureInitialized;
private bool _appHostInfrastructureInitialized;
private bool _appNotificationsRegistered;
private int _initialNotificationActivationHandled; private int _initialNotificationActivationHandled;
private int _initialShareActivationHandled; private int _initialShareActivationHandled;
private CancellationTokenSource? _autoSynchronizationLoopCts; private CancellationTokenSource? _autoSynchronizationLoopCts;
private readonly SemaphoreSlim _autoSynchronizationSemaphore = new(1, 1); private readonly SemaphoreSlim _autoSynchronizationSemaphore = new(1, 1);
private readonly SemaphoreSlim _activationInfrastructureSemaphore = new(1, 1); private readonly SemaphoreSlim _activationInfrastructureSemaphore = new(1, 1);
private readonly SemaphoreSlim _appHostInfrastructureSemaphore = new(1, 1);
private readonly ConcurrentDictionary<Guid, int> _inboxSyncCounters = []; private readonly ConcurrentDictionary<Guid, int> _inboxSyncCounters = [];
private readonly AppNotificationActivationBuffer _bufferedAppNotificationActivations = new();
private NativeTrayIcon? _trayIcon; private NativeTrayIcon? _trayIcon;
private readonly record struct NotificationActivationRoute(bool RequiresForegroundWindow, Func<Task>? ExecuteAsync);
private readonly record struct ShellWindowActivationResult(IWinoShellWindow? ShellWindow, bool WasCreated);
internal bool IsExiting => _isExiting; internal bool IsExiting => _isExiting;
@@ -83,6 +90,7 @@ public partial class App : WinoApplication,
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
CryptographyContext.Register(typeof(WindowsSecureMimeContext)); CryptographyContext.Register(typeof(WindowsSecureMimeContext));
EnsureAppNotificationRegistration();
RegisterRecipients(); RegisterRecipients();
} }
@@ -271,12 +279,16 @@ public partial class App : WinoApplication,
shellWindow.Close(); shellWindow.Close();
} }
private async Task ActivateWindowAsync(WindowEx window) private async Task ActivateWindowAsync(WindowEx window, bool applyThemeToWindow = true)
{ {
var windowManager = Services.GetRequiredService<IWinoWindowManager>(); var windowManager = Services.GetRequiredService<IWinoWindowManager>();
MainWindow = window; MainWindow = window;
windowManager.ActivateWindow(window); windowManager.ActivateWindow(window);
await NewThemeService.ApplyThemeToActiveWindowAsync();
if (applyThemeToWindow)
{
await NewThemeService.ApplyThemeToActiveWindowAsync();
}
} }
private Task ExitApplicationAsync() private Task ExitApplicationAsync()
@@ -410,6 +422,12 @@ public partial class App : WinoApplication,
=> Services.GetRequiredService<IWinoWindowManager>().GetWindow(WinoWindowKind.Shell) is IWinoShellWindow; => Services.GetRequiredService<IWinoWindowManager>().GetWindow(WinoWindowKind.Shell) is IWinoShellWindow;
private async Task EnsureActivationInfrastructureAsync() private async Task EnsureActivationInfrastructureAsync()
{
await EnsureCoreActivationInfrastructureAsync();
await EnsureAppHostInfrastructureAsync();
}
private async Task EnsureCoreActivationInfrastructureAsync()
{ {
if (_activationInfrastructureInitialized) if (_activationInfrastructureInitialized)
return; return;
@@ -421,7 +439,7 @@ public partial class App : WinoApplication,
if (_activationInfrastructureInitialized) if (_activationInfrastructureInitialized)
return; return;
TryRegisterAppNotifications(); EnsureAppNotificationRegistration();
await Services.GetRequiredService<ReleaseLocalAccountDataCleanupService>() await Services.GetRequiredService<ReleaseLocalAccountDataCleanupService>()
.RunIfNeededAsync(); .RunIfNeededAsync();
@@ -432,18 +450,8 @@ public partial class App : WinoApplication,
_preferencesService = Services.GetRequiredService<IPreferencesService>(); _preferencesService = Services.GetRequiredService<IPreferencesService>();
_accountService = Services.GetRequiredService<IAccountService>(); _accountService = Services.GetRequiredService<IAccountService>();
EnsureWindowManagerConfigured();
EnsureTrayIconCreated();
_hasConfiguredAccounts = (await _accountService.GetAccountsAsync()).Any(); _hasConfiguredAccounts = (await _accountService.GetAccountsAsync()).Any();
if (_hasConfiguredAccounts)
{
_preferencesService.PreferenceChanged -= PreferencesServiceChanged;
_preferencesService.PreferenceChanged += PreferencesServiceChanged;
RestartAutoSynchronizationLoop();
}
_activationInfrastructureInitialized = true; _activationInfrastructureInitialized = true;
} }
finally 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() private bool TryMarkInitialNotificationActivationHandled()
=> Interlocked.Exchange(ref _initialNotificationActivationHandled, 1) == 0; => Interlocked.Exchange(ref _initialNotificationActivationHandled, 1) == 0;
@@ -462,115 +502,123 @@ public partial class App : WinoApplication,
{ {
base.OnLaunched(args); base.OnLaunched(args);
await EnsureActivationInfrastructureAsync(); await EnsureCoreActivationInfrastructureAsync();
var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs(); var activationArgs = ResolveStartupActivation();
if (activationArgs.Kind == ExtendedActivationKind.ShareTarget && if (await TryHandleStartupAppNotificationActivationAsync(activationArgs))
TryMarkInitialShareActivationHandled()) return;
{
LogActivation("Processing share target activation from OnLaunched.");
if (await HandleShareTargetActivationAsync(activationArgs, activateWindow: true)) await EnsureAppHostInfrastructureAsync();
return;
}
var hasAnyAccount = _hasConfiguredAccounts; var hasAnyAccount = _hasConfiguredAccounts;
if (!IsStartupTaskLaunch() && !hasAnyAccount) var isStartupTaskLaunch = activationArgs.Kind == ExtendedActivationKind.StartupTask;
if (!hasAnyAccount && !isStartupTaskLaunch)
{ {
CreateWelcomeWindow(); await LaunchWelcomeWindowAsync();
await NewThemeService.InitializeAsync();
MainWindow?.Activate();
LogActivation("Welcome window created and activated.");
return; return;
} }
// Check if launched from toast notification. if (await TryHandleLaunchActivationAsync(args, activationArgs))
if (IsNotificationActivation(out AppNotificationActivatedEventArgs toastArgs) &&
TryMarkInitialNotificationActivationHandled())
{
LogActivation($"Processing notification activation from OnLaunched. Arguments: {toastArgs.Argument}");
await HandleToastActivationAsync(toastArgs.Argument, toastArgs.UserInput);
return; return;
}
if (ToastActivationResolver.TryParse(args.Arguments, out var launchToastArguments) && await CompleteStandardLaunchAsync(args, hasAnyAccount, isStartupTaskLaunch);
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.");
}
} }
public async Task HandleInitialActivationAsync() private AppActivationArguments ResolveStartupActivation()
{ {
var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs(); var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
if (Program.TryConsumeDeferredAppNotificationStartup())
{
LogActivation($"Resolved deferred COM activation after notification registration. Kind: {activationArgs.Kind}");
}
return activationArgs;
}
private async Task<bool> TryHandleStartupAppNotificationActivationAsync(AppActivationArguments activationArgs)
{
if (activationArgs.Kind != ExtendedActivationKind.AppNotification || if (activationArgs.Kind != ExtendedActivationKind.AppNotification ||
activationArgs.Data is not AppNotificationActivatedEventArgs toastArgs || activationArgs.Data is not AppNotificationActivatedEventArgs toastArgs ||
!TryMarkInitialNotificationActivationHandled()) !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(); if (!TryResolveNotificationActivationRoute(toastArgs, out var route))
await HandleToastActivationAsync(toastArgs.Argument, toastArgs.UserInput); {
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) 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. // AppNotification callbacks are not guaranteed to run on the UI thread.
// Marshal toast handling to the window dispatcher before touching window APIs. // 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; return;
LogActivation($"Processing notification activation from NotificationInvoked. Arguments: {args.Argument}"); LogActivation($"Processing notification activation from NotificationInvoked. Arguments: {args.Argument}");
_ = HandleToastActivationAsync(args.Argument, args.UserInput); _ = 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; var notificationManager = AppNotificationManager.Default;
notificationManager.NotificationInvoked -= AppNotificationInvoked; notificationManager.NotificationInvoked -= AppNotificationInvoked;
@@ -579,6 +627,7 @@ public partial class App : WinoApplication,
try try
{ {
notificationManager.Register(); notificationManager.Register();
_appNotificationsRegistered = true;
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -591,70 +640,17 @@ public partial class App : WinoApplication,
/// </summary> /// </summary>
private async Task HandleToastActivationAsync(NotificationArguments toastArguments, IDictionary<string, string>? userInput = null) private async Task HandleToastActivationAsync(NotificationArguments toastArguments, IDictionary<string, string>? userInput = null)
{ {
LogActivation("Handling app notification activation."); if (!TryCreateNotificationActivationRoute(toastArguments, userInput, out var route))
if (toastArguments.TryGetValue(Constants.ToastStoreUpdateActionKey, out string storeUpdateAction) &&
storeUpdateAction == Constants.ToastStoreUpdateActionInstall)
{ {
await HandleStoreUpdateToastAsync(); LogActivation("App notification activation did not match any known handler.");
return; return;
} }
if (toastArguments.TryGetValue(Constants.ToastDismissActionKey, out string _)) LogActivation(route.RequiresForegroundWindow
{ ? "Handling foreground app notification activation."
LogActivation("Handling notification dismiss action."); : "Handling background app notification activation.");
return;
}
// Check calendar reminder toast activation first. await route.ExecuteAsync!.Invoke();
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.");
} }
private Task HandleToastActivationAsync(string toastArgument, IDictionary<string, string>? userInput = null) private Task HandleToastActivationAsync(string toastArgument, IDictionary<string, string>? userInput = null)
@@ -705,6 +701,37 @@ public partial class App : WinoApplication,
return true; return true;
} }
private async Task<bool> 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<MailShareRequest?> ExtractMailShareRequestAsync(ShareTargetActivatedEventArgs shareTargetArgs) private async Task<MailShareRequest?> ExtractMailShareRequestAsync(ShareTargetActivatedEventArgs shareTargetArgs)
{ {
var shareOperation = shareTargetArgs.ShareOperation; var shareOperation = shareTargetArgs.ShareOperation;
@@ -751,22 +778,36 @@ public partial class App : WinoApplication,
} }
} }
private async Task<IWinoShellWindow?> 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<ShellWindowActivationResult> EnsureShellWindowAsync(WinoApplicationMode mode,
bool activateWindow,
bool suppressStartupFlows = true,
object? activationParameter = null)
{ {
var windowManager = Services.GetRequiredService<IWinoWindowManager>(); var windowManager = Services.GetRequiredService<IWinoWindowManager>();
var navigationService = Services.GetRequiredService<INavigationService>(); var navigationService = Services.GetRequiredService<INavigationService>();
var shellWindow = windowManager.GetWindow(WinoWindowKind.Shell) as IWinoShellWindow; var shellWindow = windowManager.GetWindow(WinoWindowKind.Shell) as IWinoShellWindow;
var wasCreated = false;
if (shellWindow == null) if (shellWindow == null)
{ {
LogActivation($"Creating shell window for {mode} activation."); LogActivation($"Creating shell window for {mode} activation.");
wasCreated = true;
CreateWindow( CreateWindow(
null, null,
AppEntryConstants.GetModeLaunchArgument(mode), AppEntryConstants.GetModeLaunchArgument(mode),
new ShellModeActivationContext new ShellModeActivationContext
{ {
SuppressStartupFlows = suppressStartupFlows SuppressStartupFlows = suppressStartupFlows,
Parameter = activationParameter
}); });
await NewThemeService.InitializeAsync(); await NewThemeService.InitializeAsync();
@@ -780,18 +821,22 @@ public partial class App : WinoApplication,
} }
else else
{ {
ApplyShellWindowTaskbarIdentity(shellWindow, mode);
navigationService.ChangeApplicationMode(mode, new ShellModeActivationContext navigationService.ChangeApplicationMode(mode, new ShellModeActivationContext
{ {
SuppressStartupFlows = suppressStartupFlows SuppressStartupFlows = suppressStartupFlows,
Parameter = activationParameter
}); });
} }
ApplyShellWindowTaskbarIdentity(shellWindow, mode);
if (activateWindow && shellWindow is WindowEx window) 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() private async Task HandleStoreUpdateToastAsync()
@@ -808,18 +853,42 @@ public partial class App : WinoApplication,
private async Task HandleCalendarToastNavigationAsync(Guid calendarItemId) private async Task HandleCalendarToastNavigationAsync(Guid calendarItemId)
{ {
var calendarService = Services.GetRequiredService<ICalendarService>(); var calendarService = Services.GetRequiredService<ICalendarService>();
var navigationService = Services.GetRequiredService<INavigationService>(); var fallbackNavigationArgs = new CalendarPageNavigationArgs
{
RequestDefaultNavigation = true
};
if (!HasShellWindow())
{
await EnsureShellWindowAsync(
WinoApplicationMode.Calendar,
activateWindow: true,
activationParameter: fallbackNavigationArgs);
}
var calendarItem = await calendarService.GetCalendarItemAsync(calendarItemId); var calendarItem = await calendarService.GetCalendarItemAsync(calendarItemId);
if (calendarItem == null) 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; return;
}
var target = new CalendarItemTarget(calendarItem, CalendarEventTargetType.Single); var target = new CalendarItemTarget(calendarItem, CalendarEventTargetType.Single);
var navigationArgs = new CalendarPageNavigationArgs
{
NavigationDate = calendarItem.LocalStartDate,
PendingTarget = target
};
await EnsureShellWindowAsync(WinoApplicationMode.Calendar, activateWindow: true); await EnsureShellWindowAsync(
WinoApplicationMode.Calendar,
navigationService.ChangeApplicationMode(Core.Domain.Enums.WinoApplicationMode.Calendar); activateWindow: true,
navigationService.Navigate(WinoPage.EventDetailsPage, target); activationParameter: navigationArgs);
} }
private async Task HandleCalendarToastSnoozeAsync(IDictionary<string, string>? userInput, Guid calendarItemId) private async Task HandleCalendarToastSnoozeAsync(IDictionary<string, string>? userInput, Guid calendarItemId)
@@ -848,6 +917,38 @@ public partial class App : WinoApplication,
await nativeAppService.LaunchUriAsync(joinUri); 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<string, string>? userInput, out int snoozeDurationMinutes) private bool TryGetSnoozeDurationMinutes(IDictionary<string, string>? userInput, out int snoozeDurationMinutes)
{ {
snoozeDurationMinutes = _preferencesService?.DefaultSnoozeDurationInMinutes ?? 0; snoozeDurationMinutes = _preferencesService?.DefaultSnoozeDurationInMinutes ?? 0;
@@ -871,7 +972,6 @@ public partial class App : WinoApplication,
private async Task HandleToastNavigationAsync(Guid mailItemUniqueId) private async Task HandleToastNavigationAsync(Guid mailItemUniqueId)
{ {
var mailService = Services.GetRequiredService<IMailService>(); var mailService = Services.GetRequiredService<IMailService>();
var navigationService = Services.GetRequiredService<INavigationService>();
var account = await mailService.GetMailAccountByUniqueIdAsync(mailItemUniqueId); var account = await mailService.GetMailAccountByUniqueIdAsync(mailItemUniqueId);
if (account == null) if (account == null)
@@ -896,7 +996,6 @@ public partial class App : WinoApplication,
var shellWindowAlreadyExists = HasShellWindow(); var shellWindowAlreadyExists = HasShellWindow();
await EnsureShellWindowAsync(WinoApplicationMode.Mail, activateWindow: true); await EnsureShellWindowAsync(WinoApplicationMode.Mail, activateWindow: true);
navigationService.ChangeApplicationMode(Core.Domain.Enums.WinoApplicationMode.Mail);
if (shellWindowAlreadyExists) if (shellWindowAlreadyExists)
{ {
@@ -1036,7 +1135,6 @@ public partial class App : WinoApplication,
} }
await EnsureShellWindowAsync(WinoApplicationMode.Mail, activateWindow: true); await EnsureShellWindowAsync(WinoApplicationMode.Mail, activateWindow: true);
navigationService.ChangeApplicationMode(WinoApplicationMode.Mail);
if (mailShellViewModel.MenuItems.TryGetAccountMenuItem(account.Id, out IAccountMenuItem accountMenuItem)) if (mailShellViewModel.MenuItems.TryGetAccountMenuItem(account.Id, out IAccountMenuItem accountMenuItem))
{ {
@@ -1131,6 +1229,7 @@ public partial class App : WinoApplication,
? resolvedActivationMode ? resolvedActivationMode
: AppModeActivationResolver.Resolve(args?.Arguments, GetCurrentLaunchTileId(), Environment.CommandLine, defaultMode); : AppModeActivationResolver.Resolve(args?.Arguments, GetCurrentLaunchTileId(), Environment.CommandLine, defaultMode);
ApplyShellWindowTaskbarIdentity(shellWindow, targetMode);
navigationService.ChangeApplicationMode(targetMode, activationContextOverride); navigationService.ChangeApplicationMode(targetMode, activationContextOverride);
return; return;
} }
@@ -1164,6 +1263,18 @@ public partial class App : WinoApplication,
shellWindow.HandleAppActivation(args?.Arguments, GetCurrentLaunchTileId(), Environment.CommandLine); 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() private void CreateWelcomeWindow()
{ {
LogActivation("Creating welcome window."); LogActivation("Creating welcome window.");
@@ -1642,7 +1753,7 @@ public partial class App : WinoApplication,
// Handle toast notification activation // Handle toast notification activation
var toastArgs = (AppNotificationActivatedEventArgs)args.Data; var toastArgs = (AppNotificationActivatedEventArgs)args.Data;
LogActivation($"Processing redirected notification activation. Arguments: {toastArgs.Argument}"); 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) else if (args.Kind == ExtendedActivationKind.ShareTarget)
{ {
@@ -1662,7 +1773,7 @@ public partial class App : WinoApplication,
{ {
shouldActivateWindow = ToastActivationResolver.ShouldBringToForeground(launchToastArguments); shouldActivateWindow = ToastActivationResolver.ShouldBringToForeground(launchToastArguments);
LogActivation($"Processing redirected toast launch activation. Arguments: {launchArgs.Arguments}"); LogActivation($"Processing redirected toast launch activation. Arguments: {launchArgs.Arguments}");
_ = HandleToastActivationAsync(launchToastArguments); await HandleToastActivationAsync(launchToastArguments);
} }
else else
{ {
@@ -1692,12 +1803,89 @@ public partial class App : WinoApplication,
} }
// Dispatch to UI thread since this is called from Program.OnActivated. // Dispatch to UI thread since this is called from Program.OnActivated.
if (MainWindow?.DispatcherQueue.TryEnqueue(() => _ = HandleRedirectedActivationAsync()) == true) if (TryEnqueueActivationOnUiThread(() => _ = HandleRedirectedActivationAsync()))
return; return;
_ = HandleRedirectedActivationAsync(); _ = HandleRedirectedActivationAsync();
} }
private bool TryCreateNotificationActivationRoute(NotificationArguments toastArguments,
IDictionary<string, string>? 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<bool> 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) private static string AppendLaunchArgument(string? launchArguments, string launchArgument)
{ {
return string.IsNullOrWhiteSpace(launchArguments) return string.IsNullOrWhiteSpace(launchArguments)
@@ -1778,6 +1966,32 @@ public partial class App : WinoApplication,
return null; return null;
} }
private bool TryEnqueueActivationOnUiThread(Action action)
{
var dispatcherQueue = MainWindow?.DispatcherQueue;
if (dispatcherQueue == null)
{
var windowManager = Services.GetService<IWinoWindowManager>();
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());
}
} }
@@ -20,8 +20,7 @@
CornerRadius="4" CornerRadius="4"
DoubleTapped="ControlDoubleTapped" DoubleTapped="ControlDoubleTapped"
RightTapped="ControlRightTapped" RightTapped="ControlRightTapped"
Tapped="ControlTapped" Tapped="ControlTapped">
ToolTipService.ToolTip="{x:Bind CalendarItem.DisplayTitle, Mode=OneWay}">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="5" /> <ColumnDefinition Width="5" />
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
@@ -36,6 +35,9 @@
Placement="BottomEdgeAlignedLeft" Placement="BottomEdgeAlignedLeft"
ShowMode="Auto" /> ShowMode="Auto" />
</Grid.ContextFlyout> </Grid.ContextFlyout>
<ToolTipService.ToolTip>
<ToolTip Content="{x:Bind CalendarItem.DisplayTitle, Mode=OneWay}" />
</ToolTipService.ToolTip>
<Grid <Grid
@@ -443,10 +443,13 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
return; return;
} }
calendarItemViewModel.DisplayingPeriod = new TimeRange( DispatcherQueue.TryEnqueue(() =>
{
calendarItemViewModel.DisplayingPeriod = new TimeRange(
date.ToDateTime(TimeOnly.MinValue), date.ToDateTime(TimeOnly.MinValue),
date.AddDays(1).ToDateTime(TimeOnly.MinValue)); date.AddDays(1).ToDateTime(TimeOnly.MinValue));
calendarItemViewModel.CalendarSettings = CalendarSettings; calendarItemViewModel.CalendarSettings = CalendarSettings;
});
} }
private static VisibleDateRange CreateLayoutRange(VisibleDateRange visibleRange, CalendarSettings calendarSettings) private static VisibleDateRange CreateLayoutRange(VisibleDateRange visibleRange, CalendarSettings calendarSettings)
-12
View File
@@ -133,18 +133,6 @@
</uap:VisualElements> </uap:VisualElements>
<Extensions> <Extensions>
<desktop:Extension Category="windows.toastNotificationActivation">
<desktop:ToastNotificationActivation ToastActivatorCLSID="44c05d2b-aa1d-4e59-9d7d-8b4c8607cb8d" />
</desktop:Extension>
<com:Extension Category="windows.comServer">
<com:ComServer>
<com:ExeServer Executable="Wino.Mail.WinUI.exe" Arguments="----AppNotificationActivated:" DisplayName="Calendar toast activator">
<com:Class Id="44c05d2b-aa1d-4e59-9d7d-8b4c8607cb8d" DisplayName="Calendar toast activator"/>
</com:ExeServer>
</com:ComServer>
</com:Extension>
<uap:Extension Category="windows.protocol"> <uap:Extension Category="windows.protocol">
<uap:Protocol Name="webcal"> <uap:Protocol Name="webcal">
<uap:DisplayName>Calendar Protocol</uap:DisplayName> <uap:DisplayName>Calendar Protocol</uap:DisplayName>
+114 -11
View File
@@ -13,38 +13,87 @@ namespace Wino.Mail.WinUI;
public class Program public class Program
{ {
private const string AppNotificationActivatedCommandLinePrefix = "----AppNotificationActivated:";
private const string SingleInstanceKey = "WinoMailSingleInstance"; private const string SingleInstanceKey = "WinoMailSingleInstance";
private const string ForceAlternateModeSignalEventName = "Local\\WinoMailForceAlternateMode"; private const string ForceAlternateModeSignalEventName = "Local\\WinoMailForceAlternateMode";
private const string MailHostRunningMutexName = "Local\\WinoMailMailHostRunning";
private const int VkControl = 0x11; private const int VkControl = 0x11;
private static bool _forceAlternateModeOnLaunch; private static bool _forceAlternateModeOnLaunch;
private static EventWaitHandle? _forceAlternateModeSignalHandle; private static EventWaitHandle? _forceAlternateModeSignalHandle;
private static Mutex? _mailHostRunningMutex;
private static PendingBootstrapActivation? _pendingBootstrapActivation;
private static bool _hasDeferredAppNotificationStartup;
private static bool _shouldRegisterAppNotifications;
[STAThread] [STAThread]
static int Main(string[] args) static int Main(string[] args)
{ {
WinRT.ComWrappersSupport.InitializeComWrappers(); 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) if (!isRedirect)
{ {
Application.Start((p) => EnsureMailHostRunningMutex();
{ StartApplication();
var context = new DispatcherQueueSynchronizationContext(
DispatcherQueue.GetForCurrentThread());
SynchronizationContext.SetSynchronizationContext(context);
var app = new App();
_ = app.HandleInitialActivationAsync();
});
} }
return 0; 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; bool isRedirect = false;
AppActivationArguments args = AppInstance.GetCurrent().GetActivatedEventArgs();
_forceAlternateModeOnLaunch = args.Kind == ExtendedActivationKind.Launch && IsCtrlKeyDown(); _forceAlternateModeOnLaunch = args.Kind == ExtendedActivationKind.Launch && IsCtrlKeyDown();
AppInstance keyInstance = AppInstance.FindOrRegisterForKey(SingleInstanceKey); AppInstance keyInstance = AppInstance.FindOrRegisterForKey(SingleInstanceKey);
@@ -69,6 +118,60 @@ public class Program
return isRedirect; 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)] [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr CreateEvent( private static extern IntPtr CreateEvent(
IntPtr lpEventAttributes, bool bManualReset, IntPtr lpEventAttributes, bool bManualReset,
+13 -1
View File
@@ -243,7 +243,19 @@ public class NavigationService : NavigationServiceBase, INavigationService
_statePersistanceService.AppModeTitle = GetApplicationModeTitle(mode); _statePersistanceService.AppModeTitle = GetApplicationModeTitle(mode);
if (coreFrame.Content is IShellHost activeShell && activeShell.HasShellContent && currentMode == 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; return true;
}
_pendingInnerShellTransition = isInitialShellNavigation _pendingInnerShellTransition = isInitialShellNavigation
? null ? null
@@ -504,7 +516,7 @@ public class NavigationService : NavigationServiceBase, INavigationService
: DateOnly.FromDateTime(args.NavigationDate.Date); : DateOnly.FromDateTime(args.NavigationDate.Date);
var displayRequest = new CalendarDisplayRequest(_statePersistanceService.CalendarDisplayType, targetDate); 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) private bool NavigateInnerShellFrame(Frame frame, Type pageType, object? parameter, NavigationTransitionType transition)
@@ -71,14 +71,20 @@ public sealed partial class CalendarPage : CalendarPageAbstract, ITitleBarSearch
} }
var anchorDate = DateOnly.FromDateTime(DateTime.Now.Date); var anchorDate = DateOnly.FromDateTime(DateTime.Now.Date);
CalendarItemTarget? pendingTarget = null;
if (e.Parameter is CalendarPageNavigationArgs args && !args.RequestDefaultNavigation) if (e.Parameter is CalendarPageNavigationArgs args && !args.RequestDefaultNavigation)
{ {
anchorDate = DateOnly.FromDateTime(args.NavigationDate.Date); 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); 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) protected override void OnNavigatedFrom(NavigationEventArgs e)
@@ -89,7 +89,16 @@ public sealed partial class WinoAppShell : Views.Abstract.WinoAppShellAbstract,
public void ActivateMode(WinoApplicationMode mode, ShellModeActivationContext activationContext) public void ActivateMode(WinoApplicationMode mode, ShellModeActivationContext activationContext)
{ {
if (_activeMode == mode && InnerShellFrame.Content != null) if (_activeMode == mode && InnerShellFrame.Content != null)
{
if (activationContext.Parameter != null)
{
ViewModel.SetCurrentMode(mode);
ViewModel.CurrentClient.Activate(activationContext);
NotifyTitleBarContentChanged();
}
return; return;
}
DeactivateCurrentMode(); DeactivateCurrentMode();
ResetShellModeNavigationState(); ResetShellModeNavigationState();
@@ -1,3 +1,4 @@
#nullable enable
using Wino.Core.Domain.Models.Calendar; using Wino.Core.Domain.Models.Calendar;
namespace Wino.Messaging.Client.Calendar; namespace Wino.Messaging.Client.Calendar;
@@ -7,4 +8,8 @@ namespace Wino.Messaging.Client.Calendar;
/// </summary> /// </summary>
/// <param name="DisplayRequest">Display type and anchor date to resolve.</param> /// <param name="DisplayRequest">Display type and anchor date to resolve.</param>
/// <param name="ForceReload">Force a reload even if the resolved range did not change.</param> /// <param name="ForceReload">Force a reload even if the resolved range did not change.</param>
public record LoadCalendarMessage(CalendarDisplayRequest DisplayRequest, bool ForceReload = false); /// <param name="PendingTarget">Optional event target to open after the requested range is loaded.</param>
public record LoadCalendarMessage(
CalendarDisplayRequest DisplayRequest,
bool ForceReload = false,
CalendarItemTarget? PendingTarget = null);