Toast notification navigations and some improvements for list view selection.
This commit is contained in:
@@ -10,7 +10,7 @@ public interface INotificationBuilder
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates toast notifications for new mails.
|
/// Creates toast notifications for new mails.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task CreateNotificationsAsync(Guid inboxFolderId, IEnumerable<MailCopy> newMailItems);
|
Task CreateNotificationsAsync(IEnumerable<MailCopy> newMailItems);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the unread Inbox messages for each account and updates the taskbar icon.
|
/// Gets the unread Inbox messages for each account and updates the taskbar icon.
|
||||||
@@ -18,11 +18,6 @@ public interface INotificationBuilder
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task UpdateTaskbarIconBadgeAsync();
|
Task UpdateTaskbarIconBadgeAsync();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates test notification for test purposes.
|
|
||||||
/// </summary>
|
|
||||||
Task CreateTestNotificationAsync(string title, string message);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes the toast notification for a specific mail by unique id.
|
/// Removes the toast notification for a specific mail by unique id.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -19,9 +19,10 @@ public interface ISynchronizationManager
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes the SynchronizationManager with required dependencies.
|
/// Initializes the SynchronizationManager with required dependencies.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task InitializeAsync(ISynchronizerFactory synchronizerFactory,
|
Task InitializeAsync(ISynchronizerFactory synchronizerFactory,
|
||||||
IImapTestService imapTestService,
|
IImapTestService imapTestService,
|
||||||
IAccountService accountService,
|
IAccountService accountService,
|
||||||
|
INotificationBuilder notificationBuilder,
|
||||||
IAuthenticationProvider authenticationProvider);
|
IAuthenticationProvider authenticationProvider);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -32,7 +33,7 @@ public interface ISynchronizationManager
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Starts a new mail synchronization for the given account.
|
/// Starts a new mail synchronization for the given account.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<MailSynchronizationResult> SynchronizeMailAsync(MailSynchronizationOptions options,
|
Task<MailSynchronizationResult> SynchronizeMailAsync(MailSynchronizationOptions options,
|
||||||
CancellationToken cancellationToken = default);
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -53,25 +54,25 @@ public interface ISynchronizationManager
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles folder synchronization for the given account.
|
/// Handles folder synchronization for the given account.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<MailSynchronizationResult> SynchronizeFoldersAsync(Guid accountId,
|
Task<MailSynchronizationResult> SynchronizeFoldersAsync(Guid accountId,
|
||||||
CancellationToken cancellationToken = default);
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles alias synchronization for the given account.
|
/// Handles alias synchronization for the given account.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<MailSynchronizationResult> SynchronizeAliasesAsync(Guid accountId,
|
Task<MailSynchronizationResult> SynchronizeAliasesAsync(Guid accountId,
|
||||||
CancellationToken cancellationToken = default);
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles profile synchronization for the given account.
|
/// Handles profile synchronization for the given account.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<MailSynchronizationResult> SynchronizeProfileAsync(Guid accountId,
|
Task<MailSynchronizationResult> SynchronizeProfileAsync(Guid accountId,
|
||||||
CancellationToken cancellationToken = default);
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Downloads a MIME message for the given mail item.
|
/// Downloads a MIME message for the given mail item.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<string> DownloadMimeMessageAsync(MailCopy mailItem, Guid accountId,
|
Task<string> DownloadMimeMessageAsync(MailCopy mailItem, Guid accountId,
|
||||||
CancellationToken cancellationToken = default);
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -97,7 +98,7 @@ public interface ISynchronizationManager
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles OAuth authentication for the specified provider.
|
/// Handles OAuth authentication for the specified provider.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<TokenInformationEx> HandleAuthorizationAsync(MailProviderType providerType,
|
Task<TokenInformationEx> HandleAuthorizationAsync(MailProviderType providerType,
|
||||||
MailAccount account = null,
|
MailAccount account = null,
|
||||||
bool proposeCopyAuthorizationURL = false);
|
bool proposeCopyAuthorizationURL = false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ public class ListItemComparer : IComparer<object>
|
|||||||
public int Compare(object x, object y)
|
public int Compare(object x, object y)
|
||||||
{
|
{
|
||||||
if (x is IMailListItemSorting xSorting && y is IMailListItemSorting ySorting)
|
if (x is IMailListItemSorting xSorting && y is IMailListItemSorting ySorting)
|
||||||
return SortByName ? string.Compare(xSorting.SortingName, ySorting.SortingName, StringComparison.OrdinalIgnoreCase) : DateTime.Compare(xSorting.SortingDate, ySorting.SortingDate);
|
return SortByName ? string.Compare(xSorting.SortingName, ySorting.SortingName, StringComparison.OrdinalIgnoreCase) : DateTime.Compare(ySorting.SortingDate, xSorting.SortingDate);
|
||||||
else if (x is MailCopy xMail && y is MailCopy yMail)
|
else if (x is MailCopy xMail && y is MailCopy yMail)
|
||||||
return SortByName ? string.Compare(xMail.FromName, yMail.FromName, StringComparison.OrdinalIgnoreCase) : DateTime.Compare(yMail.CreationDate, xMail.CreationDate);
|
return SortByName ? string.Compare(xMail.FromName, yMail.FromName, StringComparison.OrdinalIgnoreCase) : DateTime.Compare(yMail.CreationDate, xMail.CreationDate);
|
||||||
else if (x is DateTime dateX && y is DateTime dateY)
|
else if (x is DateTime dateX && y is DateTime dateY)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using Microsoft.Toolkit.Uwp.Notifications;
|
using Microsoft.Toolkit.Uwp.Notifications;
|
||||||
@@ -16,8 +15,6 @@ using Wino.Messaging.UI;
|
|||||||
|
|
||||||
namespace Wino.Core.WinUI.Services;
|
namespace Wino.Core.WinUI.Services;
|
||||||
|
|
||||||
// TODO: Refactor this thing. It's garbage.
|
|
||||||
|
|
||||||
public class NotificationBuilder : INotificationBuilder
|
public class NotificationBuilder : INotificationBuilder
|
||||||
{
|
{
|
||||||
private readonly IUnderlyingThemeService _underlyingThemeService;
|
private readonly IUnderlyingThemeService _underlyingThemeService;
|
||||||
@@ -44,12 +41,43 @@ public class NotificationBuilder : INotificationBuilder
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateNotificationsAsync(Guid inboxFolderId, IEnumerable<MailCopy> downloadedMailItems)
|
public async Task CreateNotificationsAsync(IEnumerable<MailCopy> downloadedMailItems)
|
||||||
{
|
{
|
||||||
var mailCount = downloadedMailItems.Count();
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Filter mails to only include Inbox folder items
|
||||||
|
var inboxMailItems = new List<MailCopy>();
|
||||||
|
|
||||||
|
foreach (var item in downloadedMailItems)
|
||||||
|
{
|
||||||
|
var mailItem = await _mailService.GetSingleMailItemAsync(item.UniqueId);
|
||||||
|
|
||||||
|
//if (mailItem == null || mailItem.AssignedFolder == null)
|
||||||
|
// continue;
|
||||||
|
|
||||||
|
//// Only create notifications for Inbox folder mails
|
||||||
|
//if (mailItem.AssignedFolder.SpecialFolderType != SpecialFolderType.Inbox)
|
||||||
|
// continue;
|
||||||
|
|
||||||
|
//// Skip folders with synchronization disabled
|
||||||
|
//if (!mailItem.AssignedFolder.IsSynchronizationEnabled)
|
||||||
|
// continue;
|
||||||
|
|
||||||
|
//// Skip already read mails
|
||||||
|
//if (mailItem.IsRead)
|
||||||
|
//{
|
||||||
|
// RemoveNotification(mailItem.UniqueId);
|
||||||
|
// continue;
|
||||||
|
//}
|
||||||
|
|
||||||
|
inboxMailItems.Add(mailItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
var mailCount = inboxMailItems.Count;
|
||||||
|
|
||||||
|
if (mailCount == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
// If there are more than 3 mails, just display 1 general toast.
|
// If there are more than 3 mails, just display 1 general toast.
|
||||||
if (mailCount > 3)
|
if (mailCount > 3)
|
||||||
{
|
{
|
||||||
@@ -69,66 +97,9 @@ public class NotificationBuilder : INotificationBuilder
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var validItems = new List<MailCopy>();
|
foreach (var mailItem in inboxMailItems)
|
||||||
|
|
||||||
// Fetch mails again to fill up assigned folder data and latest statuses.
|
|
||||||
// They've been marked as read by executing synchronizer tasks until inital sync finishes.
|
|
||||||
|
|
||||||
foreach (var item in downloadedMailItems)
|
|
||||||
{
|
{
|
||||||
var mailItem = await _mailService.GetSingleMailItemAsync(item.UniqueId);
|
await CreateSingleNotificationAsync(mailItem);
|
||||||
|
|
||||||
if (mailItem != null && mailItem.AssignedFolder != null)
|
|
||||||
{
|
|
||||||
validItems.Add(mailItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var mailItem in validItems)
|
|
||||||
{
|
|
||||||
if (mailItem.IsRead)
|
|
||||||
{
|
|
||||||
// Remove the notification for a specific mail if it exists
|
|
||||||
ToastNotificationManager.History.Remove(mailItem.UniqueId.ToString());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var builder = new ToastContentBuilder();
|
|
||||||
builder.SetToastScenario(ToastScenario.Default);
|
|
||||||
|
|
||||||
var avatarThumbnail = await _thumbnailService.GetThumbnailAsync(mailItem.FromAddress, awaitLoad: true);
|
|
||||||
if (!string.IsNullOrEmpty(avatarThumbnail))
|
|
||||||
{
|
|
||||||
var tempFile = await Windows.Storage.ApplicationData.Current.TemporaryFolder.CreateFileAsync($"{Guid.NewGuid()}.png", Windows.Storage.CreationCollisionOption.ReplaceExisting);
|
|
||||||
await using (var stream = await tempFile.OpenStreamForWriteAsync())
|
|
||||||
{
|
|
||||||
var bytes = Convert.FromBase64String(avatarThumbnail);
|
|
||||||
await stream.WriteAsync(bytes);
|
|
||||||
}
|
|
||||||
builder.AddAppLogoOverride(new Uri($"ms-appdata:///temp/{tempFile.Name}"), hintCrop: ToastGenericAppLogoCrop.Default);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Override system notification timetamp with received date of the mail.
|
|
||||||
// It may create confusion for some users, but still it's the truth...
|
|
||||||
builder.AddCustomTimeStamp(mailItem.CreationDate.ToLocalTime());
|
|
||||||
|
|
||||||
builder.AddText(mailItem.FromName);
|
|
||||||
builder.AddText(mailItem.Subject);
|
|
||||||
builder.AddText(mailItem.PreviewText);
|
|
||||||
|
|
||||||
builder.AddArgument(Constants.ToastMailUniqueIdKey, mailItem.UniqueId.ToString());
|
|
||||||
builder.AddArgument(Constants.ToastActionKey, MailOperation.Navigate);
|
|
||||||
|
|
||||||
builder.AddButton(GetMarkAsReadButton(mailItem.UniqueId));
|
|
||||||
builder.AddButton(GetDeleteButton(mailItem.UniqueId));
|
|
||||||
builder.AddButton(GetArchiveButton(mailItem.UniqueId));
|
|
||||||
builder.AddAudio(new ToastAudio()
|
|
||||||
{
|
|
||||||
Src = new Uri("ms-winsoundevent:Notification.Mail")
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use UniqueId as tag to allow removal
|
|
||||||
builder.Show(toast => toast.Tag = mailItem.UniqueId.ToString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await UpdateTaskbarIconBadgeAsync();
|
await UpdateTaskbarIconBadgeAsync();
|
||||||
@@ -140,6 +111,45 @@ public class NotificationBuilder : INotificationBuilder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task CreateSingleNotificationAsync(MailCopy mailItem)
|
||||||
|
{
|
||||||
|
var builder = new ToastContentBuilder();
|
||||||
|
builder.SetToastScenario(ToastScenario.Default);
|
||||||
|
|
||||||
|
var avatarThumbnail = await _thumbnailService.GetThumbnailAsync(mailItem.FromAddress, awaitLoad: true);
|
||||||
|
if (!string.IsNullOrEmpty(avatarThumbnail))
|
||||||
|
{
|
||||||
|
var tempFile = await Windows.Storage.ApplicationData.Current.TemporaryFolder.CreateFileAsync($"{Guid.NewGuid()}.png", Windows.Storage.CreationCollisionOption.ReplaceExisting);
|
||||||
|
await using (var stream = await tempFile.OpenStreamForWriteAsync())
|
||||||
|
{
|
||||||
|
var bytes = Convert.FromBase64String(avatarThumbnail);
|
||||||
|
await stream.WriteAsync(bytes);
|
||||||
|
}
|
||||||
|
builder.AddAppLogoOverride(new Uri($"ms-appdata:///temp/{tempFile.Name}"), hintCrop: ToastGenericAppLogoCrop.Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override system notification timestamp with received date of the mail.
|
||||||
|
builder.AddCustomTimeStamp(mailItem.CreationDate.ToLocalTime());
|
||||||
|
|
||||||
|
builder.AddText(mailItem.FromName);
|
||||||
|
builder.AddText(mailItem.Subject);
|
||||||
|
builder.AddText(mailItem.PreviewText);
|
||||||
|
|
||||||
|
builder.AddArgument(Constants.ToastMailUniqueIdKey, mailItem.UniqueId.ToString());
|
||||||
|
builder.AddArgument(Constants.ToastActionKey, MailOperation.Navigate);
|
||||||
|
|
||||||
|
builder.AddButton(GetMarkAsReadButton(mailItem.UniqueId));
|
||||||
|
builder.AddButton(GetDeleteButton(mailItem.UniqueId));
|
||||||
|
builder.AddButton(GetArchiveButton(mailItem.UniqueId));
|
||||||
|
builder.AddAudio(new ToastAudio()
|
||||||
|
{
|
||||||
|
Src = new Uri("ms-winsoundevent:Notification.Mail")
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use UniqueId as tag to allow removal
|
||||||
|
builder.Show(toast => toast.Tag = mailItem.UniqueId.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
private ToastButton GetDismissButton()
|
private ToastButton GetDismissButton()
|
||||||
=> new ToastButton()
|
=> new ToastButton()
|
||||||
.SetDismissActivation()
|
.SetDismissActivation()
|
||||||
@@ -216,32 +226,6 @@ public class NotificationBuilder : INotificationBuilder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateTestNotificationAsync(string title, string message)
|
|
||||||
{
|
|
||||||
// with args test.
|
|
||||||
//await CreateNotificationsAsync(Guid.Parse("28c3c39b-7147-4de3-b209-949bd19eede6"), new List<IMailItem>()
|
|
||||||
//{
|
|
||||||
// new MailCopy()
|
|
||||||
// {
|
|
||||||
// Subject = "test subject",
|
|
||||||
// PreviewText = "preview html",
|
|
||||||
// CreationDate = DateTime.UtcNow,
|
|
||||||
// FromAddress = "bkaankose@outlook.com",
|
|
||||||
// Id = "AAkALgAAAAAAHYQDEapmEc2byACqAC-EWg0AnMdP0zg8wkS_Ib2Eeh80LAAGq91I3QAA",
|
|
||||||
// }
|
|
||||||
//});
|
|
||||||
|
|
||||||
//var builder = new ToastContentBuilder();
|
|
||||||
//builder.SetToastScenario(ToastScenario.Default);
|
|
||||||
|
|
||||||
//builder.AddText(title);
|
|
||||||
//builder.AddText(message);
|
|
||||||
|
|
||||||
//builder.Show();
|
|
||||||
|
|
||||||
//await Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RemoveNotification(Guid mailUniqueId)
|
public void RemoveNotification(Guid mailUniqueId)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ using Wino.Core.Domain.Interfaces;
|
|||||||
using Wino.Core.Domain.Models.Authentication;
|
using Wino.Core.Domain.Models.Authentication;
|
||||||
using Wino.Core.Domain.Models.Connectivity;
|
using Wino.Core.Domain.Models.Connectivity;
|
||||||
using Wino.Core.Domain.Models.Synchronization;
|
using Wino.Core.Domain.Models.Synchronization;
|
||||||
using Wino.Messaging.Server;
|
|
||||||
|
|
||||||
namespace Wino.Core.Services;
|
namespace Wino.Core.Services;
|
||||||
|
|
||||||
@@ -30,11 +29,12 @@ public class SynchronizationManager : ISynchronizationManager
|
|||||||
private readonly SemaphoreSlim _initializationSemaphore = new(1, 1);
|
private readonly SemaphoreSlim _initializationSemaphore = new(1, 1);
|
||||||
private readonly ILogger _logger = Log.ForContext<SynchronizationManager>();
|
private readonly ILogger _logger = Log.ForContext<SynchronizationManager>();
|
||||||
|
|
||||||
private ISynchronizerFactory _synchronizerFactory;
|
|
||||||
private SynchronizerFactory _concreteSynchronizerFactory;
|
private SynchronizerFactory _concreteSynchronizerFactory;
|
||||||
private IImapTestService _imapTestService;
|
private IImapTestService _imapTestService;
|
||||||
private IAccountService _accountService;
|
private IAccountService _accountService;
|
||||||
private IAuthenticationProvider _authenticationProvider;
|
private IAuthenticationProvider _authenticationProvider;
|
||||||
|
private INotificationBuilder _notificationBuilder;
|
||||||
|
|
||||||
private bool _isInitialized = false;
|
private bool _isInitialized = false;
|
||||||
|
|
||||||
private SynchronizationManager() { }
|
private SynchronizationManager() { }
|
||||||
@@ -47,38 +47,40 @@ public class SynchronizationManager : ISynchronizationManager
|
|||||||
/// <param name="imapTestService">Service for testing IMAP connectivity</param>
|
/// <param name="imapTestService">Service for testing IMAP connectivity</param>
|
||||||
/// <param name="accountService">Service for account operations</param>
|
/// <param name="accountService">Service for account operations</param>
|
||||||
/// <param name="authenticationProvider">Provider for OAuth authentication</param>
|
/// <param name="authenticationProvider">Provider for OAuth authentication</param>
|
||||||
public async Task InitializeAsync(ISynchronizerFactory synchronizerFactory,
|
public async Task InitializeAsync(ISynchronizerFactory synchronizerFactory,
|
||||||
IImapTestService imapTestService,
|
IImapTestService imapTestService,
|
||||||
IAccountService accountService,
|
IAccountService accountService,
|
||||||
|
INotificationBuilder notificationBuilder,
|
||||||
IAuthenticationProvider authenticationProvider)
|
IAuthenticationProvider authenticationProvider)
|
||||||
{
|
{
|
||||||
await _initializationSemaphore.WaitAsync();
|
await _initializationSemaphore.WaitAsync();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_isInitialized) return;
|
if (_isInitialized) return;
|
||||||
|
|
||||||
_synchronizerFactory = synchronizerFactory ?? throw new ArgumentNullException(nameof(synchronizerFactory));
|
|
||||||
_concreteSynchronizerFactory = synchronizerFactory as SynchronizerFactory ?? throw new ArgumentException("SynchronizerFactory must be the concrete implementation");
|
_concreteSynchronizerFactory = synchronizerFactory as SynchronizerFactory ?? throw new ArgumentException("SynchronizerFactory must be the concrete implementation");
|
||||||
_imapTestService = imapTestService ?? throw new ArgumentNullException(nameof(imapTestService));
|
_imapTestService = imapTestService ?? throw new ArgumentNullException(nameof(imapTestService));
|
||||||
_accountService = accountService ?? throw new ArgumentNullException(nameof(accountService));
|
_accountService = accountService ?? throw new ArgumentNullException(nameof(accountService));
|
||||||
_authenticationProvider = authenticationProvider ?? throw new ArgumentNullException(nameof(authenticationProvider));
|
_authenticationProvider = authenticationProvider ?? throw new ArgumentNullException(nameof(authenticationProvider));
|
||||||
|
_notificationBuilder = notificationBuilder ?? throw new ArgumentNullException(nameof(notificationBuilder));
|
||||||
|
|
||||||
// Get all accounts and create synchronizers for them
|
// Get all accounts and create synchronizers for them
|
||||||
var accounts = await _accountService.GetAccountsAsync();
|
var accounts = await _accountService.GetAccountsAsync();
|
||||||
|
|
||||||
foreach (var account in accounts)
|
foreach (var account in accounts)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var synchronizer = _concreteSynchronizerFactory.CreateNewSynchronizer(account);
|
var synchronizer = _concreteSynchronizerFactory.CreateNewSynchronizer(account);
|
||||||
_synchronizerCache.TryAdd(account.Id, synchronizer);
|
_synchronizerCache.TryAdd(account.Id, synchronizer);
|
||||||
|
|
||||||
_logger.Information("Created synchronizer for account {AccountName} ({AccountId})",
|
_logger.Information("Created synchronizer for account {AccountName} ({AccountId})",
|
||||||
account.Name, account.Id);
|
account.Name, account.Id);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.Error(ex, "Failed to create synchronizer for account {AccountName} ({AccountId})",
|
_logger.Error(ex, "Failed to create synchronizer for account {AccountName} ({AccountId})",
|
||||||
account.Name, account.Id);
|
account.Name, account.Id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,12 +106,12 @@ public class SynchronizationManager : ISynchronizationManager
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.Information("Testing IMAP connectivity for {Server}:{Port}",
|
_logger.Information("Testing IMAP connectivity for {Server}:{Port}",
|
||||||
serverInformation.IncomingServer,
|
serverInformation.IncomingServer,
|
||||||
serverInformation.IncomingServerPort);
|
serverInformation.IncomingServerPort);
|
||||||
|
|
||||||
await _imapTestService.TestImapConnectionAsync(serverInformation, allowSSLHandshake);
|
await _imapTestService.TestImapConnectionAsync(serverInformation, allowSSLHandshake);
|
||||||
|
|
||||||
_logger.Information("IMAP connectivity test successful");
|
_logger.Information("IMAP connectivity test successful");
|
||||||
return ImapConnectivityTestResults.Success();
|
return ImapConnectivityTestResults.Success();
|
||||||
}
|
}
|
||||||
@@ -117,8 +119,8 @@ public class SynchronizationManager : ISynchronizationManager
|
|||||||
{
|
{
|
||||||
_logger.Warning("IMAP connectivity test requires SSL certificate confirmation");
|
_logger.Warning("IMAP connectivity test requires SSL certificate confirmation");
|
||||||
return ImapConnectivityTestResults.CertificateUIRequired(
|
return ImapConnectivityTestResults.CertificateUIRequired(
|
||||||
sslTestException.Issuer,
|
sslTestException.Issuer,
|
||||||
sslTestException.ExpirationDateString,
|
sslTestException.ExpirationDateString,
|
||||||
sslTestException.ValidFromDateString);
|
sslTestException.ValidFromDateString);
|
||||||
}
|
}
|
||||||
catch (ImapClientPoolException clientPoolException)
|
catch (ImapClientPoolException clientPoolException)
|
||||||
@@ -139,7 +141,7 @@ public class SynchronizationManager : ISynchronizationManager
|
|||||||
/// <param name="options">Mail synchronization options</param>
|
/// <param name="options">Mail synchronization options</param>
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <returns>Synchronization result</returns>
|
/// <returns>Synchronization result</returns>
|
||||||
public async Task<MailSynchronizationResult> SynchronizeMailAsync(MailSynchronizationOptions options,
|
public async Task<MailSynchronizationResult> SynchronizeMailAsync(MailSynchronizationOptions options,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
EnsureInitialized();
|
EnsureInitialized();
|
||||||
@@ -151,16 +153,20 @@ public class SynchronizationManager : ISynchronizationManager
|
|||||||
return MailSynchronizationResult.Failed;
|
return MailSynchronizationResult.Failed;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.Information("Starting mail synchronization for account {AccountId} with type {SyncType}",
|
_logger.Information("Starting mail synchronization for account {AccountId} with type {SyncType}",
|
||||||
options.AccountId, options.Type);
|
options.AccountId, options.Type);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await synchronizer.SynchronizeMailsAsync(options, cancellationToken);
|
var result = await synchronizer.SynchronizeMailsAsync(options, cancellationToken);
|
||||||
|
|
||||||
_logger.Information("Mail synchronization completed for account {AccountId} with state {State}",
|
_logger.Information("Mail synchronization completed for account {AccountId} with state {State}",
|
||||||
options.AccountId, result.CompletedState);
|
options.AccountId, result.CompletedState);
|
||||||
|
|
||||||
|
// Create notifications.
|
||||||
|
if (result.DownloadedMessages?.Any() ?? false)
|
||||||
|
await _notificationBuilder.CreateNotificationsAsync(result.DownloadedMessages);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -181,7 +187,7 @@ public class SynchronizationManager : ISynchronizationManager
|
|||||||
|
|
||||||
if (_synchronizerCache.TryGetValue(accountId, out var synchronizer))
|
if (_synchronizerCache.TryGetValue(accountId, out var synchronizer))
|
||||||
{
|
{
|
||||||
return synchronizer.State == AccountSynchronizerState.Synchronizing ||
|
return synchronizer.State == AccountSynchronizerState.Synchronizing ||
|
||||||
synchronizer.State == AccountSynchronizerState.ExecutingRequests;
|
synchronizer.State == AccountSynchronizerState.ExecutingRequests;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,7 +221,7 @@ public class SynchronizationManager : ISynchronizationManager
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.Debug("Queuing request {RequestType} for account {AccountId}",
|
_logger.Debug("Queuing request {RequestType} for account {AccountId}",
|
||||||
request.GetType().Name, accountId);
|
request.GetType().Name, accountId);
|
||||||
|
|
||||||
synchronizer.QueueRequest(request);
|
synchronizer.QueueRequest(request);
|
||||||
@@ -224,7 +230,7 @@ public class SynchronizationManager : ISynchronizationManager
|
|||||||
{
|
{
|
||||||
// Trigger synchronization to execute the queued request
|
// Trigger synchronization to execute the queued request
|
||||||
_logger.Debug("Triggering synchronization to execute queued request for account {AccountId}", accountId);
|
_logger.Debug("Triggering synchronization to execute queued request for account {AccountId}", accountId);
|
||||||
|
|
||||||
var synchronizationOptions = new MailSynchronizationOptions()
|
var synchronizationOptions = new MailSynchronizationOptions()
|
||||||
{
|
{
|
||||||
AccountId = accountId,
|
AccountId = accountId,
|
||||||
@@ -253,7 +259,7 @@ public class SynchronizationManager : ISynchronizationManager
|
|||||||
/// <param name="accountId">Account ID to synchronize folders for</param>
|
/// <param name="accountId">Account ID to synchronize folders for</param>
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <returns>Synchronization result</returns>
|
/// <returns>Synchronization result</returns>
|
||||||
public async Task<MailSynchronizationResult> SynchronizeFoldersAsync(Guid accountId,
|
public async Task<MailSynchronizationResult> SynchronizeFoldersAsync(Guid accountId,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
EnsureInitialized();
|
EnsureInitialized();
|
||||||
@@ -273,7 +279,7 @@ public class SynchronizationManager : ISynchronizationManager
|
|||||||
/// <param name="accountId">Account ID to synchronize aliases for</param>
|
/// <param name="accountId">Account ID to synchronize aliases for</param>
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <returns>Synchronization result</returns>
|
/// <returns>Synchronization result</returns>
|
||||||
public async Task<MailSynchronizationResult> SynchronizeAliasesAsync(Guid accountId,
|
public async Task<MailSynchronizationResult> SynchronizeAliasesAsync(Guid accountId,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
EnsureInitialized();
|
EnsureInitialized();
|
||||||
@@ -293,7 +299,7 @@ public class SynchronizationManager : ISynchronizationManager
|
|||||||
/// <param name="accountId">Account ID to synchronize profile for</param>
|
/// <param name="accountId">Account ID to synchronize profile for</param>
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <returns>Synchronization result</returns>
|
/// <returns>Synchronization result</returns>
|
||||||
public async Task<MailSynchronizationResult> SynchronizeProfileAsync(Guid accountId,
|
public async Task<MailSynchronizationResult> SynchronizeProfileAsync(Guid accountId,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
EnsureInitialized();
|
EnsureInitialized();
|
||||||
@@ -314,7 +320,7 @@ public class SynchronizationManager : ISynchronizationManager
|
|||||||
/// <param name="accountId">Account ID that owns the mail item</param>
|
/// <param name="accountId">Account ID that owns the mail item</param>
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <returns>Downloaded MIME content path</returns>
|
/// <returns>Downloaded MIME content path</returns>
|
||||||
public async Task<string> DownloadMimeMessageAsync(MailCopy mailItem, Guid accountId,
|
public async Task<string> DownloadMimeMessageAsync(MailCopy mailItem, Guid accountId,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
EnsureInitialized();
|
EnsureInitialized();
|
||||||
@@ -353,15 +359,15 @@ public class SynchronizationManager : ISynchronizationManager
|
|||||||
{
|
{
|
||||||
var synchronizer = _concreteSynchronizerFactory.CreateNewSynchronizer(account);
|
var synchronizer = _concreteSynchronizerFactory.CreateNewSynchronizer(account);
|
||||||
_synchronizerCache.TryAdd(account.Id, synchronizer);
|
_synchronizerCache.TryAdd(account.Id, synchronizer);
|
||||||
|
|
||||||
_logger.Information("Created new synchronizer for account {AccountName} ({AccountId})",
|
_logger.Information("Created new synchronizer for account {AccountName} ({AccountId})",
|
||||||
account.Name, account.Id);
|
account.Name, account.Id);
|
||||||
|
|
||||||
return Task.FromResult(synchronizer);
|
return Task.FromResult(synchronizer);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.Error(ex, "Failed to create synchronizer for account {AccountName} ({AccountId})",
|
_logger.Error(ex, "Failed to create synchronizer for account {AccountName} ({AccountId})",
|
||||||
account.Name, account.Id);
|
account.Name, account.Id);
|
||||||
return Task.FromResult<IWinoSynchronizerBase>(null);
|
return Task.FromResult<IWinoSynchronizerBase>(null);
|
||||||
}
|
}
|
||||||
@@ -434,8 +440,8 @@ public class SynchronizationManager : ISynchronizationManager
|
|||||||
/// <param name="account">Optional account to authenticate (null for initial authentication)</param>
|
/// <param name="account">Optional account to authenticate (null for initial authentication)</param>
|
||||||
/// <param name="proposeCopyAuthorizationURL">Whether to propose copying auth URL for Gmail</param>
|
/// <param name="proposeCopyAuthorizationURL">Whether to propose copying auth URL for Gmail</param>
|
||||||
/// <returns>Token information containing access token and username</returns>
|
/// <returns>Token information containing access token and username</returns>
|
||||||
public async Task<TokenInformationEx> HandleAuthorizationAsync(MailProviderType providerType,
|
public async Task<TokenInformationEx> HandleAuthorizationAsync(MailProviderType providerType,
|
||||||
MailAccount account = null,
|
MailAccount account = null,
|
||||||
bool proposeCopyAuthorizationURL = false)
|
bool proposeCopyAuthorizationURL = false)
|
||||||
{
|
{
|
||||||
EnsureInitialized();
|
EnsureInitialized();
|
||||||
@@ -485,4 +491,4 @@ public class SynchronizationManager : ISynchronizationManager
|
|||||||
throw new InvalidOperationException("SynchronizationManager must be initialized before use. Call InitializeAsync first.");
|
throw new InvalidOperationException("SynchronizationManager must be initialized before use. Call InitializeAsync first.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ using System;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
using Wino.Core.Services;
|
|
||||||
|
|
||||||
namespace Wino.Core.Services;
|
namespace Wino.Core.Services;
|
||||||
|
|
||||||
@@ -24,10 +23,11 @@ public class SynchronizationManagerInitializer : IInitializeAsync
|
|||||||
var imapTestService = _serviceProvider.GetRequiredService<IImapTestService>();
|
var imapTestService = _serviceProvider.GetRequiredService<IImapTestService>();
|
||||||
var accountService = _serviceProvider.GetRequiredService<IAccountService>();
|
var accountService = _serviceProvider.GetRequiredService<IAccountService>();
|
||||||
var authenticationProvider = _serviceProvider.GetRequiredService<IAuthenticationProvider>();
|
var authenticationProvider = _serviceProvider.GetRequiredService<IAuthenticationProvider>();
|
||||||
|
var notificationBuilder = _serviceProvider.GetRequiredService<INotificationBuilder>();
|
||||||
|
|
||||||
// Cast to concrete type to access CreateNewSynchronizer method
|
// Cast to concrete type to access CreateNewSynchronizer method
|
||||||
var concreteSynchronizerFactory = synchronizerFactory as SynchronizerFactory;
|
var concreteSynchronizerFactory = synchronizerFactory as SynchronizerFactory;
|
||||||
|
|
||||||
await SynchronizationManager.Instance.InitializeAsync(concreteSynchronizerFactory, imapTestService, accountService, authenticationProvider);
|
await SynchronizationManager.Instance.InitializeAsync(concreteSynchronizerFactory, imapTestService, accountService, notificationBuilder, authenticationProvider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -217,6 +217,42 @@ public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsC
|
|||||||
return !string.IsNullOrEmpty(threadId) && _threadIdToItemsMap.ContainsKey(threadId);
|
return !string.IsNullOrEmpty(threadId) && _threadIdToItemsMap.ContainsKey(threadId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds a MailItemViewModel by its UniqueId, searching through all items including those inside threads.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="uniqueId">The UniqueId of the mail item to find.</param>
|
||||||
|
/// <returns>The MailItemViewModel if found, otherwise null.</returns>
|
||||||
|
public MailItemViewModel Find(Guid uniqueId)
|
||||||
|
{
|
||||||
|
// First check the cache for fast lookup
|
||||||
|
if (_uniqueIdToMailItemMap.TryGetValue(uniqueId, out var cachedMailItem))
|
||||||
|
{
|
||||||
|
return cachedMailItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not in cache, search through all groups
|
||||||
|
foreach (var group in _mailItemSource)
|
||||||
|
{
|
||||||
|
foreach (var item in group)
|
||||||
|
{
|
||||||
|
if (item is MailItemViewModel mailItem && mailItem.MailCopy.UniqueId == uniqueId)
|
||||||
|
{
|
||||||
|
return mailItem;
|
||||||
|
}
|
||||||
|
else if (item is ThreadMailItemViewModel threadItem)
|
||||||
|
{
|
||||||
|
var foundInThread = threadItem.ThreadEmails.FirstOrDefault(e => e.MailCopy.UniqueId == uniqueId);
|
||||||
|
if (foundInThread != null)
|
||||||
|
{
|
||||||
|
return foundInThread;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task InsertItemInternalAsync(object groupKey, IMailListItem mailItem)
|
private async Task InsertItemInternalAsync(object groupKey, IMailListItem mailItem)
|
||||||
{
|
{
|
||||||
UpdateUniqueIdHashes(mailItem, true);
|
UpdateUniqueIdHashes(mailItem, true);
|
||||||
|
|||||||
@@ -184,7 +184,19 @@ public partial class ThreadMailItemViewModel : ObservableRecipient, IMailListIte
|
|||||||
if (email.MailCopy.ThreadId != _threadId)
|
if (email.MailCopy.ThreadId != _threadId)
|
||||||
throw new ArgumentException($"Email ThreadId '{email.MailCopy.ThreadId}' does not match expander ThreadId '{_threadId}'");
|
throw new ArgumentException($"Email ThreadId '{email.MailCopy.ThreadId}' does not match expander ThreadId '{_threadId}'");
|
||||||
|
|
||||||
ThreadEmails.Add(email);
|
// Insert email in sorted order by CreationDate (newest first, oldest last)
|
||||||
|
var insertIndex = 0;
|
||||||
|
for (int i = 0; i < ThreadEmails.Count; i++)
|
||||||
|
{
|
||||||
|
if (ThreadEmails[i].MailCopy.CreationDate < email.MailCopy.CreationDate)
|
||||||
|
{
|
||||||
|
insertIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
insertIndex = i + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadEmails.Insert(insertIndex, email);
|
||||||
// Reassign to trigger property change notifications
|
// Reassign to trigger property change notifications
|
||||||
ThreadEmails = ThreadEmails;
|
ThreadEmails = ThreadEmails;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
private readonly IAccountService _accountService;
|
private readonly IAccountService _accountService;
|
||||||
private readonly IMailDialogService _mailDialogService;
|
private readonly IMailDialogService _mailDialogService;
|
||||||
private readonly IMailService _mailService;
|
private readonly IMailService _mailService;
|
||||||
|
private readonly INotificationBuilder _notificationBuilder;
|
||||||
private readonly IFolderService _folderService;
|
private readonly IFolderService _folderService;
|
||||||
private readonly IContextMenuItemService _contextMenuItemService;
|
private readonly IContextMenuItemService _contextMenuItemService;
|
||||||
private readonly IWinoRequestDelegator _winoRequestDelegator;
|
private readonly IWinoRequestDelegator _winoRequestDelegator;
|
||||||
@@ -155,6 +156,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
IMailDialogService mailDialogService,
|
IMailDialogService mailDialogService,
|
||||||
IMailService mailService,
|
IMailService mailService,
|
||||||
IStatePersistanceService statePersistenceService,
|
IStatePersistanceService statePersistenceService,
|
||||||
|
INotificationBuilder notificationBuilder,
|
||||||
IFolderService folderService,
|
IFolderService folderService,
|
||||||
IContextMenuItemService contextMenuItemService,
|
IContextMenuItemService contextMenuItemService,
|
||||||
IWinoRequestDelegator winoRequestDelegator,
|
IWinoRequestDelegator winoRequestDelegator,
|
||||||
@@ -175,6 +177,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
PreferencesService = preferencesService;
|
PreferencesService = preferencesService;
|
||||||
ThemeService = themeService;
|
ThemeService = themeService;
|
||||||
StatePersistenceService = statePersistenceService;
|
StatePersistenceService = statePersistenceService;
|
||||||
|
_notificationBuilder = notificationBuilder;
|
||||||
NavigationService = navigationService;
|
NavigationService = navigationService;
|
||||||
|
|
||||||
SelectedFilterOption = FilterOptions[0];
|
SelectedFilterOption = FilterOptions[0];
|
||||||
@@ -468,6 +471,10 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void SyncFolder()
|
private void SyncFolder()
|
||||||
{
|
{
|
||||||
|
var mails = MailCollection.SelectedItems;
|
||||||
|
_notificationBuilder.CreateNotificationsAsync(mails.Select(a => a.MailCopy));
|
||||||
|
|
||||||
|
return;
|
||||||
if (!CanSynchronize) return;
|
if (!CanSynchronize) return;
|
||||||
|
|
||||||
// Only synchronize listed folders.
|
// Only synchronize listed folders.
|
||||||
@@ -710,7 +717,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
await MailCollection.RemoveAsync(removedMail);
|
await MailCollection.RemoveAsync(removedMail);
|
||||||
|
|
||||||
if (nextItem != null)
|
if (nextItem != null)
|
||||||
WeakReferenceMessenger.Default.Send(new SelectMailItemContainerEvent(nextItem, ScrollToItem: true));
|
WeakReferenceMessenger.Default.Send(new SelectMailItemContainerEvent(nextItem.UniqueId, ScrollToItem: true));
|
||||||
else if (isDeletedMailSelected)
|
else if (isDeletedMailSelected)
|
||||||
{
|
{
|
||||||
// There are no next item to select, but we removed the last item which was selected.
|
// There are no next item to select, but we removed the last item which was selected.
|
||||||
@@ -1022,30 +1029,9 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
|
|
||||||
void IRecipient<MailItemNavigationRequested>.Receive(MailItemNavigationRequested message)
|
void IRecipient<MailItemNavigationRequested>.Receive(MailItemNavigationRequested message)
|
||||||
{
|
{
|
||||||
Debug.WriteLine($"Mail item navigation requested");
|
// TODO: Remove this.
|
||||||
// Find mail item and add to selected items.
|
|
||||||
|
|
||||||
MailItemViewModel navigatingMailItem = null;
|
WeakReferenceMessenger.Default.Send(new SelectMailItemContainerEvent(message.UniqueMailId, message.ScrollToItem));
|
||||||
ThreadMailItemViewModel threadMailItemViewModel = null;
|
|
||||||
|
|
||||||
for (int i = 0; i < 3; i++)
|
|
||||||
{
|
|
||||||
var mailContainer = MailCollection.GetMailItemContainer(message.UniqueMailId);
|
|
||||||
|
|
||||||
if (mailContainer != null)
|
|
||||||
{
|
|
||||||
navigatingMailItem = mailContainer.ItemViewModel;
|
|
||||||
threadMailItemViewModel = mailContainer.ThreadViewModel;
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (threadMailItemViewModel != null)
|
|
||||||
threadMailItemViewModel.IsThreadExpanded = true;
|
|
||||||
|
|
||||||
if (navigatingMailItem != null)
|
|
||||||
WeakReferenceMessenger.Default.Send(new SelectMailItemContainerEvent(navigatingMailItem, message.ScrollToItem));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using Wino.Mail.ViewModels.Data;
|
using System;
|
||||||
|
|
||||||
namespace Wino.Mail.ViewModels.Messages;
|
namespace Wino.Mail.ViewModels.Messages;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// When listing view model manipulated the selected mail container in the UI.
|
/// When listing view model manipulated the selected mail container in the UI.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record SelectMailItemContainerEvent(MailItemViewModel SelectedMailViewModel, bool ScrollToItem = false);
|
public record SelectMailItemContainerEvent(Guid MailUniqueId, bool ScrollToItem = false);
|
||||||
|
|||||||
@@ -2,13 +2,16 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Toolkit.Uwp.Notifications;
|
||||||
using Microsoft.Windows.AppLifecycle;
|
using Microsoft.Windows.AppLifecycle;
|
||||||
|
using Wino.Core.Domain;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
using Wino.Core.WinUI;
|
using Wino.Core.WinUI;
|
||||||
using Wino.Core.WinUI.Interfaces;
|
using Wino.Core.WinUI.Interfaces;
|
||||||
using Wino.Mail.Services;
|
using Wino.Mail.Services;
|
||||||
using Wino.Mail.ViewModels;
|
using Wino.Mail.ViewModels;
|
||||||
|
using Wino.Messaging.Client.Accounts;
|
||||||
using Wino.Messaging.Server;
|
using Wino.Messaging.Server;
|
||||||
using Wino.Services;
|
using Wino.Services;
|
||||||
namespace Wino.Mail.WinUI;
|
namespace Wino.Mail.WinUI;
|
||||||
@@ -22,10 +25,45 @@ public partial class App : WinoApplication, IRecipient<NewMailSynchronizationReq
|
|||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||||
|
ToastNotificationManagerCompat.OnActivated += ToastActivationHandler;
|
||||||
|
|
||||||
RegisterRecipients();
|
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
|
#region Dependency Injection
|
||||||
|
|
||||||
|
|
||||||
@@ -80,9 +118,13 @@ public partial class App : WinoApplication, IRecipient<NewMailSynchronizationReq
|
|||||||
}
|
}
|
||||||
|
|
||||||
private bool IsStartupTaskLaunch() => AppInstance.GetCurrent().GetActivatedEventArgs()?.Kind == ExtendedActivationKind.StartupTask;
|
private bool IsStartupTaskLaunch() => AppInstance.GetCurrent().GetActivatedEventArgs()?.Kind == ExtendedActivationKind.StartupTask;
|
||||||
|
public bool IsAppRunning() => MainWindow != null;
|
||||||
|
|
||||||
protected override async void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
|
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.
|
// TODO: Check app relaunch mutex before loading anything.
|
||||||
|
|
||||||
// Initialize NewThemeService first to get backdrop settings before creating window
|
// 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)
|
if (innerListViewControl != null)
|
||||||
{
|
{
|
||||||
innerListView = innerListViewControl;
|
innerListView = innerListViewControl;
|
||||||
// TODO: What if it wasn't realized in the thread?
|
|
||||||
itemContainer = innerListViewControl.ContainerFromItem(mailItemViewModel) as WinoMailItemViewModelListViewItem;
|
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;
|
break;
|
||||||
|
|||||||
@@ -5,8 +5,10 @@
|
|||||||
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
|
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
|
||||||
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
||||||
xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5"
|
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"
|
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||||
IgnorableNamespaces="uap rescap">
|
IgnorableNamespaces="uap rescap com">
|
||||||
|
|
||||||
<!-- Publisher Cache Folders -->
|
<!-- Publisher Cache Folders -->
|
||||||
<Extensions>
|
<Extensions>
|
||||||
@@ -60,6 +62,19 @@
|
|||||||
Enabled="true"
|
Enabled="true"
|
||||||
DisplayName="Wino Startup Service" />
|
DisplayName="Wino Startup Service" />
|
||||||
</uap5:Extension>
|
</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 -->
|
<!-- Protocol activation: mailto -->
|
||||||
<uap:Extension Category="windows.protocol">
|
<uap:Extension Category="windows.protocol">
|
||||||
|
|||||||
@@ -313,13 +313,18 @@ public sealed partial class MailListPage : MailListPageAbstract,
|
|||||||
|
|
||||||
public async void Receive(SelectMailItemContainerEvent message)
|
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 () =>
|
await DispatcherQueue.EnqueueAsync(async () =>
|
||||||
{
|
{
|
||||||
// MailListView.ClearSelections(message.SelectedMailViewModel, true);
|
var collectionContainer = await MailListView.GetItemContainersAsync(item);
|
||||||
|
|
||||||
var collectionContainer = await MailListView.GetItemContainersAsync(message.SelectedMailViewModel);
|
|
||||||
|
|
||||||
if (collectionContainer.Item1 == null && collectionContainer.Item2 == null) return;
|
if (collectionContainer.Item1 == null && collectionContainer.Item2 == null) return;
|
||||||
|
|
||||||
@@ -347,11 +352,7 @@ public sealed partial class MailListPage : MailListPageAbstract,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var listView = collectionContainer.Item3 ?? MailListView;
|
await WinoClickItemInternalAsync(item, true);
|
||||||
var mailItemViewModelContainer = collectionContainer.Item1;
|
|
||||||
var threadMailItemViewModelContainer = collectionContainer.Item2;
|
|
||||||
|
|
||||||
await WinoClickItemInternalAsync(listView, collectionContainer.Item1?.Item ?? null);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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;
|
if (clickedItem == null) return;
|
||||||
|
|
||||||
@@ -621,29 +622,40 @@ public sealed partial class MailListPage : MailListPageAbstract,
|
|||||||
if (clickedItem is ThreadMailItemViewModel clickedThread)
|
if (clickedItem is ThreadMailItemViewModel clickedThread)
|
||||||
{
|
{
|
||||||
bool wasThreadSelected = clickedThread.IsSelected;
|
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)
|
// Reset everything first (exclusive selection scenario)
|
||||||
await ViewModel.MailCollection.UnselectAllAsync();
|
await ViewModel.MailCollection.UnselectAllAsync();
|
||||||
await CollapseAllThreadsExceptAsync(clickedThread);
|
await CollapseAllThreadsExceptAsync(clickedThread);
|
||||||
|
|
||||||
if (wasThreadSelected)
|
if (wasThreadSelected && wasThreadExpanded)
|
||||||
{
|
{
|
||||||
// Toggle off -> leave nothing selected (all unselected, thread collapsed)
|
// Toggle off -> leave nothing selected (all unselected, thread collapsed)
|
||||||
clickedThread.IsThreadExpanded = false;
|
clickedThread.IsThreadExpanded = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Select thread + first child only
|
// Select thread header
|
||||||
clickedThread.IsSelected = true;
|
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
|
alreadySelectedChild.IsSelected = true;
|
||||||
foreach (var child in clickedThread.ThreadEmails)
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var firstChild = clickedThread.ThreadEmails.FirstOrDefault();
|
||||||
|
if (firstChild != null)
|
||||||
{
|
{
|
||||||
child.IsSelected = child == firstChild;
|
firstChild.IsSelected = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clickedThread.IsThreadExpanded = true; // Show contents of active thread
|
clickedThread.IsThreadExpanded = true; // Show contents of active thread
|
||||||
}
|
}
|
||||||
else if (clickedItem is MailItemViewModel clickedMail)
|
else if (clickedItem is MailItemViewModel clickedMail)
|
||||||
@@ -695,6 +707,13 @@ public sealed partial class MailListPage : MailListPageAbstract,
|
|||||||
await ViewModel.MailCollection.UnselectAllAsync();
|
await ViewModel.MailCollection.UnselectAllAsync();
|
||||||
await ViewModel.MailCollection.CollapseAllThreadsAsync();
|
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)
|
if (!wasSelected)
|
||||||
{
|
{
|
||||||
clickedMail.IsSelected = true; // Toggle on
|
clickedMail.IsSelected = true; // Toggle on
|
||||||
@@ -706,6 +725,6 @@ public sealed partial class MailListPage : MailListPageAbstract,
|
|||||||
{
|
{
|
||||||
if (sender is not WinoListView listView) return;
|
if (sender is not WinoListView listView) return;
|
||||||
|
|
||||||
await WinoClickItemInternalAsync(listView, e.ClickedItem);
|
await WinoClickItemInternalAsync(e.ClickedItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user