Toast actions.
This commit is contained in:
@@ -48,7 +48,7 @@ internal class ToastNotificationActivationHandler : ActivationHandler<ToastNotif
|
||||
var message = new AccountMenuItemExtended(mailItem.AssignedFolder.Id, mailItem);
|
||||
|
||||
// Delegate this event to LaunchProtocolService so app shell can pick it up on launch if app doesn't work.
|
||||
var launchProtocolService = App.Current.Services.GetService<ILaunchProtocolService>();
|
||||
var launchProtocolService = App.Current.Services.GetRequiredService<ILaunchProtocolService>();
|
||||
launchProtocolService.LaunchParameter = message;
|
||||
|
||||
// Send the messsage anyways. Launch protocol service will be ignored if the message is picked up by subscriber shell.
|
||||
|
||||
+233
-80
@@ -1,13 +1,18 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Toolkit.Uwp.Notifications;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.Windows.AppLifecycle;
|
||||
using Microsoft.Windows.AppNotifications;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
using Wino.Core.WinUI;
|
||||
using Wino.Core.WinUI.Interfaces;
|
||||
using Wino.Mail.Services;
|
||||
@@ -26,75 +31,23 @@ public partial class App : WinoApplication, IRecipient<NewMailSynchronizationReq
|
||||
InitializeComponent();
|
||||
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
ToastNotificationManagerCompat.OnActivated += ToastActivationHandler;
|
||||
|
||||
RegisterRecipients();
|
||||
}
|
||||
|
||||
private async void ToastActivationHandler(ToastNotificationActivatedEventArgsCompat e)
|
||||
public bool IsNotificationActivation(out AppNotificationActivatedEventArgs args)
|
||||
{
|
||||
// If we weren't launched by an app, launch our window like normal.
|
||||
// Otherwise if launched by a toast, our OnActivated callback will be triggered.
|
||||
// https://learn.microsoft.com/en-us/windows/apps/windows-app-sdk/migrate-to-windows-app-sdk/guides/toast-notifications?tabs=appsdk
|
||||
var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
|
||||
|
||||
var toastArgs = ToastArguments.Parse(e.Argument);
|
||||
|
||||
var mailService = Services.GetRequiredService<IMailService>();
|
||||
var accountService = Services.GetRequiredService<IAccountService>();
|
||||
var actionKey = toastArgs.Contains(Constants.ToastActionKey) ? toastArgs[Constants.ToastActionKey] : string.Empty;
|
||||
|
||||
if (Guid.TryParse(toastArgs[Constants.ToastMailUniqueIdKey], out Guid mailItemUniqueId))
|
||||
if (activationArgs.Kind == ExtendedActivationKind.AppNotification)
|
||||
{
|
||||
// Action triggered.
|
||||
if (toastArgs.TryGetValue(Constants.ToastActionKey, out MailOperation action))
|
||||
{
|
||||
// Check if the app is running.
|
||||
|
||||
if (IsAppRunning())
|
||||
{
|
||||
// Get the synchronizer and queue the action for the given item.
|
||||
|
||||
var processor = Services.GetRequiredService<IWinoRequestProcessor>();
|
||||
var delegator = Services.GetRequiredService<IWinoRequestDelegator>();
|
||||
|
||||
var mailItem = await mailService.GetSingleMailItemAsync(mailItemUniqueId);
|
||||
|
||||
if (mailItem != null)
|
||||
{
|
||||
var package = new MailOperationPreperationRequest(action, mailItem);
|
||||
|
||||
await delegator.ExecuteAsync(package);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Notification clicked. Handle navigation.
|
||||
|
||||
var account = await mailService.GetMailAccountByUniqueIdAsync(mailItemUniqueId).ConfigureAwait(false);
|
||||
if (account == null) return;
|
||||
|
||||
var mailItem = await mailService.GetSingleMailItemAsync(mailItemUniqueId).ConfigureAwait(false);
|
||||
if (mailItem == null) return;
|
||||
|
||||
var message = new AccountMenuItemExtended(mailItem.AssignedFolder.Id, mailItem);
|
||||
|
||||
// Delegate this event to LaunchProtocolService so app shell can pick it up on launch if app doesn't work.
|
||||
var launchProtocolService = Services.GetRequiredService<ILaunchProtocolService>();
|
||||
launchProtocolService.LaunchParameter = message;
|
||||
|
||||
// Send the messsage anyways. Launch protocol service will be ignored if the message is picked up by subscriber shell.
|
||||
WeakReferenceMessenger.Default.Send(message);
|
||||
}
|
||||
args = ((AppNotificationActivatedEventArgs)activationArgs.Data);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ToastNotificationManagerCompat.WasCurrentProcessToastActivated())
|
||||
{
|
||||
MainWindow.BringToFront();
|
||||
}
|
||||
args = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
#region Dependency Injection
|
||||
@@ -155,38 +108,238 @@ public partial class App : WinoApplication, IRecipient<NewMailSynchronizationReq
|
||||
|
||||
protected override async void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
|
||||
{
|
||||
// If it's toast activation, compat will handle it.
|
||||
if (IsAppRunning()) return;
|
||||
base.OnLaunched(args);
|
||||
|
||||
// TODO: Check app relaunch mutex before loading anything.
|
||||
AppNotificationManager notificationManager = AppNotificationManager.Default;
|
||||
|
||||
// Initialize NewThemeService first to get backdrop settings before creating window
|
||||
var newThemeService = Services.GetRequiredService<INewThemeService>();
|
||||
var configService = Services.GetRequiredService<IConfigurationService>();
|
||||
var nativeAppService = Services.GetRequiredService<INativeAppService>();
|
||||
notificationManager.NotificationInvoked -= AppNotificationInvoked;
|
||||
notificationManager.NotificationInvoked += AppNotificationInvoked;
|
||||
notificationManager.Register();
|
||||
|
||||
// Initialize required services regardless of launch activation type.
|
||||
// All activation scenarios require these services to be ready.
|
||||
// Note: Theme service is initialized separately after window creation.
|
||||
await InitializeServicesAsync();
|
||||
|
||||
_synchronizationManager = Services.GetRequiredService<ISynchronizationManager>();
|
||||
|
||||
// Load saved backdrop type before creating window
|
||||
var savedBackdropType = (WindowBackdropType)configService.Get("WindowBackdropTypeKey", (int)WindowBackdropType.Mica);
|
||||
// Check if launched from toast notification.
|
||||
if (IsNotificationActivation(out AppNotificationActivatedEventArgs toastArgs))
|
||||
{
|
||||
await HandleToastActivationAsync(toastArgs);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if launched by startup task.
|
||||
bool isStartupTaskLaunch = IsStartupTaskLaunch();
|
||||
|
||||
// Create the window (needed for system tray icon even in startup task scenario).
|
||||
CreateWindow(args);
|
||||
|
||||
// Initialize theme service after window creation.
|
||||
// Theme service requires the window to exist to properly load and apply themes.
|
||||
await NewThemeService.InitializeAsync();
|
||||
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).");
|
||||
// Window is created but not activated. User can show it from system tray.
|
||||
}
|
||||
else
|
||||
{
|
||||
// Normal launch - show and activate the window.
|
||||
MainWindow.Activate();
|
||||
LogActivation("Window created and activated.");
|
||||
}
|
||||
}
|
||||
|
||||
private async void AppNotificationInvoked(AppNotificationManager sender, AppNotificationActivatedEventArgs args)
|
||||
=> await HandleToastActivationAsync(args);
|
||||
|
||||
/// <summary>
|
||||
/// Handles toast notification activation scenarios.
|
||||
/// </summary>
|
||||
private async Task HandleToastActivationAsync(AppNotificationActivatedEventArgs toastArgs)
|
||||
{
|
||||
var toastArguments = ToastArguments.Parse(toastArgs.Argument);
|
||||
|
||||
// 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
|
||||
{
|
||||
// User clicked action button (Mark as Read, Delete, etc.)
|
||||
// Execute action without window and exit.
|
||||
await HandleToastActionAsync(action, mailItemUniqueId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles toast notification click for navigation.
|
||||
/// Creates window if not running, sets up navigation parameter.
|
||||
/// </summary>
|
||||
private async Task HandleToastNavigationAsync(Guid mailItemUniqueId)
|
||||
{
|
||||
var mailService = Services.GetRequiredService<IMailService>();
|
||||
|
||||
var account = await mailService.GetMailAccountByUniqueIdAsync(mailItemUniqueId).ConfigureAwait(false);
|
||||
if (account == null) return;
|
||||
|
||||
var mailItem = await mailService.GetSingleMailItemAsync(mailItemUniqueId).ConfigureAwait(false);
|
||||
if (mailItem == null) return;
|
||||
|
||||
var message = new AccountMenuItemExtended(mailItem.AssignedFolder.Id, mailItem);
|
||||
|
||||
// Store navigation parameter in LaunchProtocolService so AppShell can pick it up.
|
||||
var launchProtocolService = Services.GetRequiredService<ILaunchProtocolService>();
|
||||
launchProtocolService.LaunchParameter = message;
|
||||
|
||||
// Create window if not already created.
|
||||
if (!IsAppRunning())
|
||||
{
|
||||
// Pass null for args since we're handling toast navigation
|
||||
await CreateAndActivateWindow(null!);
|
||||
}
|
||||
else
|
||||
{
|
||||
// App is already running - send message and bring window to front.
|
||||
WeakReferenceMessenger.Default.Send(message);
|
||||
MainWindow.BringToFront();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles toast action button clicks (Mark as Read, Delete, etc.).
|
||||
/// Executes the action without showing UI and exits the app.
|
||||
/// </summary>
|
||||
private async Task HandleToastActionAsync(MailOperation action, Guid mailItemUniqueId)
|
||||
{
|
||||
LogActivation($"Handling toast action: {action} for mail {mailItemUniqueId}");
|
||||
|
||||
var mailService = Services.GetRequiredService<IMailService>();
|
||||
var mailItem = await mailService.GetSingleMailItemAsync(mailItemUniqueId);
|
||||
|
||||
if (mailItem == null)
|
||||
{
|
||||
LogActivation("Mail item not found. Exiting.");
|
||||
Application.Current.Exit();
|
||||
return;
|
||||
}
|
||||
|
||||
var package = new MailOperationPreperationRequest(action, mailItem);
|
||||
|
||||
// Check if app is already running (has a window).
|
||||
if (IsAppRunning())
|
||||
{
|
||||
// App is running - use the simple delegator pattern.
|
||||
// The synchronization will happen in the background.
|
||||
LogActivation("App is running. Queueing request via delegator.");
|
||||
|
||||
var delegator = Services.GetRequiredService<IWinoRequestDelegator>();
|
||||
await delegator.ExecuteAsync(package);
|
||||
|
||||
// Don't exit - app continues running.
|
||||
LogActivation($"Toast action {action} queued successfully.");
|
||||
}
|
||||
else
|
||||
{
|
||||
// App is not running - we need to wait for sync before exiting.
|
||||
LogActivation("App is not running. Executing synchronization and waiting for completion.");
|
||||
|
||||
if (_synchronizationManager == null)
|
||||
{
|
||||
LogActivation("Synchronization manager is not initialized. Exiting.");
|
||||
Application.Current.Exit();
|
||||
return;
|
||||
}
|
||||
|
||||
var processor = Services.GetRequiredService<IWinoRequestProcessor>();
|
||||
var notificationBuilder = Services.GetRequiredService<INotificationBuilder>();
|
||||
|
||||
// Prepare the requests for the action.
|
||||
var requests = await processor.PrepareRequestsAsync(package);
|
||||
|
||||
if (requests != null && requests.Any())
|
||||
{
|
||||
// Group requests by account ID (usually just one account).
|
||||
var accountIds = requests.GroupBy(a => a.Item.AssignedAccount.Id);
|
||||
|
||||
foreach (var accountGroup in accountIds)
|
||||
{
|
||||
var accountId = accountGroup.Key;
|
||||
|
||||
// Queue all requests for this account.
|
||||
foreach (var request in accountGroup)
|
||||
{
|
||||
await _synchronizationManager.QueueRequestAsync(request, accountId, triggerSynchronization: false);
|
||||
}
|
||||
|
||||
// Create synchronization options to execute the queued requests.
|
||||
var syncOptions = new MailSynchronizationOptions()
|
||||
{
|
||||
AccountId = accountId,
|
||||
Type = MailSynchronizationType.ExecuteRequests
|
||||
};
|
||||
|
||||
LogActivation($"Executing synchronization for account {accountId}...");
|
||||
|
||||
// Wait for synchronization to complete before exiting.
|
||||
var syncResult = await _synchronizationManager.SynchronizeMailAsync(syncOptions);
|
||||
|
||||
LogActivation($"Toast action {action} completed. Sync result: {syncResult.CompletedState}");
|
||||
}
|
||||
|
||||
await notificationBuilder.UpdateTaskbarIconBadgeAsync();
|
||||
}
|
||||
|
||||
LogActivation("Toast action handling complete. Exiting app.");
|
||||
|
||||
// Exit the app after synchronization is complete.
|
||||
Application.Current.Exit();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the main window and activates it.
|
||||
/// </summary>
|
||||
private async Task CreateAndActivateWindow(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
|
||||
{
|
||||
CreateWindow(args);
|
||||
|
||||
// Initialize theme service after window is created.
|
||||
await NewThemeService.InitializeAsync();
|
||||
|
||||
MainWindow.Activate();
|
||||
LogActivation("Window created and activated.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the main window without activating it.
|
||||
/// Used for both normal launch and startup task launch (tray only).
|
||||
/// </summary>
|
||||
private void CreateWindow(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
|
||||
{
|
||||
LogActivation("Creating main window.");
|
||||
|
||||
MainWindow = new ShellWindow();
|
||||
|
||||
var nativeAppService = Services.GetRequiredService<INativeAppService>();
|
||||
nativeAppService.GetCoreWindowHwnd = () => WinRT.Interop.WindowNative.GetWindowHandle(MainWindow);
|
||||
|
||||
await InitializeServicesAsync();
|
||||
|
||||
if (MainWindow is not IWinoShellWindow shellWindow) throw new ArgumentException("MainWindow must implement IWinoShellWindow");
|
||||
|
||||
bool isStartupTaskLaunch = IsStartupTaskLaunch();
|
||||
if (MainWindow is not IWinoShellWindow shellWindow)
|
||||
throw new ArgumentException("MainWindow must implement IWinoShellWindow");
|
||||
|
||||
shellWindow.HandleAppActivation(args);
|
||||
|
||||
// Do not actiavate window if launched from startup task. Keep running in the system tray.
|
||||
if (!isStartupTaskLaunch)
|
||||
{
|
||||
MainWindow.Activate();
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterRecipients()
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
|
||||
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
|
||||
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||
IgnorableNamespaces="uap rescap com">
|
||||
IgnorableNamespaces="uap rescap com desktop">
|
||||
|
||||
<!-- Publisher Cache Folders -->
|
||||
<Extensions>
|
||||
@@ -46,7 +46,7 @@
|
||||
Executable="$targetnametoken$.exe"
|
||||
EntryPoint="$targetentrypoint$">
|
||||
<uap:VisualElements
|
||||
DisplayName="Wino Mail (Preview)"
|
||||
DisplayName="Wino Mail"
|
||||
Description="Wino.Mail.WinUI"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Assets\Square150x150Logo.png"
|
||||
@@ -70,7 +70,7 @@
|
||||
|
||||
<com:Extension Category="windows.comServer">
|
||||
<com:ComServer>
|
||||
<com:ExeServer Executable="Wino.Mail.WinUI.exe" Arguments="-ToastActivated" DisplayName="Toast activator">
|
||||
<com:ExeServer Executable="Wino.Mail.WinUI.exe" Arguments="----AppNotificationActivated:" DisplayName="Toast activator">
|
||||
<com:Class Id="72c6d2d0-2538-44fe-a1b1-499f47bb1181" DisplayName="Toast activator"/>
|
||||
</com:ExeServer>
|
||||
</com:ComServer>
|
||||
|
||||
Reference in New Issue
Block a user