Toast notification navigations and some improvements for list view selection.

This commit is contained in:
Burak Kaan Köse
2025-11-12 15:44:43 +01:00
parent 16e06af76f
commit 777219ab87
14 changed files with 300 additions and 190 deletions
+42
View File
@@ -2,13 +2,16 @@
using System.Text;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Toolkit.Uwp.Notifications;
using Microsoft.Windows.AppLifecycle;
using Wino.Core.Domain;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.WinUI;
using Wino.Core.WinUI.Interfaces;
using Wino.Mail.Services;
using Wino.Mail.ViewModels;
using Wino.Messaging.Client.Accounts;
using Wino.Messaging.Server;
using Wino.Services;
namespace Wino.Mail.WinUI;
@@ -22,10 +25,45 @@ public partial class App : WinoApplication, IRecipient<NewMailSynchronizationReq
InitializeComponent();
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
ToastNotificationManagerCompat.OnActivated += ToastActivationHandler;
RegisterRecipients();
}
private async void ToastActivationHandler(ToastNotificationActivatedEventArgsCompat e)
{
// 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.
var toastArgs = ToastArguments.Parse(e.Argument);
var mailService = Services.GetRequiredService<IMailService>();
var accountService = Services.GetRequiredService<IAccountService>();
if (Guid.TryParse(toastArgs[Constants.ToastMailUniqueIdKey], out Guid mailItemUniqueId))
{
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);
}
if (ToastNotificationManagerCompat.WasCurrentProcessToastActivated())
{
MainWindow.BringToFront();
}
}
#region Dependency Injection
@@ -80,9 +118,13 @@ public partial class App : WinoApplication, IRecipient<NewMailSynchronizationReq
}
private bool IsStartupTaskLaunch() => AppInstance.GetCurrent().GetActivatedEventArgs()?.Kind == ExtendedActivationKind.StartupTask;
public bool IsAppRunning() => MainWindow != null;
protected override async void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
// If it's toast activation, compat will handle it.
if (IsAppRunning()) return;
// TODO: Check app relaunch mutex before loading anything.
// Initialize NewThemeService first to get backdrop settings before creating window
@@ -177,8 +177,22 @@ public partial class WinoListView : Microsoft.UI.Xaml.Controls.ListView
if (innerListViewControl != null)
{
innerListView = innerListViewControl;
// TODO: What if it wasn't realized in the thread?
itemContainer = innerListViewControl.ContainerFromItem(mailItemViewModel) as WinoMailItemViewModelListViewItem;
// Item thread has been found but container is not realized yet.
// This could happen when Sent item passed to navigate for Inbox or vice-versa.
// Ideally, we should select the first UniqueId match in the thread in this case.
if (itemContainer == null)
{
var realThreadItem = innerListViewControl.Items.Cast<MailItemViewModel>().FirstOrDefault(a => a.UniqueId == mailItemViewModel.MailCopy.UniqueId);
if (realThreadItem != null)
{
itemContainer = innerListViewControl.ContainerFromItem(realThreadItem) as WinoMailItemViewModelListViewItem;
}
}
}
}
break;
+16 -1
View File
@@ -5,8 +5,10 @@
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5"
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">
IgnorableNamespaces="uap rescap com">
<!-- Publisher Cache Folders -->
<Extensions>
@@ -60,6 +62,19 @@
Enabled="true"
DisplayName="Wino Startup Service" />
</uap5:Extension>
<!-- App notification activation -->
<desktop:Extension Category="windows.toastNotificationActivation">
<desktop:ToastNotificationActivation ToastActivatorCLSID="72c6d2d0-2538-44fe-a1b1-499f47bb1181" />
</desktop:Extension>
<com:Extension Category="windows.comServer">
<com:ComServer>
<com:ExeServer Executable="Wino.Mail.WinUI.exe" Arguments="-ToastActivated" DisplayName="Toast activator">
<com:Class Id="72c6d2d0-2538-44fe-a1b1-499f47bb1181" DisplayName="Toast activator"/>
</com:ExeServer>
</com:ComServer>
</com:Extension>
<!-- Protocol activation: mailto -->
<uap:Extension Category="windows.protocol">
+37 -18
View File
@@ -313,13 +313,18 @@ public sealed partial class MailListPage : MailListPageAbstract,
public async void Receive(SelectMailItemContainerEvent message)
{
if (message.SelectedMailViewModel == null) return;
if (message.MailUniqueId == Guid.Empty) return;
// Find the item from the collection.
// Folder should be initialized already.
var item = ViewModel.MailCollection.Find(message.MailUniqueId);
if (item == null) return;
await DispatcherQueue.EnqueueAsync(async () =>
{
// MailListView.ClearSelections(message.SelectedMailViewModel, true);
var collectionContainer = await MailListView.GetItemContainersAsync(message.SelectedMailViewModel);
var collectionContainer = await MailListView.GetItemContainersAsync(item);
if (collectionContainer.Item1 == null && collectionContainer.Item2 == null) return;
@@ -347,11 +352,7 @@ public sealed partial class MailListPage : MailListPageAbstract,
}
}
var listView = collectionContainer.Item3 ?? MailListView;
var mailItemViewModelContainer = collectionContainer.Item1;
var threadMailItemViewModelContainer = collectionContainer.Item2;
await WinoClickItemInternalAsync(listView, collectionContainer.Item1?.Item ?? null);
await WinoClickItemInternalAsync(item, true);
});
}
@@ -554,7 +555,7 @@ public sealed partial class MailListPage : MailListPageAbstract,
}
}
private async Task WinoClickItemInternalAsync(WinoListView listView, object? clickedItem)
private async Task WinoClickItemInternalAsync(object? clickedItem, bool selectExpandThread = false)
{
if (clickedItem == null) return;
@@ -621,29 +622,40 @@ public sealed partial class MailListPage : MailListPageAbstract,
if (clickedItem is ThreadMailItemViewModel clickedThread)
{
bool wasThreadSelected = clickedThread.IsSelected;
bool wasThreadExpanded = clickedThread.IsThreadExpanded;
// Check if any child in this thread is already selected (e.g., from notification click)
var alreadySelectedChild = clickedThread.ThreadEmails.FirstOrDefault(e => e.IsSelected);
// Reset everything first (exclusive selection scenario)
await ViewModel.MailCollection.UnselectAllAsync();
await CollapseAllThreadsExceptAsync(clickedThread);
if (wasThreadSelected)
if (wasThreadSelected && wasThreadExpanded)
{
// Toggle off -> leave nothing selected (all unselected, thread collapsed)
clickedThread.IsThreadExpanded = false;
return;
}
// Select thread + first child only
// Select thread header
clickedThread.IsSelected = true;
var firstChild = clickedThread.ThreadEmails.FirstOrDefault();
if (firstChild != null)
// If a child was already selected (e.g., from notification), keep that selection
// Otherwise, select the first child
if (alreadySelectedChild != null)
{
// Ensure only first child selected
foreach (var child in clickedThread.ThreadEmails)
alreadySelectedChild.IsSelected = true;
}
else
{
var firstChild = clickedThread.ThreadEmails.FirstOrDefault();
if (firstChild != null)
{
child.IsSelected = child == firstChild;
firstChild.IsSelected = true;
}
}
clickedThread.IsThreadExpanded = true; // Show contents of active thread
}
else if (clickedItem is MailItemViewModel clickedMail)
@@ -695,6 +707,13 @@ public sealed partial class MailListPage : MailListPageAbstract,
await ViewModel.MailCollection.UnselectAllAsync();
await ViewModel.MailCollection.CollapseAllThreadsAsync();
if (parentThread != null && selectExpandThread)
{
// We're clicking an item inside a thread; select & expand the thread header as well.
parentThread.IsSelected = true;
parentThread.IsThreadExpanded = true;
}
if (!wasSelected)
{
clickedMail.IsSelected = true; // Toggle on
@@ -706,6 +725,6 @@ public sealed partial class MailListPage : MailListPageAbstract,
{
if (sender is not WinoListView listView) return;
await WinoClickItemInternalAsync(listView, e.ClickedItem);
await WinoClickItemInternalAsync(e.ClickedItem);
}
}