diff --git a/Wino.Core.Domain/Entities/Mail/MailCopy.cs b/Wino.Core.Domain/Entities/Mail/MailCopy.cs index b3c1049d..80ba6b26 100644 --- a/Wino.Core.Domain/Entities/Mail/MailCopy.cs +++ b/Wino.Core.Domain/Entities/Mail/MailCopy.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using SQLite; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Models.MailItem; namespace Wino.Core.Domain.Entities.Mail; @@ -11,7 +10,7 @@ namespace Wino.Core.Domain.Entities.Mail; /// Summary of the parsed MIME messages. /// Wino will do non-network operations on this table and others from the original MIME. /// -public class MailCopy : IMailItem +public class MailCopy { /// /// Unique Id of the mail. diff --git a/Wino.Core.Domain/Interfaces/IContextMenuItemService.cs b/Wino.Core.Domain/Interfaces/IContextMenuItemService.cs index c3b1c54d..eee75ad5 100644 --- a/Wino.Core.Domain/Interfaces/IContextMenuItemService.cs +++ b/Wino.Core.Domain/Interfaces/IContextMenuItemService.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; +using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Models.Folders; -using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.Menus; namespace Wino.Core.Domain.Interfaces; @@ -8,6 +8,6 @@ namespace Wino.Core.Domain.Interfaces; public interface IContextMenuItemService { IEnumerable GetFolderContextMenuActions(IBaseFolderMenuItem folderInformation); - IEnumerable GetMailItemContextMenuActions(IEnumerable selectedMailItems); - IEnumerable GetMailItemRenderMenuActions(IMailItem mailItem, bool isDarkEditor); + IEnumerable GetMailItemContextMenuActions(IEnumerable selectedMailItems); + IEnumerable GetMailItemRenderMenuActions(MailCopy mailItem, bool isDarkEditor); } diff --git a/Wino.Core.Domain/Interfaces/IContextMenuProvider.cs b/Wino.Core.Domain/Interfaces/IContextMenuProvider.cs index 7e7ef089..d5b676b4 100644 --- a/Wino.Core.Domain/Interfaces/IContextMenuProvider.cs +++ b/Wino.Core.Domain/Interfaces/IContextMenuProvider.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; +using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Models.Folders; -using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.Menus; namespace Wino.Core.Domain.Interfaces; @@ -18,12 +18,12 @@ public interface IContextMenuProvider /// /// Current folder that asks for the menu items. /// Selected menu items in the given folder. - IEnumerable GetMailItemContextMenuActions(IMailItemFolder folderInformation, IEnumerable selectedMailItems); + IEnumerable GetMailItemContextMenuActions(IMailItemFolder folderInformation, IEnumerable selectedMailItems); /// /// Calculates and returns available mail operations for mail rendering CommandBar. /// /// Rendered mail item. /// Folder that mail item belongs to. - IEnumerable GetMailItemRenderMenuActions(IMailItem mailItem, IMailItemFolder activeFolder, bool isDarkEditor); + IEnumerable GetMailItemRenderMenuActions(MailCopy mailItem, IMailItemFolder activeFolder, bool isDarkEditor); } diff --git a/Wino.Core.Domain/Interfaces/IGmailThreadingStrategy.cs b/Wino.Core.Domain/Interfaces/IGmailThreadingStrategy.cs deleted file mode 100644 index 17ec3b74..00000000 --- a/Wino.Core.Domain/Interfaces/IGmailThreadingStrategy.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Wino.Core.Domain.Interfaces; - -public interface IGmailThreadingStrategy : IThreadingStrategy { } diff --git a/Wino.Core.Domain/Interfaces/IImapThreadingStrategy.cs b/Wino.Core.Domain/Interfaces/IImapThreadingStrategy.cs deleted file mode 100644 index b613cd70..00000000 --- a/Wino.Core.Domain/Interfaces/IImapThreadingStrategy.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Wino.Core.Domain.Interfaces; - -public interface IImapThreadingStrategy : IThreadingStrategy { } diff --git a/Wino.Core.Domain/Interfaces/IMailService.cs b/Wino.Core.Domain/Interfaces/IMailService.cs index 544889ea..e6dfd7aa 100644 --- a/Wino.Core.Domain/Interfaces/IMailService.cs +++ b/Wino.Core.Domain/Interfaces/IMailService.cs @@ -25,7 +25,7 @@ public interface IMailService /// Caution: This method is not safe. Use other overrides. /// Task> GetMailItemsAsync(IEnumerable mailCopyIds); - Task> FetchMailsAsync(MailListInitializationOptions options, CancellationToken cancellationToken = default); + Task> FetchMailsAsync(MailListInitializationOptions options, CancellationToken cancellationToken = default); /// /// Deletes all mail copies for all folders. diff --git a/Wino.Core.Domain/Interfaces/INotificationBuilder.cs b/Wino.Core.Domain/Interfaces/INotificationBuilder.cs index 86961ad7..aa85e8a6 100644 --- a/Wino.Core.Domain/Interfaces/INotificationBuilder.cs +++ b/Wino.Core.Domain/Interfaces/INotificationBuilder.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Wino.Core.Domain.Models.MailItem; +using Wino.Core.Domain.Entities.Mail; namespace Wino.Core.Domain.Interfaces; @@ -10,7 +10,7 @@ public interface INotificationBuilder /// /// Creates toast notifications for new mails. /// - Task CreateNotificationsAsync(Guid inboxFolderId, IEnumerable newMailItems); + Task CreateNotificationsAsync(Guid inboxFolderId, IEnumerable newMailItems); /// /// Gets the unread Inbox messages for each account and updates the taskbar icon. diff --git a/Wino.Core.Domain/Interfaces/IOutlookThreadingStrategy.cs b/Wino.Core.Domain/Interfaces/IOutlookThreadingStrategy.cs deleted file mode 100644 index 03525f6f..00000000 --- a/Wino.Core.Domain/Interfaces/IOutlookThreadingStrategy.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Wino.Core.Domain.Interfaces; - -public interface IOutlookThreadingStrategy : IThreadingStrategy { } diff --git a/Wino.Core.Domain/Interfaces/IThreadingStrategy.cs b/Wino.Core.Domain/Interfaces/IThreadingStrategy.cs deleted file mode 100644 index c27b8e56..00000000 --- a/Wino.Core.Domain/Interfaces/IThreadingStrategy.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Wino.Core.Domain.Entities.Mail; -using Wino.Core.Domain.Models.Folders; -using Wino.Core.Domain.Models.MailItem; - -namespace Wino.Core.Domain.Interfaces; - -public interface IThreadingStrategy -{ - /// - /// Attach thread mails to the list. - /// - /// Original mails. - /// Original mails with thread mails. - Task> ThreadItemsAsync(List items, IMailItemFolder threadingForFolder); - bool ShouldThreadWithItem(IMailItem originalItem, IMailItem targetItem); -} diff --git a/Wino.Core.Domain/Interfaces/IThreadingStrategyProvider.cs b/Wino.Core.Domain/Interfaces/IThreadingStrategyProvider.cs deleted file mode 100644 index cca544f5..00000000 --- a/Wino.Core.Domain/Interfaces/IThreadingStrategyProvider.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Wino.Core.Domain.Enums; - -namespace Wino.Core.Domain.Interfaces; - -public interface IThreadingStrategyProvider -{ - /// - /// Returns corresponding threading strategy that applies to given provider type. - /// - /// Provider type. - IThreadingStrategy GetStrategy(MailProviderType mailProviderType); -} diff --git a/Wino.Core.Domain/Interfaces/IWinoSynchronizerBase.cs b/Wino.Core.Domain/Interfaces/IWinoSynchronizerBase.cs index ba2384b5..1ff8e530 100644 --- a/Wino.Core.Domain/Interfaces/IWinoSynchronizerBase.cs +++ b/Wino.Core.Domain/Interfaces/IWinoSynchronizerBase.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using MailKit; using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Models.Folders; -using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.Synchronization; namespace Wino.Core.Domain.Interfaces; @@ -30,7 +29,7 @@ public interface IWinoSynchronizerBase : IBaseSynchronizer /// Mail item to download from server. /// Optional progress reporting for download operation. /// Cancellation token. - Task DownloadMissingMimeMessageAsync(IMailItem mailItem, ITransferProgress transferProgress, CancellationToken cancellationToken = default); + Task DownloadMissingMimeMessageAsync(MailCopy mailItem, ITransferProgress transferProgress, CancellationToken cancellationToken = default); /// /// 1. Cancel active synchronization. diff --git a/Wino.Core.Domain/Models/Comparers/DateComparer.cs b/Wino.Core.Domain/Models/Comparers/DateComparer.cs deleted file mode 100644 index 7729831c..00000000 --- a/Wino.Core.Domain/Models/Comparers/DateComparer.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using Wino.Core.Domain.Models.MailItem; - -namespace Wino.Core.Domain.Models.Comparers; - -public class DateComparer : IComparer, IEqualityComparer -{ - public int Compare(IMailItem x, IMailItem y) - { - return DateTime.Compare(y.CreationDate, x.CreationDate); - } - - public new bool Equals(object x, object y) - { - if (x is IMailItem firstItem && y is IMailItem secondItem) - { - return firstItem.Equals(secondItem); - } - - return false; - } - - public int GetHashCode(object obj) => (obj as IMailItem).GetHashCode(); - - public DateComparer() - { - - } -} diff --git a/Wino.Core.Domain/Models/Comparers/ListItemComparer.cs b/Wino.Core.Domain/Models/Comparers/ListItemComparer.cs deleted file mode 100644 index 393b9f6b..00000000 --- a/Wino.Core.Domain/Models/Comparers/ListItemComparer.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Collections.Generic; -using Wino.Core.Domain.Models.MailItem; - -namespace Wino.Core.Domain.Models.Comparers; - -public class ListItemComparer : IComparer -{ - public bool SortByName { get; set; } - - public DateComparer DateComparer = new DateComparer(); - public readonly NameComparer NameComparer = new NameComparer(); - - public int Compare(object x, object y) - { - if (x is IMailItem xMail && y is IMailItem yMail) - { - var itemComparer = GetItemComparer(); - - return itemComparer.Compare(xMail, yMail); - } - else if (x is DateTime dateX && y is DateTime dateY) - return DateTime.Compare(dateY, dateX); - else if (x is string stringX && y is string stringY) - return stringY.CompareTo(stringX); - - return 0; - } - - public IComparer GetItemComparer() - { - if (SortByName) - return NameComparer; - else - return DateComparer; - } -} diff --git a/Wino.Core.Domain/Models/Comparers/NameComparer.cs b/Wino.Core.Domain/Models/Comparers/NameComparer.cs deleted file mode 100644 index 9dd03636..00000000 --- a/Wino.Core.Domain/Models/Comparers/NameComparer.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Collections.Generic; -using Wino.Core.Domain.Models.MailItem; - -namespace Wino.Core.Domain.Models.Comparers; - -public class NameComparer : IComparer -{ - public int Compare(IMailItem x, IMailItem y) - { - return string.Compare(x.FromName, y.FromName); - } -} diff --git a/Wino.Core.Domain/Models/MailItem/IMailHashContainer.cs b/Wino.Core.Domain/Models/MailItem/IMailHashContainer.cs deleted file mode 100644 index 52cd64ad..00000000 --- a/Wino.Core.Domain/Models/MailItem/IMailHashContainer.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Wino.Core.Domain.Models.MailItem; - -/// -/// An interface that returns the UniqueId store for IMailItem. -/// For threads, it may be multiple items. -/// For single mails, it'll always be one item. -/// -public interface IMailHashContainer -{ - IEnumerable GetContainingIds(); -} diff --git a/Wino.Core.Domain/Models/MailItem/IMailItem.cs b/Wino.Core.Domain/Models/MailItem/IMailItem.cs deleted file mode 100644 index b5cd99b2..00000000 --- a/Wino.Core.Domain/Models/MailItem/IMailItem.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using Wino.Core.Domain.Entities.Mail; -using Wino.Core.Domain.Entities.Shared; - -namespace Wino.Core.Domain.Models.MailItem; - -/// -/// Interface of simplest representation of a MailCopy. -/// -public interface IMailItem : IMailHashContainer -{ - Guid UniqueId { get; } - string Id { get; } - string Subject { get; } - string ThreadId { get; } - string MessageId { get; } - string References { get; } - string InReplyTo { get; } - string PreviewText { get; } - string FromName { get; } - DateTime CreationDate { get; } - string FromAddress { get; } - bool HasAttachments { get; } - bool IsFlagged { get; } - bool IsFocused { get; } - bool IsRead { get; } - string DraftId { get; } - bool IsDraft { get; } - Guid FileId { get; } - - MailItemFolder AssignedFolder { get; } - MailAccount AssignedAccount { get; } - AccountContact SenderContact { get; } -} diff --git a/Wino.Core.Domain/Models/MailItem/IMailItemThread.cs b/Wino.Core.Domain/Models/MailItem/IMailItemThread.cs deleted file mode 100644 index 5c3cd554..00000000 --- a/Wino.Core.Domain/Models/MailItem/IMailItemThread.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.ObjectModel; - -namespace Wino.Core.Domain.Models.MailItem; - -/// -/// Interface that represents conversation threads. -/// Even though this type has 1 single UI representation most of the time, -/// it can contain multiple IMailItem. -/// -public interface IMailItemThread : IMailItem -{ - ObservableCollection ThreadItems { get; } - IMailItem LatestMailItem { get; } - IMailItem FirstMailItem { get; } -} diff --git a/Wino.Core.Domain/Models/MailItem/MailDragPackage.cs b/Wino.Core.Domain/Models/MailItem/MailDragPackage.cs index e2fb2949..50a4e40e 100644 --- a/Wino.Core.Domain/Models/MailItem/MailDragPackage.cs +++ b/Wino.Core.Domain/Models/MailItem/MailDragPackage.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Wino.Core.Domain.Entities.Mail; namespace Wino.Core.Domain.Models.MailItem; @@ -7,12 +8,12 @@ namespace Wino.Core.Domain.Models.MailItem; /// public class MailDragPackage { - public MailDragPackage(IEnumerable draggingMails) + public MailDragPackage(IEnumerable draggingMails) { DraggingMails = draggingMails; } - public MailDragPackage(IMailItem draggingMail) + public MailDragPackage(MailCopy draggingMail) { DraggingMails = [ @@ -20,5 +21,5 @@ public class MailDragPackage ]; } - public IEnumerable DraggingMails { get; set; } + public IEnumerable DraggingMails { get; set; } } diff --git a/Wino.Core.Domain/Models/MailItem/ThreadMailItem.cs b/Wino.Core.Domain/Models/MailItem/ThreadMailItem.cs deleted file mode 100644 index 1d13f909..00000000 --- a/Wino.Core.Domain/Models/MailItem/ThreadMailItem.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using Wino.Core.Domain.Entities.Mail; -using Wino.Core.Domain.Entities.Shared; - -namespace Wino.Core.Domain.Models.MailItem; - -public class ThreadMailItem : IMailItemThread -{ - // TODO: Ideally this should be SortedList. - public ObservableCollection ThreadItems { get; } = new ObservableCollection(); - - public IMailItem LatestMailItem => ThreadItems.LastOrDefault(); - public IMailItem FirstMailItem => ThreadItems.FirstOrDefault(); - - public bool AddThreadItem(IMailItem item) - { - if (item == null) return false; - - if (ThreadItems.Any(a => a.Id == item.Id)) - { - return false; - } - - if (item != null && item.IsDraft) - { - ThreadItems.Insert(0, item); - return true; - } - - var insertItem = ThreadItems.FirstOrDefault(a => !a.IsDraft && a.CreationDate < item.CreationDate); - - if (insertItem == null) - ThreadItems.Insert(ThreadItems.Count, item); - else - { - var index = ThreadItems.IndexOf(insertItem); - - ThreadItems.Insert(index, item); - } - - return true; - } - - public IEnumerable GetContainingIds() => ThreadItems?.Select(a => a.UniqueId) ?? default; - - #region IMailItem - - public Guid UniqueId => LatestMailItem?.UniqueId ?? Guid.Empty; - public string Id => LatestMailItem?.Id ?? string.Empty; - - // Show subject from last item. - public string Subject => LatestMailItem?.Subject ?? string.Empty; - - public string ThreadId => LatestMailItem?.ThreadId ?? string.Empty; - - public string PreviewText => FirstMailItem?.PreviewText ?? string.Empty; - - public string FromName => LatestMailItem?.FromName ?? string.Empty; - - public string FromAddress => LatestMailItem?.FromAddress ?? string.Empty; - - public bool HasAttachments => ThreadItems.Any(a => a.HasAttachments); - - public bool IsFlagged => ThreadItems.Any(a => a.IsFlagged); - - public bool IsFocused => LatestMailItem?.IsFocused ?? false; - - public bool IsRead => ThreadItems.All(a => a.IsRead); - - public DateTime CreationDate => FirstMailItem?.CreationDate ?? DateTime.MinValue; - - public bool IsDraft => ThreadItems.Any(a => a.IsDraft); - - public string DraftId => string.Empty; - - public string MessageId => LatestMailItem?.MessageId; - - public string References => LatestMailItem?.References ?? string.Empty; - - public string InReplyTo => LatestMailItem?.InReplyTo ?? string.Empty; - - public MailItemFolder AssignedFolder => LatestMailItem?.AssignedFolder; - - public MailAccount AssignedAccount => LatestMailItem?.AssignedAccount; - - public Guid FileId => LatestMailItem?.FileId ?? Guid.Empty; - - public AccountContact SenderContact => LatestMailItem?.SenderContact; - - #endregion -} diff --git a/Wino.Core.Domain/Models/MailItem/ToggleRequestRule.cs b/Wino.Core.Domain/Models/MailItem/ToggleRequestRule.cs index 70045ead..e3ebb453 100644 --- a/Wino.Core.Domain/Models/MailItem/ToggleRequestRule.cs +++ b/Wino.Core.Domain/Models/MailItem/ToggleRequestRule.cs @@ -1,4 +1,5 @@ using System; +using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Enums; namespace Wino.Core.Domain.Models.MailItem; @@ -10,4 +11,4 @@ namespace Wino.Core.Domain.Models.MailItem; /// /// /// -public record ToggleRequestRule(MailOperation SourceAction, MailOperation TargetAction, Func Condition); +public record ToggleRequestRule(MailOperation SourceAction, MailOperation TargetAction, Func Condition); diff --git a/Wino.Core.Domain/Models/Reader/SortingOption.cs b/Wino.Core.Domain/Models/Reader/SortingOption.cs index 09deec80..bb887d1e 100644 --- a/Wino.Core.Domain/Models/Reader/SortingOption.cs +++ b/Wino.Core.Domain/Models/Reader/SortingOption.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; -using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Models.Comparers; -using Wino.Core.Domain.Models.MailItem; +using Wino.Core.Domain.Enums; namespace Wino.Core.Domain.Models.Reader; @@ -9,16 +6,6 @@ public class SortingOption { public SortingOptionType Type { get; set; } public string Title { get; set; } - public IComparer Comparer - { - get - { - if (Type == SortingOptionType.ReceiveDate) - return new DateComparer(); - else - return new NameComparer(); - } - } public SortingOption(string title, SortingOptionType type) { diff --git a/Wino.Core.Domain/Models/Synchronization/MailSynchronizationResult.cs b/Wino.Core.Domain/Models/Synchronization/MailSynchronizationResult.cs index f7280889..bdd05b3b 100644 --- a/Wino.Core.Domain/Models/Synchronization/MailSynchronizationResult.cs +++ b/Wino.Core.Domain/Models/Synchronization/MailSynchronizationResult.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using System.Text.Json.Serialization; +using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Models.Accounts; -using Wino.Core.Domain.Models.MailItem; namespace Wino.Core.Domain.Models.Synchronization; @@ -16,7 +16,7 @@ public class MailSynchronizationResult /// It's ignored in serialization. Client should not react to this. /// [JsonIgnore] - public IEnumerable DownloadedMessages { get; set; } = []; + public IEnumerable DownloadedMessages { get; set; } = []; public ProfileInformation ProfileInformation { get; set; } @@ -25,7 +25,7 @@ public class MailSynchronizationResult public static MailSynchronizationResult Empty => new() { CompletedState = SynchronizationCompletedState.Success }; // Mail synchronization - public static MailSynchronizationResult Completed(IEnumerable downloadedMessages) + public static MailSynchronizationResult Completed(IEnumerable downloadedMessages) => new() { DownloadedMessages = downloadedMessages, diff --git a/Wino.Core.WinUI/Helpers/XamlHelpers.cs b/Wino.Core.WinUI/Helpers/XamlHelpers.cs index 96acbf03..b2318e7d 100644 --- a/Wino.Core.WinUI/Helpers/XamlHelpers.cs +++ b/Wino.Core.WinUI/Helpers/XamlHelpers.cs @@ -12,9 +12,9 @@ using Microsoft.UI.Xaml.Media; using Windows.UI; using Windows.UI.Text; using Wino.Core.Domain; +using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Models.MailItem; using Wino.Core.WinUI.Controls; namespace Wino.Helpers; @@ -104,9 +104,9 @@ public static class XamlHelpers public static Color GetWindowsColorFromHex(string hex) => hex.ToColor(); public static SolidColorBrush GetSolidColorBrushFromHex(string colorHex) => string.IsNullOrEmpty(colorHex) ? new SolidColorBrush(Colors.Transparent) : new SolidColorBrush(colorHex.ToColor()); - public static Visibility IsSelectionModeMultiple(ListViewSelectionMode mode) => mode == ListViewSelectionMode.Multiple ? Visibility.Visible : Visibility.Collapsed; public static FontWeight GetFontWeightBySyncState(bool isSyncing) => isSyncing ? FontWeights.SemiBold : FontWeights.Normal; public static FontWeight GetFontWeightByChildSelectedState(bool isChildSelected) => isChildSelected ? FontWeights.SemiBold : FontWeights.Normal; + public static FontWeight GetFontWeightByReadState(bool isChildSelected) => isChildSelected ? FontWeights.Normal : FontWeights.SemiBold; public static Visibility StringToVisibilityConverter(string value) => string.IsNullOrWhiteSpace(value) ? Visibility.Collapsed : Visibility.Visible; public static Visibility StringToVisibilityReversedConverter(string value) => string.IsNullOrWhiteSpace(value) ? Visibility.Visible : Visibility.Collapsed; public static string GetMailItemDisplaySummaryForListing(bool isDraft, DateTime receivedDate, bool prefer24HourTime) @@ -135,7 +135,7 @@ public static class XamlHelpers // From regular mail header template if (groupObject is DateTime groupedDate) dateObject = groupedDate; - else if (groupObject is IGrouping groupKey) + else if (groupObject is IGrouping groupKey) { // From semantic group header. dateObject = groupKey.Key; diff --git a/Wino.Core.WinUI/Services/NotificationBuilder.cs b/Wino.Core.WinUI/Services/NotificationBuilder.cs index 9609b88a..77bb844d 100644 --- a/Wino.Core.WinUI/Services/NotificationBuilder.cs +++ b/Wino.Core.WinUI/Services/NotificationBuilder.cs @@ -12,7 +12,6 @@ using Wino.Core.Domain; using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.MailItem; using Wino.Messaging.UI; namespace Wino.Core.WinUI.Services; @@ -45,7 +44,7 @@ public class NotificationBuilder : INotificationBuilder }); } - public async Task CreateNotificationsAsync(Guid inboxFolderId, IEnumerable downloadedMailItems) + public async Task CreateNotificationsAsync(Guid inboxFolderId, IEnumerable downloadedMailItems) { var mailCount = downloadedMailItems.Count(); @@ -70,7 +69,7 @@ public class NotificationBuilder : INotificationBuilder } else { - var validItems = new List(); + var validItems = new List(); // 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. @@ -220,17 +219,17 @@ 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() - { - new MailCopy() - { - Subject = "test subject", - PreviewText = "preview html", - CreationDate = DateTime.UtcNow, - FromAddress = "bkaankose@outlook.com", - Id = "AAkALgAAAAAAHYQDEapmEc2byACqAC-EWg0AnMdP0zg8wkS_Ib2Eeh80LAAGq91I3QAA", - } - }); + //await CreateNotificationsAsync(Guid.Parse("28c3c39b-7147-4de3-b209-949bd19eede6"), new List() + //{ + // 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); diff --git a/Wino.Core/Integration/Json/ServerRequestTypeInfoResolver.cs b/Wino.Core/Integration/Json/ServerRequestTypeInfoResolver.cs deleted file mode 100644 index 918b1e60..00000000 --- a/Wino.Core/Integration/Json/ServerRequestTypeInfoResolver.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Text.Json.Serialization.Metadata; -using Wino.Core.Domain.Entities.Mail; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.Folders; -using Wino.Core.Domain.Models.MailItem; -using Wino.Core.Requests.Folder; -using Wino.Core.Requests.Mail; - -namespace Wino.Core.Integration.Json; - -public class ServerRequestTypeInfoResolver : DefaultJsonTypeInfoResolver -{ - public ServerRequestTypeInfoResolver() - { - Modifiers.Add(new System.Action(t => - { - if (t.Type == typeof(IRequestBase)) - { - t.PolymorphismOptions = new() - { - DerivedTypes = - { - new JsonDerivedType(typeof(AlwaysMoveToRequest), nameof(AlwaysMoveToRequest)), - new JsonDerivedType(typeof(ArchiveRequest), nameof(ArchiveRequest)), - new JsonDerivedType(typeof(ChangeFlagRequest), nameof(ChangeFlagRequest)), - new JsonDerivedType(typeof(CreateDraftRequest), nameof(CreateDraftRequest)), - new JsonDerivedType(typeof(DeleteRequest), nameof(DeleteRequest)), - new JsonDerivedType(typeof(EmptyFolderRequest), nameof(EmptyFolderRequest)), - new JsonDerivedType(typeof(MarkFolderAsReadRequest), nameof(MarkFolderAsReadRequest)), - new JsonDerivedType(typeof(MarkReadRequest), nameof(MarkReadRequest)), - new JsonDerivedType(typeof(MoveRequest), nameof(MoveRequest)), - new JsonDerivedType(typeof(MoveToFocusedRequest), nameof(MoveToFocusedRequest)), - new JsonDerivedType(typeof(RenameFolderRequest), nameof(RenameFolderRequest)), - new JsonDerivedType(typeof(SendDraftRequest), nameof(SendDraftRequest)), - } - }; - } - else if (t.Type == typeof(IMailItem)) - { - t.PolymorphismOptions = new JsonPolymorphismOptions() - { - DerivedTypes = - { - new JsonDerivedType(typeof(MailCopy), nameof(MailCopy)), - } - }; - } - else if (t.Type == typeof(IMailItemFolder)) - { - t.PolymorphismOptions = new JsonPolymorphismOptions() - { - DerivedTypes = - { - new JsonDerivedType(typeof(MailItemFolder), nameof(MailItemFolder)), - } - }; - } - })); - } -} diff --git a/Wino.Core/Services/ThreadingStrategyProvider.cs b/Wino.Core/Services/ThreadingStrategyProvider.cs deleted file mode 100644 index 37af968e..00000000 --- a/Wino.Core/Services/ThreadingStrategyProvider.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Interfaces; -using Wino.Services.Threading; - -namespace Wino.Core.Services; - -public class ThreadingStrategyProvider : IThreadingStrategyProvider -{ - private readonly OutlookThreadingStrategy _outlookThreadingStrategy; - private readonly GmailThreadingStrategy _gmailThreadingStrategy; - private readonly ImapThreadingStrategy _imapThreadStrategy; - - public ThreadingStrategyProvider(OutlookThreadingStrategy outlookThreadingStrategy, - GmailThreadingStrategy gmailThreadingStrategy, - ImapThreadingStrategy imapThreadStrategy) - { - _outlookThreadingStrategy = outlookThreadingStrategy; - _gmailThreadingStrategy = gmailThreadingStrategy; - _imapThreadStrategy = imapThreadStrategy; - } - - public IThreadingStrategy GetStrategy(MailProviderType mailProviderType) - { - return mailProviderType switch - { - MailProviderType.Outlook => _outlookThreadingStrategy, - MailProviderType.Gmail => _gmailThreadingStrategy, - _ => _imapThreadStrategy, - }; - } -} diff --git a/Wino.Core/Services/WinoRequestProcessor.cs b/Wino.Core/Services/WinoRequestProcessor.cs index 5eead88c..0055d2d8 100644 --- a/Wino.Core/Services/WinoRequestProcessor.cs +++ b/Wino.Core/Services/WinoRequestProcessor.cs @@ -31,10 +31,10 @@ public class WinoRequestProcessor : IWinoRequestProcessor /// private readonly List _toggleRequestRules = [ - new ToggleRequestRule(MailOperation.MarkAsRead, MailOperation.MarkAsUnread, new System.Func((item) => item.IsRead)), - new ToggleRequestRule(MailOperation.MarkAsUnread, MailOperation.MarkAsRead, new System.Func((item) => !item.IsRead)), - new ToggleRequestRule(MailOperation.SetFlag, MailOperation.ClearFlag, new System.Func((item) => item.IsFlagged)), - new ToggleRequestRule(MailOperation.ClearFlag, MailOperation.SetFlag, new System.Func((item) => !item.IsFlagged)), + new ToggleRequestRule(MailOperation.MarkAsRead, MailOperation.MarkAsUnread, new System.Func((item) => item.IsRead)), + new ToggleRequestRule(MailOperation.MarkAsUnread, MailOperation.MarkAsRead, new System.Func((item) => !item.IsRead)), + new ToggleRequestRule(MailOperation.SetFlag, MailOperation.ClearFlag, new System.Func((item) => item.IsFlagged)), + new ToggleRequestRule(MailOperation.ClearFlag, MailOperation.SetFlag, new System.Func((item) => !item.IsFlagged)), ]; public WinoRequestProcessor(IFolderService folderService, diff --git a/Wino.Core/Synchronizers/GmailSynchronizer.cs b/Wino.Core/Synchronizers/GmailSynchronizer.cs index e1fb9b49..c0d0e851 100644 --- a/Wino.Core/Synchronizers/GmailSynchronizer.cs +++ b/Wino.Core/Synchronizers/GmailSynchronizer.cs @@ -1032,7 +1032,7 @@ public class GmailSynchronizer : WinoSynchronizerMail item that its mime file does not exist on the disk. /// Optional download progress for IMAP synchronizer. /// Cancellation token. - public virtual Task DownloadMissingMimeMessageAsync(IMailItem mailItem, ITransferProgress transferProgress = null, CancellationToken cancellationToken = default) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType())); + public virtual Task DownloadMissingMimeMessageAsync(MailCopy mailItem, ITransferProgress transferProgress = null, CancellationToken cancellationToken = default) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType())); /// /// Performs an online search for the given query text in the given folders. diff --git a/Wino.Mail.ViewModels/Collections/GroupHeaders.cs b/Wino.Mail.ViewModels/Collections/GroupHeaders.cs new file mode 100644 index 00000000..834d9064 --- /dev/null +++ b/Wino.Mail.ViewModels/Collections/GroupHeaders.cs @@ -0,0 +1,79 @@ +using System; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Wino.Mail.ViewModels.Collections; + +/// +/// Base class for group headers in the flat collection +/// +public abstract partial class GroupHeaderBase : ObservableObject +{ + [ObservableProperty] + private int itemCount; + + [ObservableProperty] + private int unreadCount; + + protected GroupHeaderBase(string key, string displayName) + { + Key = key; + DisplayName = displayName; + } + + /// + /// The unique key for this group (used for sorting and identification) + /// + public string Key { get; } + + /// + /// The display name shown in the UI + /// + public string DisplayName { get; } +} + +/// +/// Group header for date-based grouping +/// +public partial class DateGroupHeader : GroupHeaderBase +{ + public DateGroupHeader(DateTime date) : base(date.ToString("yyyy-MM-dd"), FormatDisplayName(date)) + { + Date = date; + } + + /// + /// The date this group represents + /// + public DateTime Date { get; } + + private static string FormatDisplayName(DateTime date) + { + var today = DateTime.Today; + var yesterday = today.AddDays(-1); + + return date.Date switch + { + var d when d == today => "Today", + var d when d == yesterday => "Yesterday", + var d when d >= today.AddDays(-7) => date.ToString("dddd"), // This week + var d when d.Year == today.Year => date.ToString("MMMM dd"), // This year + _ => date.ToString("MMMM dd, yyyy") // Other years + }; + } +} + +/// +/// Group header for sender name-based grouping +/// +public partial class SenderGroupHeader : GroupHeaderBase +{ + public SenderGroupHeader(string senderName) : base(senderName, senderName) + { + SenderName = senderName; + } + + /// + /// The sender name this group represents + /// + public string SenderName { get; } +} diff --git a/Wino.Mail.ViewModels/Collections/GroupedEmailCollection.cs b/Wino.Mail.ViewModels/Collections/GroupedEmailCollection.cs new file mode 100644 index 00000000..72b5bb6e --- /dev/null +++ b/Wino.Mail.ViewModels/Collections/GroupedEmailCollection.cs @@ -0,0 +1,1070 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Messaging; +using CommunityToolkit.Mvvm.Messaging.Messages; +using Wino.Mail.ViewModels.Data; + +namespace Wino.Mail.ViewModels.Collections; + +/// +/// Grouping options for emails +/// +public enum EmailGroupingType +{ + ByFromName, + ByDate +} + +/// +/// Sorting options for emails within groups +/// +public enum EmailSortDirection +{ + Ascending, + Descending +} + +/// +/// Collection that automatically groups MailItemViewModels with ThreadMailItemViewModels in a flat structure for ItemsView. +/// All emails are in the same flat list with proper selection support. Thread emails are placed consecutively after their expander. +/// +public partial class GroupedEmailCollection : ObservableObject, IRecipient>, IDisposable +{ + private readonly ObservableCollection _sourceItems = []; + private readonly Dictionary _groupHeaders = []; + private readonly Dictionary _groupHeaderIndexCache = []; + private readonly Dictionary> _groupItems = []; + private readonly Dictionary _threadExpanders = []; + private bool _disposed; + private bool _isUpdating; + + [ObservableProperty] + private EmailGroupingType groupingType = EmailGroupingType.ByDate; + + [ObservableProperty] + private EmailSortDirection sortDirection = EmailSortDirection.Descending; + + public GroupedEmailCollection() + { + // Create a flat collection for ItemsView with headers, expanders and emails mixed + Items = []; + + // Subscribe to source collection changes to update grouping + _sourceItems.CollectionChanged += OnSourceItemsChanged; + + // Register for PropertyChanged messages + WeakReferenceMessenger.Default.Register>(this); + + RefreshGrouping(); + } + + /// + /// Flat observable collection containing group headers, thread expanders, and email items for ItemsView binding. + /// Structure: GroupHeader -> [ThreadExpander -> ThreadEmail1, ThreadEmail2, ...] -> StandaloneEmail1 -> StandaloneEmail2 + /// + public ObservableCollection Items { get; } + + /// + /// Total number of emails across all groups + /// + public int TotalCount => _sourceItems.Count; + + /// + /// Total number of unread emails across all groups + /// + public int TotalUnreadCount => _sourceItems.Count(e => e.MailCopy?.IsRead == false); + + /// + /// Gets all email items across all groups as a flat collection + /// + public IEnumerable AllItems => _sourceItems; + + /// + /// Gets all currently selected email items. + /// Includes: + /// - Standalone mail items where IsSelected=true + /// - Mail items inside threads where the mail item's IsSelected=true (regardless of thread expansion) + /// - All mail items inside a thread where the thread's IsSelected=true + /// + public IEnumerable SelectedItems + { + get + { + var selectedItems = new List(); + + // Add selected standalone emails (not in threads) + selectedItems.AddRange(_sourceItems.Where(e => e.IsSelected && !e.IsDisplayedInThread)); + + // Process thread expanders + foreach (var expander in _threadExpanders.Values) + { + if (expander.IsSelected) + { + // If thread is selected, add all emails in the thread + selectedItems.AddRange(expander.ThreadEmails); + } + else + { + // If thread is not selected, add only individually selected emails within the thread + selectedItems.AddRange(expander.ThreadEmails.Where(e => e.IsSelected)); + } + } + + return selectedItems; + } + } + + /// + /// Gets the count of all currently selected email items. + /// Counts: + /// - Standalone mail items where IsSelected=true + /// - Mail items inside threads where the mail item's IsSelected=true (regardless of thread expansion) + /// - All mail items inside a thread where the thread's IsSelected=true + /// + public int SelectedItemsCount => SelectedItems.Count(); + + /// + /// Gets the number of visible email items (excluding group headers). + /// For threads, counts the expander as 1 if collapsed, or all thread emails if expanded. + /// + public int Count + { + get + { + int count = 0; + + foreach (var item in Items) + { + switch (item) + { + case GroupHeaderBase: + // Skip group headers + break; + case ThreadMailItemViewModel thread: + count += thread.ThreadEmails.Count; + break; + case MailItemViewModel: + count += 1; + break; + } + } + return count; + } + } + + /// + /// Handles PropertyChanged messages for thread expansion + /// + public void Receive(PropertyChangedMessage message) + { + // Only handle IsThreadExpanded property changes from ThreadMailItemViewModel + if (_isUpdating) + return; + + if (message.PropertyName == nameof(ThreadMailItemViewModel.IsThreadExpanded) && message.Sender is ThreadMailItemViewModel expander) + { + HandleThreadExpansion(expander); + } + } + + private void HandleThreadExpansion(ThreadMailItemViewModel expander) + { + _isUpdating = true; + try + { + var expanderIndex = Items.IndexOf(expander); + if (expanderIndex == -1) + return; + + if (expander.IsThreadExpanded) + { + // Add thread emails after the expander + var insertIndex = expanderIndex + 1; + var sortedThreadEmails = SortDirection == EmailSortDirection.Descending + ? expander.ThreadEmails.OrderByDescending(e => e.MailCopy.CreationDate).ToList() + : expander.ThreadEmails.OrderBy(e => e.MailCopy?.CreationDate).ToList(); + + foreach (var email in sortedThreadEmails) + { + Items.Insert(insertIndex, email); + insertIndex++; + } + + UpdateHeaderIndicesAfterInsertion(expanderIndex + 1, expander.EmailCount); + } + else + { + // Remove thread emails from UI + foreach (var email in expander.ThreadEmails.ToList()) + { + var emailIndex = Items.IndexOf(email); + if (emailIndex >= 0) + { + var sourceItem = _sourceItems.FirstOrDefault(a => a == email); + + Items.RemoveAt(emailIndex); + UpdateHeaderIndicesAfterRemoval(emailIndex); + } + } + } + } + finally + { + _isUpdating = false; + } + } + + /// + /// Adds an email to the collection which will automatically group it. + /// If an email with the same ThreadId exists, adds it to the existing thread or creates a new thread. + /// + public void AddEmail(MailItemViewModel email) + { + if (email?.MailCopy == null) + return; + + _isUpdating = true; + try + { + // Check if this email belongs to a thread + if (!string.IsNullOrEmpty(email.MailCopy.ThreadId)) + { + // Look for existing emails with the same ThreadId + var existingThreadEmails = _sourceItems + .Where(e => e.MailCopy?.ThreadId == email.MailCopy.ThreadId) + .ToList(); + + var existingExpander = _threadExpanders.GetValueOrDefault(email.MailCopy.ThreadId); + + if (existingThreadEmails.Any() || existingExpander != null) + { + // Add to existing thread + if (existingExpander == null) + { + // Create thread expander for the first time (existing emails become part of thread) + existingExpander = new ThreadMailItemViewModel(email.MailCopy.ThreadId); + _threadExpanders[email.MailCopy.ThreadId] = existingExpander; + + // Remove existing standalone emails from UI and add them to the thread + foreach (var existingEmail in existingThreadEmails) + { + RemoveEmailFromUI(existingEmail); + existingExpander.AddEmail(existingEmail); + existingEmail.IsDisplayedInThread = true; + } + } + + // Add the new email to the thread + existingExpander.AddEmail(email); + email.IsDisplayedInThread = true; + + // Add to source collection + var insertIndex = FindInsertionIndex(email); + _sourceItems.Insert(insertIndex, email); + + // Add thread expander and all emails to UI in correct positions + RefreshThreadInUI(existingExpander); + } + else + { + // First email with this ThreadId - treat as standalone for now + email.IsDisplayedInThread = false; + var insertIndex = FindInsertionIndex(email); + _sourceItems.Insert(insertIndex, email); + AddEmailToUI(email); + } + } + else + { + // No ThreadId - standalone email + email.IsDisplayedInThread = false; + var insertIndex = FindInsertionIndex(email); + _sourceItems.Insert(insertIndex, email); + AddEmailToUI(email); + } + + OnPropertyChanged(nameof(TotalCount)); + OnPropertyChanged(nameof(TotalUnreadCount)); + } + finally + { + _isUpdating = false; + } + } + + /// + /// Removes an email from the collection. + /// If the email is part of a thread and removing it would leave only 1 item in the thread, + /// the thread is converted back to a single email. + /// + public void RemoveEmail(MailItemViewModel email) + { + if (email?.MailCopy == null) + return; + + _isUpdating = true; + try + { + var threadId = email.MailCopy.ThreadId; + + // Remove from source collection + if (!_sourceItems.Remove(email)) + return; // Email not found + + if (!string.IsNullOrEmpty(threadId) && _threadExpanders.TryGetValue(threadId, out var expander)) + { + // Remove from thread + expander.RemoveEmail(email); + email.IsDisplayedInThread = false; + + // Remove email from UI + RemoveEmailFromUI(email); + + // Check if thread now has only 1 email - convert back to standalone + if (expander.EmailCount == 1) + { + var remainingEmail = expander.ThreadEmails.First(); + + // Remove thread expander and remaining email from UI + RemoveThreadFromUI(expander); + + // Set remaining email as no longer displayed in thread + remainingEmail.IsDisplayedInThread = false; + + // Remove thread expander tracking + _threadExpanders.Remove(threadId); + expander.Dispose(); + + // Add remaining email as standalone + AddEmailToUI(remainingEmail); + } + else if (expander.EmailCount == 0) + { + // Thread is empty - remove completely + RemoveThreadFromUI(expander); + _threadExpanders.Remove(threadId); + expander.Dispose(); + } + else + { + // Thread still has multiple emails - refresh its position + RefreshThreadInUI(expander); + } + } + else + { + // Standalone email + email.IsDisplayedInThread = false; + RemoveEmailFromUI(email); + } + + // Update group headers + UpdateGroupAfterChanges(); + + OnPropertyChanged(nameof(TotalCount)); + OnPropertyChanged(nameof(TotalUnreadCount)); + } + finally + { + _isUpdating = false; + } + } + + /// + /// Adds multiple emails to the collection efficiently using bulk operations + /// + public void AddEmails(IEnumerable emails) + { + var emailList = emails.Where(e => e?.MailCopy != null).ToList(); + if (!emailList.Any()) + return; + + _isUpdating = true; + try + { + // For bulk loading, add to source and refresh + foreach (var email in emailList) + { + var insertIndex = FindInsertionIndex(email); + _sourceItems.Insert(insertIndex, email); + } + + RefreshGrouping(); + + OnPropertyChanged(nameof(TotalCount)); + OnPropertyChanged(nameof(TotalUnreadCount)); + } + finally + { + _isUpdating = false; + } + } + + /// + /// Clears all emails, threads, and headers + /// + public void Clear() + { + _isUpdating = true; + try + { + // Reset IsDisplayedInThread for all emails before clearing + foreach (var email in _sourceItems) + { + email.IsDisplayedInThread = false; + } + + // Dispose all thread expanders + foreach (var expander in _threadExpanders.Values) + { + expander.Dispose(); + } + + _sourceItems.Clear(); + Items.Clear(); + _groupHeaders.Clear(); + _groupHeaderIndexCache.Clear(); + _groupItems.Clear(); + _threadExpanders.Clear(); + + OnPropertyChanged(nameof(TotalCount)); + OnPropertyChanged(nameof(TotalUnreadCount)); + } + finally + { + _isUpdating = false; + } + } + + /// + /// Changes the grouping type and rebuilds the collection + /// + public void ChangeGrouping(EmailGroupingType newGroupingType, EmailSortDirection newSortDirection = EmailSortDirection.Descending) + { + if (GroupingType == newGroupingType && SortDirection == newSortDirection) + return; + + GroupingType = newGroupingType; + SortDirection = newSortDirection; + RefreshGrouping(); + } + + /// + /// Manually refreshes the grouping (useful after bulk operations) + /// + public void RefreshGrouping() + { + _isUpdating = true; + try + { + // Clear UI items but preserve source and expanders + Items.Clear(); + _groupHeaders.Clear(); + _groupHeaderIndexCache.Clear(); + _groupItems.Clear(); + + if (!_sourceItems.Any()) + return; + + // Rebuild thread expanders based on current emails + RebuildThreadExpanders(); + + // Group all items (standalone emails and thread expanders) by criteria + var allItems = new List(); + + // Add standalone emails (emails without threads or not in any expander) + var standaloneEmails = _sourceItems + .Where(e => string.IsNullOrEmpty(e.MailCopy?.ThreadId) || + !_threadExpanders.ContainsKey(e.MailCopy.ThreadId)) + .ToList(); + + allItems.AddRange(standaloneEmails.Cast()); + allItems.AddRange(_threadExpanders.Values.Cast()); + + // Group by criteria + var groupedItems = allItems + .GroupBy(item => GetGroupKeyForItem(item)) + .OrderBy(g => g.Key, GetGroupComparer()); + + var currentIndex = 0; + + // Process each group + foreach (var group in groupedItems) + { + // Create group header + var groupHeader = CreateGroupHeader(group.Key); + _groupHeaders[group.Key] = groupHeader; + _groupHeaderIndexCache[group.Key] = currentIndex; + + // Sort items within the group + var sortedGroupItems = SortDirection == EmailSortDirection.Descending + ? group.OrderByDescending(GetEffectiveDate).ToList() + : group.OrderBy(GetEffectiveDate).ToList(); + + _groupItems[group.Key] = sortedGroupItems; + + // Add header to flat collection + Items.Add(groupHeader); + currentIndex++; + + // Add all items in this group to flat collection + foreach (var item in sortedGroupItems) + { + if (item is ThreadMailItemViewModel expander) + { + // Add expander + Items.Add(expander); + currentIndex++; + + // Only add thread emails if the thread is expanded + if (expander.IsThreadExpanded) + { + var sortedThreadEmails = SortDirection == EmailSortDirection.Descending + ? expander.ThreadEmails.OrderByDescending(e => e.MailCopy?.CreationDate).ToList() + : expander.ThreadEmails.OrderBy(e => e.MailCopy?.CreationDate).ToList(); + + foreach (var threadEmail in sortedThreadEmails) + { + Items.Add(threadEmail); + currentIndex++; + } + } + } + else if (item is MailItemViewModel email) + { + // Add standalone email + Items.Add(email); + currentIndex++; + } + } + } + + // Update group header counts + UpdateAllGroupHeaderCounts(); + } + finally + { + _isUpdating = false; + } + } + + /// + /// Rebuilds thread expanders based on current source emails + /// + private void RebuildThreadExpanders() + { + // Group emails by ThreadId + var threadGroups = _sourceItems + .Where(e => !string.IsNullOrEmpty(e.MailCopy?.ThreadId)) + .GroupBy(e => e.MailCopy!.ThreadId!) + .Where(g => g.Count() >= 2) // Only create threads with 2+ emails + .ToList(); + + // Remove expanders for threads that no longer have 2+ emails + var expandersToRemove = _threadExpanders.Keys + .Where(threadId => !threadGroups.Any(g => g.Key == threadId)) + .ToList(); + + foreach (var threadId in expandersToRemove) + { + // Set emails back to not displayed in thread before removing expander + foreach (var email in _threadExpanders[threadId].ThreadEmails) + { + email.IsDisplayedInThread = false; + } + + _threadExpanders[threadId].Dispose(); + _threadExpanders.Remove(threadId); + } + + // Create or update expanders for threads with 2+ emails + foreach (var threadGroup in threadGroups) + { + if (!_threadExpanders.TryGetValue(threadGroup.Key, out var threadExpander)) + { + threadExpander = new ThreadMailItemViewModel(threadGroup.Key); + _threadExpanders[threadGroup.Key] = threadExpander; + } + + // Clear and re-add emails to ensure consistency + var currentEmails = threadExpander.ThreadEmails.ToList(); + foreach (var email in currentEmails) + { + threadExpander.RemoveEmail(email); + email.IsDisplayedInThread = false; + } + + foreach (var email in threadGroup) + { + threadExpander.AddEmail(email); + email.IsDisplayedInThread = true; + } + } + + // Set standalone emails to not displayed in thread + var standaloneEmails = _sourceItems + .Where(e => string.IsNullOrEmpty(e.MailCopy?.ThreadId) || + !_threadExpanders.ContainsKey(e.MailCopy.ThreadId)) + .ToList(); + + foreach (var email in standaloneEmails) + { + email.IsDisplayedInThread = false; + } + } + + private void RefreshThreadInUI(ThreadMailItemViewModel expander) + { + // Remove thread completely from UI + RemoveThreadFromUI(expander); + + // Find correct position for thread expander based on latest email + var groupKey = GetGroupKeyForItem(expander); + AddThreadToUI(expander, groupKey); + } + + private void AddThreadToUI(ThreadMailItemViewModel expander, string groupKey) + { + var groupHeader = GetOrCreateGroupHeader(groupKey); + var headerIndex = _groupHeaderIndexCache.GetValueOrDefault(groupKey, -1); + + if (headerIndex == -1) + { + // New group - add header, expander, and thread emails (if expanded) + var insertPosition = FindGroupInsertionPosition(groupKey); + + Items.Insert(insertPosition, groupHeader); + Items.Insert(insertPosition + 1, expander); + + var currentIndex = insertPosition + 2; + var totalInserted = 2 // header + expander + ; + + // Only add thread emails if the thread is expanded + if (expander.IsThreadExpanded) + { + var sortedThreadEmails = SortDirection == EmailSortDirection.Descending + ? expander.ThreadEmails.OrderByDescending(e => e.MailCopy?.CreationDate).ToList() + : expander.ThreadEmails.OrderBy(e => e.MailCopy?.CreationDate).ToList(); + + foreach (var email in sortedThreadEmails) + { + Items.Insert(currentIndex, email); + currentIndex++; + totalInserted++; + } + } + + UpdateHeaderIndicesAfterInsertion(insertPosition, totalInserted); + _groupHeaderIndexCache[groupKey] = insertPosition; + } + else + { + // Existing group - find correct position within group + var groupEndIndex = FindGroupEndIndex(headerIndex); + var insertIndex = FindItemInsertionIndexInGroup(expander, headerIndex, groupEndIndex); + + // Insert expander + Items.Insert(insertIndex, expander); + var currentIndex = insertIndex + 1; + var totalInserted = 1 // expander + ; + + // Only insert thread emails if expanded + if (expander.IsThreadExpanded) + { + var sortedThreadEmails = SortDirection == EmailSortDirection.Descending + ? expander.ThreadEmails.OrderByDescending(e => e.MailCopy?.CreationDate).ToList() + : expander.ThreadEmails.OrderBy(e => e.MailCopy?.CreationDate).ToList(); + + foreach (var email in sortedThreadEmails) + { + Items.Insert(currentIndex, email); + currentIndex++; + totalInserted++; + } + } + + UpdateHeaderIndicesAfterInsertion(insertIndex, totalInserted); + } + + UpdateGroupHeaderCounts(groupKey, groupHeader); + } + + private void RemoveThreadFromUI(ThreadMailItemViewModel expander) + { + // Remove expander + var expanderIndex = Items.IndexOf(expander); + if (expanderIndex >= 0) + { + Items.RemoveAt(expanderIndex); + UpdateHeaderIndicesAfterRemoval(expanderIndex); + } + + // Remove all thread emails (whether expanded or not) + foreach (var email in expander.ThreadEmails.ToList()) + { + var emailIndex = Items.IndexOf(email); + if (emailIndex >= 0) + { + Items.RemoveAt(emailIndex); + UpdateHeaderIndicesAfterRemoval(emailIndex); + } + } + } + + private void RemoveEmailFromUI(MailItemViewModel email) + { + var itemIndex = Items.IndexOf(email); + if (itemIndex >= 0) + { + Items.RemoveAt(itemIndex); + UpdateHeaderIndicesAfterRemoval(itemIndex); + } + } + + private void AddEmailToUI(MailItemViewModel email) + { + var groupKey = GetGroupKey(email); + var groupHeader = GetOrCreateGroupHeader(groupKey); + var headerIndex = _groupHeaderIndexCache.GetValueOrDefault(groupKey, -1); + + if (headerIndex == -1) + { + // New group + var insertPosition = FindGroupInsertionPosition(groupKey); + Items.Insert(insertPosition, groupHeader); + Items.Insert(insertPosition + 1, email); + + UpdateHeaderIndicesAfterInsertion(insertPosition, 2); + _groupHeaderIndexCache[groupKey] = insertPosition; + } + else + { + // Existing group + var groupEndIndex = FindGroupEndIndex(headerIndex); + var insertIndex = FindItemInsertionIndexInGroup(email, headerIndex, groupEndIndex); + Items.Insert(insertIndex, email); + + UpdateHeaderIndicesAfterInsertion(insertIndex); + } + + UpdateGroupHeaderCounts(groupKey, groupHeader); + } + + #region Helper Methods + + private string GetGroupKeyForItem(object item) + { + return item switch + { + MailItemViewModel email => GetGroupKey(email), + ThreadMailItemViewModel expander => GetGroupKey(expander.ThreadEmails.OrderByDescending(e => e.MailCopy?.CreationDate).First()), + _ => "Default" + }; + } + + private string GetGroupKey(MailItemViewModel email) + { + return GroupingType switch + { + EmailGroupingType.ByFromName => email.FromName ?? "Unknown Sender", + EmailGroupingType.ByDate => email.MailCopy?.CreationDate.ToString("yyyy-MM-dd") ?? DateTime.Today.ToString("yyyy-MM-dd"), + _ => "Default" + }; + } + + private DateTime GetEffectiveDate(object item) + { + return item switch + { + MailItemViewModel email => email.MailCopy?.CreationDate ?? DateTime.MinValue, + ThreadMailItemViewModel expander => expander.LatestEmailDate, + _ => DateTime.MinValue + }; + } + + private int FindInsertionIndex(MailItemViewModel email) + { + var createdAt = email.MailCopy!.CreationDate; + int left = 0, right = _sourceItems.Count; + + while (left < right) + { + int mid = (left + right) / 2; + var comparison = createdAt.CompareTo(_sourceItems[mid].MailCopy?.CreationDate ?? DateTime.MinValue); + + if (SortDirection == EmailSortDirection.Descending) + comparison = -comparison; + + if (comparison < 0) + right = mid; + else + left = mid + 1; + } + + return left; + } + + private GroupHeaderBase GetOrCreateGroupHeader(string groupKey) + { + if (!_groupHeaders.TryGetValue(groupKey, out var groupHeader)) + { + groupHeader = CreateGroupHeader(groupKey); + _groupHeaders[groupKey] = groupHeader; + _groupItems[groupKey] = []; + } + return groupHeader; + } + + private GroupHeaderBase CreateGroupHeader(string groupKey) + { + return GroupingType switch + { + EmailGroupingType.ByFromName => new SenderGroupHeader(groupKey), + EmailGroupingType.ByDate when DateTime.TryParse(groupKey, out var date) => new DateGroupHeader(date), + EmailGroupingType.ByDate => new DateGroupHeader(DateTime.Today), + _ => new SenderGroupHeader(groupKey) + }; + } + + private IComparer GetGroupComparer() + { + return GroupingType switch + { + EmailGroupingType.ByFromName => SortDirection == EmailSortDirection.Descending + ? StringComparer.OrdinalIgnoreCase.Reverse() + : StringComparer.OrdinalIgnoreCase, + EmailGroupingType.ByDate => SortDirection == EmailSortDirection.Descending + ? CreateDateComparer(descending: true) + : CreateDateComparer(descending: false), + _ => StringComparer.Ordinal + }; + } + + private static IComparer CreateDateComparer(bool descending) + { + return Comparer.Create((x, y) => + { + var dateX = DateTime.TryParse(x, out var dx) ? dx : DateTime.MinValue; + var dateY = DateTime.TryParse(y, out var dy) ? dy : DateTime.MinValue; + + var result = dateX.CompareTo(dateY); + return descending ? -result : result; + }); + } + + private int FindGroupInsertionPosition(string groupKey) + { + var comparer = GetGroupComparer(); + + if (_groupHeaderIndexCache.Count == 0) + return 0; + + var sortedGroups = _groupHeaderIndexCache.Keys.OrderBy(k => k, comparer).ToList(); + var insertPosition = 0; + + for (int i = 0; i < sortedGroups.Count; i++) + { + var existingGroupKey = sortedGroups[i]; + var comparison = comparer.Compare(groupKey, existingGroupKey); + + if (comparison < 0) + { + insertPosition = _groupHeaderIndexCache[existingGroupKey]; + break; + } + else if (i == sortedGroups.Count - 1) + { + var lastGroupHeaderIndex = _groupHeaderIndexCache[existingGroupKey]; + var lastGroupItemCount = _groupItems[existingGroupKey].Count; + insertPosition = lastGroupHeaderIndex + 1 + lastGroupItemCount; + } + } + + return insertPosition; + } + + private int FindGroupEndIndex(int headerIndex) + { + var groupKey = string.Empty; + foreach (var kvp in _groupHeaderIndexCache) + { + if (kvp.Value == headerIndex) + { + groupKey = kvp.Key; + break; + } + } + + return headerIndex + 1 + _groupItems.GetValueOrDefault(groupKey, []).Count; + } + + private int FindItemInsertionIndexInGroup(object item, int groupStartIndex, int groupEndIndex) + { + var itemDate = GetEffectiveDate(item); + + for (int i = groupStartIndex + 1; i < groupEndIndex; i++) + { + var existingItem = Items[i]; + var existingDate = GetEffectiveDate(existingItem); + + var comparison = itemDate.CompareTo(existingDate); + if (SortDirection == EmailSortDirection.Descending) + comparison = -comparison; + + if (comparison < 0) + return i; + } + + return groupEndIndex; + } + + private void UpdateHeaderIndicesAfterInsertion(int insertIndex, int itemCount = 1) + { + var keysToUpdate = _groupHeaderIndexCache + .Where(kvp => kvp.Value > insertIndex) + .Select(kvp => kvp.Key) + .ToList(); + + foreach (var key in keysToUpdate) + { + _groupHeaderIndexCache[key] += itemCount; + } + } + + private void UpdateHeaderIndicesAfterRemoval(int removeIndex) + { + var keysToUpdate = _groupHeaderIndexCache + .Where(kvp => kvp.Value > removeIndex) + .Select(kvp => kvp.Key) + .ToList(); + + foreach (var key in keysToUpdate) + { + _groupHeaderIndexCache[key]--; + } + } + + private void UpdateAllGroupHeaderCounts() + { + foreach (var (groupKey, groupHeader) in _groupHeaders) + { + UpdateGroupHeaderCounts(groupKey, groupHeader); + } + } + + private void UpdateGroupHeaderCounts(string groupKey, GroupHeaderBase groupHeader) + { + var emailsInGroup = _sourceItems.Where(e => GetGroupKey(e) == groupKey).ToList(); + var expandersInGroup = _threadExpanders.Values + .Where(exp => GetGroupKeyForItem(exp) == groupKey) + .ToList(); + + var totalEmailCount = emailsInGroup.Count; + var unreadCount = emailsInGroup.Count(e => e.MailCopy?.IsRead == false); + + groupHeader.ItemCount = totalEmailCount; + groupHeader.UnreadCount = unreadCount; + } + + private void UpdateGroupAfterChanges() + { + // Update all group header counts and remove empty groups + var groupsToRemove = new List(); + + foreach (var (groupKey, groupHeader) in _groupHeaders.ToList()) + { + UpdateGroupHeaderCounts(groupKey, groupHeader); + + if (groupHeader.ItemCount == 0) + { + groupsToRemove.Add(groupKey); + } + } + + foreach (var groupKey in groupsToRemove) + { + RemoveGroupHeader(groupKey); + } + } + + private void RemoveGroupHeader(string groupKey) + { + if (_groupHeaderIndexCache.TryGetValue(groupKey, out var headerIndex)) + { + Items.RemoveAt(headerIndex); + UpdateHeaderIndicesAfterRemoval(headerIndex); + + _groupHeaderIndexCache.Remove(groupKey); + _groupHeaders.Remove(groupKey); + _groupItems.Remove(groupKey); + } + } + + private void OnSourceItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (!_isUpdating) + { + RefreshGrouping(); + } + } + + #endregion + + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + + if (disposing) + { + _sourceItems.CollectionChanged -= OnSourceItemsChanged; + + // Unregister from messenger + WeakReferenceMessenger.Default.Unregister>(this); + + // Reset IsDisplayedInThread for all emails before disposal + foreach (var email in _sourceItems) + { + email.IsDisplayedInThread = false; + } + + // Dispose all thread expanders + foreach (var expander in _threadExpanders.Values) + { + expander.Dispose(); + } + + _sourceItems.Clear(); + Items.Clear(); + _groupHeaders.Clear(); + _groupHeaderIndexCache.Clear(); + _groupItems.Clear(); + _threadExpanders.Clear(); + } + + _disposed = true; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } +} + +/// +/// Extension method to reverse IComparer for descending sorts +/// +internal static class ComparerExtensions +{ + public static IComparer Reverse(this IComparer comparer) + { + return Comparer.Create((x, y) => comparer.Compare(y, x)); + } +} diff --git a/Wino.Mail.ViewModels/Collections/ThreadingManager.cs b/Wino.Mail.ViewModels/Collections/ThreadingManager.cs deleted file mode 100644 index 471cf9af..00000000 --- a/Wino.Mail.ViewModels/Collections/ThreadingManager.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Wino.Core.Domain.Entities.Mail; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.MailItem; - -namespace Wino.Mail.ViewModels.Collections; - -internal class ThreadingManager -{ - private readonly IThreadingStrategyProvider _threadingStrategyProvider; - - public ThreadingManager(IThreadingStrategyProvider threadingStrategyProvider) - { - _threadingStrategyProvider = threadingStrategyProvider; - } - - public bool ShouldThread(MailCopy newItem, IMailItem existingItem) - { - if (_threadingStrategyProvider == null) return false; - - var strategy = _threadingStrategyProvider.GetStrategy(newItem.AssignedAccount.ProviderType); - return strategy?.ShouldThreadWithItem(newItem, existingItem) ?? false; - } - - public ThreadMailItem CreateNewThread(IMailItem existingItem, MailCopy newItem) - { - var thread = new ThreadMailItem(); - thread.AddThreadItem(existingItem); - thread.AddThreadItem(newItem); - return thread; - } -} diff --git a/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs b/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs deleted file mode 100644 index 459a4867..00000000 --- a/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs +++ /dev/null @@ -1,514 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.Collections; -using Serilog; -using Wino.Core.Domain.Entities.Mail; -using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.Comparers; -using Wino.Core.Domain.Models.MailItem; -using Wino.Mail.ViewModels.Data; - -namespace Wino.Mail.ViewModels.Collections; - -public class WinoMailCollection -{ - // We cache each mail copy id for faster access on updates. - // If the item provider here for update or removal doesn't exist here - // we can ignore the operation. - - public HashSet MailCopyIdHashSet = []; - - public event EventHandler MailItemRemoved; - - private ListItemComparer listComparer = new ListItemComparer(); - - private readonly ObservableGroupedCollection _mailItemSource = new ObservableGroupedCollection(); - - public ReadOnlyObservableGroupedCollection MailItems { get; } - - /// - /// Property that defines how the item sorting should be done in the collection. - /// - public SortingOptionType SortingType { get; set; } - - /// - /// Threading strategy that will help thread items according to the account type. - /// - public IThreadingStrategyProvider ThreadingStrategyProvider { get; set; } - - /// - /// Automatically deletes single mail items after the delete operation or thread->single transition. - /// This is useful when reply draft is discarded in the thread. Only enabled for Draft folder for now. - /// - public bool PruneSingleNonDraftItems { get; set; } - - public int Count => _mailItemSource.Count; - - public IDispatcher CoreDispatcher { get; set; } - - private readonly ThreadingManager _threadingManager; - - public WinoMailCollection(IThreadingStrategyProvider threadingStrategyProvider) - { - _threadingManager = new ThreadingManager(threadingStrategyProvider); - MailItems = new ReadOnlyObservableGroupedCollection(_mailItemSource); - } - - public void Clear() => _mailItemSource.Clear(); - - private object GetGroupingKey(IMailItem mailItem) - { - if (SortingType == SortingOptionType.ReceiveDate) - return mailItem.CreationDate.ToLocalTime().Date; - else - return mailItem.FromName; - } - - private void UpdateUniqueIdHashes(IMailHashContainer itemContainer, bool isAdd) - { - foreach (var item in itemContainer.GetContainingIds()) - { - if (isAdd) - { - MailCopyIdHashSet.Add(item); - } - else - { - MailCopyIdHashSet.Remove(item); - } - } - } - - private void InsertItemInternal(object groupKey, IMailItem mailItem) - { - UpdateUniqueIdHashes(mailItem, true); - - if (mailItem is MailCopy mailCopy) - { - _mailItemSource.InsertItem(groupKey, listComparer, new MailItemViewModel(mailCopy), listComparer.GetItemComparer()); - } - else if (mailItem is ThreadMailItem threadMailItem) - { - _mailItemSource.InsertItem(groupKey, listComparer, new ThreadMailItemViewModel(threadMailItem), listComparer.GetItemComparer()); - } - else - { - _mailItemSource.InsertItem(groupKey, listComparer, mailItem, listComparer.GetItemComparer()); - } - } - - private void RemoveItemInternal(ObservableGroup group, IMailItem mailItem) - { - UpdateUniqueIdHashes(mailItem, false); - - MailItemRemoved?.Invoke(this, mailItem); - - group.Remove(mailItem); - - if (group.Count == 0) - { - _mailItemSource.RemoveGroup(group.Key); - } - } - - private async Task HandleThreadingAsync(ObservableGroup group, IMailItem item, MailCopy addedItem) - { - if (item is ThreadMailItemViewModel threadViewModel) - { - await HandleExistingThreadAsync(group, threadViewModel, addedItem); - } - else - { - await HandleNewThreadAsync(group, item, addedItem); - } - } - - private async Task HandleExistingThreadAsync(ObservableGroup group, ThreadMailItemViewModel threadViewModel, MailCopy addedItem) - { - var existingGroupKey = GetGroupingKey(threadViewModel); - - await ExecuteUIThread(() => { threadViewModel.AddMailItemViewModel(addedItem); }); - - var newGroupKey = GetGroupingKey(threadViewModel); - - if (!existingGroupKey.Equals(newGroupKey)) - { - await MoveThreadToNewGroupAsync(group, threadViewModel, newGroupKey); - } - else - { - await ExecuteUIThread(() => { threadViewModel.NotifyPropertyChanges(); }); - } - - UpdateUniqueIdHashes(addedItem, true); - } - - private async Task HandleNewThreadAsync(ObservableGroup group, IMailItem item, MailCopy addedItem) - { - if (item.Id == addedItem.Id) - { - await UpdateExistingItemAsync(item, addedItem); - } - else - { - await CreateNewThreadAsync(group, item, addedItem); - } - } - - private async Task MoveThreadToNewGroupAsync(ObservableGroup currentGroup, ThreadMailItemViewModel threadViewModel, object newGroupKey) - { - var mailThreadItems = threadViewModel.GetThreadMailItem(); - - await ExecuteUIThread(() => - { - RemoveItemInternal(currentGroup, threadViewModel); - InsertItemInternal(newGroupKey, new ThreadMailItemViewModel(mailThreadItems)); - }); - } - - private async Task CreateNewThreadAsync(ObservableGroup group, IMailItem item, MailCopy addedItem) - { - var threadMailItem = _threadingManager.CreateNewThread(item, addedItem); - var newGroupKey = GetGroupingKey(threadMailItem); - - await ExecuteUIThread(() => - { - RemoveItemInternal(group, item); - InsertItemInternal(newGroupKey, threadMailItem); - }); - } - - public async Task AddAsync(MailCopy addedItem) - { - foreach (var group in _mailItemSource) - { - foreach (var item in group) - { - if (_threadingManager.ShouldThread(addedItem, item)) - { - await HandleThreadingAsync(group, item, addedItem); - return; - } - else if (item.Id == addedItem.Id && item is MailItemViewModel itemViewModel) - { - await UpdateExistingItemAsync(itemViewModel, addedItem); - return; - } - } - } - - await AddNewItemAsync(addedItem); - } - - private async Task AddNewItemAsync(MailCopy addedItem) - { - var groupKey = GetGroupingKey(addedItem); - await ExecuteUIThread(() => { InsertItemInternal(groupKey, addedItem); }); - } - - private async Task UpdateExistingItemAsync(IMailItem existingItem, MailCopy updatedItem) - { - if (existingItem is MailItemViewModel itemViewModel) - { - UpdateUniqueIdHashes(itemViewModel, false); - UpdateUniqueIdHashes(updatedItem, true); - - await ExecuteUIThread(() => { itemViewModel.MailCopy = updatedItem; }); - } - } - - public void AddRange(IEnumerable items, bool clearIdCache) - { - if (clearIdCache) - { - MailCopyIdHashSet.Clear(); - } - - var groupedByName = items - .GroupBy(a => GetGroupingKey(a)) - .Select(a => new ObservableGroup(a.Key, a)); - - foreach (var group in groupedByName) - { - // Store all mail copy ids for faster access. - foreach (var item in group) - { - if (item is MailItemViewModel mailCopyItem && !MailCopyIdHashSet.Contains(item.UniqueId)) - { - MailCopyIdHashSet.Add(item.UniqueId); - } - else if (item is ThreadMailItemViewModel threadMailItem) - { - foreach (var mailItem in threadMailItem.ThreadItems) - { - if (!MailCopyIdHashSet.Contains(mailItem.UniqueId)) - { - MailCopyIdHashSet.Add(mailItem.UniqueId); - } - } - } - } - - var existingGroup = _mailItemSource.FirstGroupByKeyOrDefault(group.Key); - - if (existingGroup == null) - { - _mailItemSource.AddGroup(group.Key, group); - } - else - { - foreach (var item in group) - { - existingGroup.Add(item); - } - } - } - } - - public MailItemContainer GetMailItemContainer(Guid uniqueMailId) - { - var groupCount = _mailItemSource.Count; - - for (int i = 0; i < groupCount; i++) - { - var group = _mailItemSource[i]; - - for (int k = 0; k < group.Count; k++) - { - var item = group[k]; - - if (item is MailItemViewModel singleMailItemViewModel && singleMailItemViewModel.UniqueId == uniqueMailId) - return new MailItemContainer(singleMailItemViewModel); - else if (item is ThreadMailItemViewModel threadMailItemViewModel && threadMailItemViewModel.HasUniqueId(uniqueMailId)) - { - var singleItemViewModel = threadMailItemViewModel.GetItemById(uniqueMailId) as MailItemViewModel; - - return new MailItemContainer(singleItemViewModel, threadMailItemViewModel); - } - } - } - - return null; - } - - public void UpdateThumbnails(string address) - { - if (CoreDispatcher == null) return; - - CoreDispatcher.ExecuteOnUIThread(() => - { - foreach (var group in _mailItemSource) - { - foreach (var item in group) - { - if (item is MailItemViewModel mailItemViewModel && - !string.IsNullOrEmpty(mailItemViewModel.MailCopy?.FromAddress) && - mailItemViewModel.MailCopy.FromAddress.Equals(address, StringComparison.OrdinalIgnoreCase)) - { - mailItemViewModel.ThumbnailUpdatedEvent = !mailItemViewModel.ThumbnailUpdatedEvent; - } - } - } - }); - } - - /// - /// Fins the item container that updated mail copy belongs to and updates it. - /// - /// Updated mail copy. - /// - public async Task UpdateMailCopy(MailCopy updatedMailCopy) - { - // This item doesn't exist in the list. - if (!MailCopyIdHashSet.Contains(updatedMailCopy.UniqueId)) - - { - return; - } - - await ExecuteUIThread(() => - { - var itemContainer = GetMailItemContainer(updatedMailCopy.UniqueId); - - if (itemContainer == null) return; - - if (itemContainer.ItemViewModel != null) - { - UpdateUniqueIdHashes(itemContainer.ItemViewModel, false); - } - - if (itemContainer.ItemViewModel != null) - { - itemContainer.ItemViewModel.MailCopy = updatedMailCopy; - } - - UpdateUniqueIdHashes(updatedMailCopy, true); - - // Call thread notifications if possible. - itemContainer.ThreadViewModel?.NotifyPropertyChanges(); - }); - } - - public MailItemViewModel GetNextItem(MailCopy mailCopy) - { - try - { - var groupCount = _mailItemSource.Count; - - for (int i = 0; i < groupCount; i++) - { - var group = _mailItemSource[i]; - - for (int k = 0; k < group.Count; k++) - { - var item = group[k]; - - if (item is MailItemViewModel singleMailItemViewModel && singleMailItemViewModel.UniqueId == mailCopy.UniqueId) - { - if (k + 1 < group.Count) - { - return group[k + 1] as MailItemViewModel; - } - else if (i + 1 < groupCount) - { - return _mailItemSource[i + 1][0] as MailItemViewModel; - } - else - { - return null; - } - } - else if (item is ThreadMailItemViewModel threadMailItemViewModel && threadMailItemViewModel.HasUniqueId(mailCopy.UniqueId)) - { - var singleItemViewModel = threadMailItemViewModel.GetItemById(mailCopy.UniqueId) as MailItemViewModel; - - if (singleItemViewModel == null) return null; - - var singleItemIndex = threadMailItemViewModel.ThreadItems.IndexOf(singleItemViewModel); - - if (singleItemIndex + 1 < threadMailItemViewModel.ThreadItems.Count) - { - return threadMailItemViewModel.ThreadItems[singleItemIndex + 1] as MailItemViewModel; - } - else if (i + 1 < groupCount) - { - return _mailItemSource[i + 1][0] as MailItemViewModel; - } - else - { - return null; - } - } - } - } - - return null; - } - catch (Exception ex) - { - Log.Warning(ex, "Failed to find the next item to select."); - } - - return null; - } - - public async Task RemoveAsync(MailCopy removeItem) - { - // This item doesn't exist in the list. - if (!MailCopyIdHashSet.Contains(removeItem.UniqueId)) return; - - // Check all items for whether this item should be threaded with them. - bool shouldExit = false; - - var groupCount = _mailItemSource.Count; - - for (int i = 0; i < groupCount; i++) - { - if (shouldExit) break; - - var group = _mailItemSource[i]; - - for (int k = 0; k < group.Count; k++) - { - var item = group[k]; - - if (item is ThreadMailItemViewModel threadMailItemViewModel && threadMailItemViewModel.HasUniqueId(removeItem.UniqueId)) - { - var removalItem = threadMailItemViewModel.GetItemById(removeItem.UniqueId); - - if (removalItem == null) return; - - // Threads' Id is equal to the last item they hold. - // We can't do Id check here because that'd remove the whole thread. - - /* Remove item from the thread. - * If thread had 1 item inside: - * -> Remove the thread and insert item as single item. - * If thread had 0 item inside: - * -> Remove the thread. - */ - - var oldGroupKey = GetGroupingKey(threadMailItemViewModel); - - await ExecuteUIThread(() => { threadMailItemViewModel.RemoveCopyItem(removalItem); }); - - if (threadMailItemViewModel.ThreadItems.Count == 1) - { - // Convert to single item. - - var singleViewModel = threadMailItemViewModel.GetSingleItemViewModel(); - var groupKey = GetGroupingKey(singleViewModel); - - await ExecuteUIThread(() => - { - RemoveItemInternal(group, threadMailItemViewModel); - InsertItemInternal(groupKey, singleViewModel); - }); - - // If thread->single conversion is being done, we should ignore it for non-draft items. - // eg. Deleting a reply message from draft folder. Single non-draft item should not be re-added. - - if (PruneSingleNonDraftItems && !singleViewModel.IsDraft) - { - // This item should not be here anymore. - // It's basically a reply mail in Draft folder. - var newGroup = _mailItemSource.FirstGroupByKeyOrDefault(groupKey); - - if (newGroup != null) - { - await ExecuteUIThread(() => { RemoveItemInternal(newGroup, singleViewModel); }); - } - } - } - else if (threadMailItemViewModel.ThreadItems.Count == 0) - { - await ExecuteUIThread(() => { RemoveItemInternal(group, threadMailItemViewModel); }); - } - else - { - // Item inside the thread is removed. - await ExecuteUIThread(() => { threadMailItemViewModel.ThreadItems.Remove(removalItem); }); - - UpdateUniqueIdHashes(removalItem, false); - } - - shouldExit = true; - break; - } - else if (item.UniqueId == removeItem.UniqueId) - { - await ExecuteUIThread(() => { RemoveItemInternal(group, item); }); - - shouldExit = true; - - break; - } - } - } - } - - private async Task ExecuteUIThread(Action action) => await CoreDispatcher?.ExecuteOnUIThread(action); -} diff --git a/Wino.Mail.ViewModels/ComposePageViewModel.cs b/Wino.Mail.ViewModels/ComposePageViewModel.cs index 9363d087..286cd117 100644 --- a/Wino.Mail.ViewModels/ComposePageViewModel.cs +++ b/Wino.Mail.ViewModels/ComposePageViewModel.cs @@ -211,7 +211,7 @@ public partial class ComposePageViewModel : MailBaseViewModel isUpdatingMimeBlocked = true; - var assignedAccount = CurrentMailDraftItem.AssignedAccount; + var assignedAccount = CurrentMailDraftItem.MailCopy.AssignedAccount; var sentFolder = await _folderService.GetSpecialFolderByAccountIdAsync(assignedAccount.Id, SpecialFolderType.Sent); using MemoryStream memoryStream = new(); @@ -223,8 +223,8 @@ public partial class ComposePageViewModel : MailBaseViewModel var draftSendPreparationRequest = new SendDraftPreparationRequest(CurrentMailDraftItem.MailCopy, SelectedAlias, sentFolder, - CurrentMailDraftItem.AssignedFolder, - CurrentMailDraftItem.AssignedAccount.Preferences, + CurrentMailDraftItem.MailCopy.AssignedFolder, + CurrentMailDraftItem.MailCopy.AssignedAccount.Preferences, base64EncodedMessage); await _worker.ExecuteAsync(draftSendPreparationRequest); @@ -355,7 +355,7 @@ public partial class ComposePageViewModel : MailBaseViewModel if (ComposingAccount != null) return true; - var composingAccount = await _accountService.GetAccountAsync(CurrentMailDraftItem.AssignedAccount.Id).ConfigureAwait(false); + var composingAccount = await _accountService.GetAccountAsync(CurrentMailDraftItem.MailCopy.AssignedAccount.Id).ConfigureAwait(false); if (composingAccount == null) return false; var aliases = await _accountService.GetAccountAliasesAsync(composingAccount.Id).ConfigureAwait(false); @@ -556,7 +556,7 @@ public partial class ComposePageViewModel : MailBaseViewModel if (CurrentMailDraftItem == null) return; - if (updatedMail.UniqueId == CurrentMailDraftItem.UniqueId) + if (updatedMail.UniqueId == CurrentMailDraftItem.MailCopy.UniqueId) { await ExecuteUIThread(() => { diff --git a/Wino.Mail.ViewModels/Data/MailItemViewModel.cs b/Wino.Mail.ViewModels/Data/MailItemViewModel.cs index 66344587..dc11402a 100644 --- a/Wino.Mail.ViewModels/Data/MailItemViewModel.cs +++ b/Wino.Mail.ViewModels/Data/MailItemViewModel.cs @@ -1,35 +1,24 @@ -using System; -using System.Collections.Generic; -using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.ComponentModel; using Wino.Core.Domain.Entities.Mail; -using Wino.Core.Domain.Entities.Shared; -using Wino.Core.Domain.Models.MailItem; namespace Wino.Mail.ViewModels.Data; /// /// Single view model for IMailItem representation. /// -public partial class MailItemViewModel(MailCopy mailCopy) : ObservableObject, IMailItem +public partial class MailItemViewModel(MailCopy mailCopy) : ObservableObject { [ObservableProperty] public partial MailCopy MailCopy { get; set; } = mailCopy; - public Guid UniqueId => ((IMailItem)MailCopy).UniqueId; - public string ThreadId => ((IMailItem)MailCopy).ThreadId; - public string MessageId => ((IMailItem)MailCopy).MessageId; - public DateTime CreationDate => ((IMailItem)MailCopy).CreationDate; - public string References => ((IMailItem)MailCopy).References; - public string InReplyTo => ((IMailItem)MailCopy).InReplyTo; - [ObservableProperty] public partial bool ThumbnailUpdatedEvent { get; set; } = false; [ObservableProperty] - public partial bool IsCustomFocused { get; set; } + public partial bool IsSelected { get; set; } [ObservableProperty] - public partial bool IsSelected { get; set; } + public partial bool IsDisplayedInThread { get; set; } public bool IsFlagged { @@ -96,14 +85,4 @@ public partial class MailItemViewModel(MailCopy mailCopy) : ObservableObject, IM get => MailCopy.HasAttachments; set => SetProperty(MailCopy.HasAttachments, value, MailCopy, (u, n) => u.HasAttachments = n); } - - public MailItemFolder AssignedFolder => ((IMailItem)MailCopy).AssignedFolder; - - public MailAccount AssignedAccount => ((IMailItem)MailCopy).AssignedAccount; - - public Guid FileId => ((IMailItem)MailCopy).FileId; - - public AccountContact SenderContact => ((IMailItem)MailCopy).SenderContact; - - public IEnumerable GetContainingIds() => new[] { UniqueId }; } diff --git a/Wino.Mail.ViewModels/Data/ThreadMailItemViewModel.cs b/Wino.Mail.ViewModels/Data/ThreadMailItemViewModel.cs index bcc87aef..0626f3a2 100644 --- a/Wino.Mail.ViewModels/Data/ThreadMailItemViewModel.cs +++ b/Wino.Mail.ViewModels/Data/ThreadMailItemViewModel.cs @@ -1,126 +1,114 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics; using System.Linq; using CommunityToolkit.Mvvm.ComponentModel; -using Wino.Core.Domain.Entities.Mail; -using Wino.Core.Domain.Entities.Shared; -using Wino.Core.Domain.Models.MailItem; namespace Wino.Mail.ViewModels.Data; /// /// Thread mail item (multiple IMailItem) view model representation. /// -public partial class ThreadMailItemViewModel : ObservableObject, IMailItemThread, IComparable, IComparable +public partial class ThreadMailItemViewModel : ObservableRecipient, IDisposable { - public ObservableCollection ThreadItems => (MailItem as IMailItemThread)?.ThreadItems ?? []; - public AccountContact SenderContact => ((IMailItemThread)MailItem).SenderContact; + private readonly string _threadId; + + private readonly List _threadEmails = []; + private bool _disposed; [ObservableProperty] - private ThreadMailItem mailItem; + [NotifyPropertyChangedRecipients] + public partial bool IsThreadExpanded { get; set; } [ObservableProperty] - private bool isThreadExpanded; + public partial bool IsSelected { get; set; } - public ThreadMailItemViewModel(ThreadMailItem threadMailItem) + /// + /// Gets the number of emails in this thread + /// + public int EmailCount => _threadEmails.Count; + + /// + /// Gets the latest email's subject for display + /// + public string? Subject => _threadEmails + .OrderByDescending(e => e.MailCopy?.CreationDate) + .FirstOrDefault()?.MailCopy?.Subject; + + /// + /// Gets the latest email's sender name for display + /// + public string? FromName => _threadEmails + .OrderByDescending(e => e.MailCopy?.CreationDate) + .FirstOrDefault()?.MailCopy?.SenderContact.Name; + + /// + /// Gets the latest email's creation date for sorting + /// + public DateTime LatestEmailDate => _threadEmails + .OrderByDescending(e => e.MailCopy?.CreationDate) + .FirstOrDefault()?.MailCopy?.CreationDate ?? DateTime.MinValue; + + /// + /// Gets all emails in this thread (read-only) + /// + public IReadOnlyList ThreadEmails => _threadEmails.AsReadOnly(); + + public ThreadMailItemViewModel(string threadId) { - MailItem = new ThreadMailItem(); + _threadId = threadId; + } - // Local copies - foreach (var item in threadMailItem.ThreadItems) + partial void OnIsSelectedChanged(bool value) + { + + } + + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + + if (disposing) { - AddMailItemViewModel(item); + _threadEmails.Clear(); } + + _disposed = true; } - public ThreadMailItem GetThreadMailItem() => MailItem; - - public IEnumerable GetMailCopies() - => ThreadItems.OfType().Select(a => a.MailCopy); - - public void AddMailItemViewModel(IMailItem mailItem) + public void Dispose() { - if (mailItem == null) return; - - if (mailItem is MailCopy mailCopy) - MailItem.AddThreadItem(new MailItemViewModel(mailCopy)); - else if (mailItem is MailItemViewModel mailItemViewModel) - MailItem.AddThreadItem(mailItemViewModel); - else - Debugger.Break(); + Dispose(true); + GC.SuppressFinalize(this); } - public bool HasUniqueId(Guid uniqueMailId) - => ThreadItems.Any(a => a.UniqueId == uniqueMailId); - - public IMailItem GetItemById(Guid uniqueMailId) - => ThreadItems.FirstOrDefault(a => a.UniqueId == uniqueMailId); - - public void RemoveCopyItem(IMailItem item) + private void NotifyPropertyChanges() { - MailCopy copyToRemove = null; + OnPropertyChanged(nameof(Subject)); + OnPropertyChanged(nameof(FromName)); + } - if (item is MailItemViewModel mailItemViewModel) - copyToRemove = mailItemViewModel.MailCopy; - else if (item is MailCopy copyItem) - copyToRemove = copyItem; - var existedItem = ThreadItems.FirstOrDefault(a => a.Id == copyToRemove.Id); - - if (existedItem == null) return; - - ThreadItems.Remove(existedItem); + /// + /// Adds an email to this thread + /// + public void AddEmail(MailItemViewModel email) + { + if (email.MailCopy.ThreadId != _threadId) + throw new ArgumentException($"Email ThreadId '{email.MailCopy.ThreadId}' does not match expander ThreadId '{_threadId}'"); + _threadEmails.Add(email); NotifyPropertyChanges(); } - public void NotifyPropertyChanges() + /// + /// Removes an email from this thread + /// + public void RemoveEmail(MailItemViewModel email) { - // TODO - // Stupid temporary fix for not updating UI. - // This view model must be reworked with ThreadMailItem together. - - var current = MailItem; - - MailItem = null; - MailItem = current; + if (_threadEmails.Remove(email)) + { + NotifyPropertyChanges(); + } } - - public IMailItem LatestMailItem => ((IMailItemThread)MailItem).LatestMailItem; - public IMailItem FirstMailItem => ((IMailItemThread)MailItem).FirstMailItem; - - public string Id => ((IMailItem)MailItem).Id; - public string Subject => ((IMailItem)MailItem).Subject; - public string ThreadId => ((IMailItem)MailItem).ThreadId; - public string MessageId => ((IMailItem)MailItem).MessageId; - public string References => ((IMailItem)MailItem).References; - public string PreviewText => ((IMailItem)MailItem).PreviewText; - public string FromName => ((IMailItem)MailItem).FromName; - public DateTime CreationDate => ((IMailItem)MailItem).CreationDate; - public string FromAddress => ((IMailItem)MailItem).FromAddress; - public bool HasAttachments => ((IMailItem)MailItem).HasAttachments; - public bool IsFlagged => ((IMailItem)MailItem).IsFlagged; - public bool IsFocused => ((IMailItem)MailItem).IsFocused; - public bool IsRead => ((IMailItem)MailItem).IsRead; - public bool IsDraft => ((IMailItem)MailItem).IsDraft; - public string DraftId => string.Empty; - public string InReplyTo => ((IMailItem)MailItem).InReplyTo; - - public MailItemFolder AssignedFolder => ((IMailItem)MailItem).AssignedFolder; - - public MailAccount AssignedAccount => ((IMailItem)MailItem).AssignedAccount; - - public Guid UniqueId => ((IMailItem)MailItem).UniqueId; - - public Guid FileId => ((IMailItem)MailItem).FileId; - - public int CompareTo(DateTime other) => CreationDate.CompareTo(other); - public int CompareTo(string other) => FromName.CompareTo(other); - - // Get single mail item view model out of the only item in thread items. - public MailItemViewModel GetSingleItemViewModel() => ThreadItems.First() as MailItemViewModel; - - public IEnumerable GetContainingIds() => ((IMailItemThread)MailItem).GetContainingIds(); } diff --git a/Wino.Mail.ViewModels/MailListPageViewModel.cs b/Wino.Mail.ViewModels/MailListPageViewModel.cs index fd2a5627..c2f8e386 100644 --- a/Wino.Mail.ViewModels/MailListPageViewModel.cs +++ b/Wino.Mail.ViewModels/MailListPageViewModel.cs @@ -59,8 +59,15 @@ public partial class MailListPageViewModel : MailBaseViewModel, private IObservable> selectionChangedObservable = null; - public WinoMailCollection MailCollection { get; } + public GroupedEmailCollection MailCollection { get; set; } = new GroupedEmailCollection(); + //public IEnumerable SelectedItems + //{ + // get + // { + + // } + //} public ObservableCollection SelectedItems { get; set; } = []; public ObservableCollection PivotFolders { get; set; } = []; public ObservableCollection ActionItems { get; set; } = []; @@ -77,7 +84,6 @@ public partial class MailListPageViewModel : MailBaseViewModel, private readonly IMailDialogService _mailDialogService; private readonly IMailService _mailService; private readonly IFolderService _folderService; - private readonly IThreadingStrategyProvider _threadingStrategyProvider; private readonly IContextMenuItemService _contextMenuItemService; private readonly IWinoRequestDelegator _winoRequestDelegator; private readonly IKeyPressService _keyPressService; @@ -154,7 +160,6 @@ public partial class MailListPageViewModel : MailBaseViewModel, IMailService mailService, IStatePersistanceService statePersistenceService, IFolderService folderService, - IThreadingStrategyProvider threadingStrategyProvider, IContextMenuItemService contextMenuItemService, IWinoRequestDelegator winoRequestDelegator, IKeyPressService keyPressService, @@ -162,7 +167,6 @@ public partial class MailListPageViewModel : MailBaseViewModel, INewThemeService themeService, IWinoLogger winoLogger) { - MailCollection = new WinoMailCollection(threadingStrategyProvider); PreferencesService = preferencesService; ThemeService = themeService; _winoLogger = winoLogger; @@ -172,7 +176,6 @@ public partial class MailListPageViewModel : MailBaseViewModel, _mailDialogService = mailDialogService; _mailService = mailService; _folderService = folderService; - _threadingStrategyProvider = threadingStrategyProvider; _contextMenuItemService = contextMenuItemService; _winoRequestDelegator = winoRequestDelegator; _keyPressService = keyPressService; @@ -190,23 +193,23 @@ public partial class MailListPageViewModel : MailBaseViewModel, await ExecuteUIThread(() => { SelectedItemCollectionUpdated(a.EventArgs); }); }); - MailCollection.MailItemRemoved += (c, removedItem) => - { - if (removedItem is ThreadMailItemViewModel removedThreadViewModelItem) - { - foreach (var viewModel in removedThreadViewModelItem.ThreadItems.Cast()) - { - if (SelectedItems.Contains(viewModel)) - { - SelectedItems.Remove(viewModel); - } - } - } - else if (removedItem is MailItemViewModel removedMailItemViewModel && SelectedItems.Contains(removedMailItemViewModel)) - { - SelectedItems.Remove(removedMailItemViewModel); - } - }; + //MailCollection.MailItemRemoved += (c, removedItem) => + //{ + // if (removedItem is ThreadMailItemViewModel removedThreadViewModelItem) + // { + // foreach (var viewModel in removedThreadViewModelItem.ThreadItems.Cast()) + // { + // if (SelectedItems.Contains(viewModel)) + // { + // SelectedItems.Remove(viewModel); + // } + // } + // } + // else if (removedItem is MailItemViewModel removedMailItemViewModel && SelectedItems.Contains(removedMailItemViewModel)) + // { + // SelectedItems.Remove(removedMailItemViewModel); + // } + //}; } private void SetupTopBarActions() @@ -243,10 +246,11 @@ public partial class MailListPageViewModel : MailBaseViewModel, { if (SetProperty(ref _selectedSortingOption, value)) { - if (value != null && MailCollection != null) - { - MailCollection.SortingType = value.Type; - } + // TODO: Update sorting in mail collection. + //if (value != null && MailCollection != null) + //{ + // MailCollection.SortingType = value.Type; + //} } } } @@ -318,16 +322,16 @@ public partial class MailListPageViewModel : MailBaseViewModel, if (markAsPreference == MailMarkAsOption.WhenSelected) { - var operation = MailOperation.MarkAsRead; - var package = new MailOperationPreperationRequest(operation, _activeMailItem.MailCopy); + //var operation = MailOperation.MarkAsRead; + //var package = new MailOperationPreperationRequest(operation, _activeMailItem.MailCopy); - if (ActiveFolder?.SpecialFolderType == SpecialFolderType.Unread && - !gmailUnreadFolderMarkedAsReadUniqueIds.Contains(_activeMailItem.UniqueId)) - { - gmailUnreadFolderMarkedAsReadUniqueIds.Add(_activeMailItem.UniqueId); - } + //if (ActiveFolder?.SpecialFolderType == SpecialFolderType.Unread && + // !gmailUnreadFolderMarkedAsReadUniqueIds.Contains(_activeMailItem.UniqueId)) + //{ + // gmailUnreadFolderMarkedAsReadUniqueIds.Add(_activeMailItem.UniqueId); + //} - await ExecuteMailOperationAsync(package); + //await ExecuteMailOperationAsync(package); } else if (markAsPreference == MailMarkAsOption.AfterDelay && PreferencesService.MarkAsDelay >= 0) { @@ -353,13 +357,6 @@ public partial class MailListPageViewModel : MailBaseViewModel, OnPropertyChanged(nameof(IsFolderEmpty)); } - protected override void OnDispatcherAssigned() - { - base.OnDispatcherAssigned(); - - MailCollection.CoreDispatcher = Dispatcher; - } - private async void UpdateBarMessage(InfoBarMessageType severity, string title, string message) { await ExecuteUIThread(() => @@ -565,67 +562,35 @@ public partial class MailListPageViewModel : MailBaseViewModel, [RelayCommand] private async Task LoadMoreItemsAsync() { - if (IsInitializingFolder || IsOnlineSearchEnabled) return; + //if (IsInitializingFolder || IsOnlineSearchEnabled) return; - await ExecuteUIThread(() => { IsInitializingFolder = true; }); + //await ExecuteUIThread(() => { IsInitializingFolder = true; }); - var initializationOptions = new MailListInitializationOptions(ActiveFolder.HandlingFolders, - SelectedFilterOption.Type, - SelectedSortingOption.Type, - PreferencesService.IsThreadingEnabled, - SelectedFolderPivot.IsFocused, - IsInSearchMode ? SearchQuery : string.Empty, - MailCollection.MailCopyIdHashSet); + //var initializationOptions = new MailListInitializationOptions(ActiveFolder.HandlingFolders, + // SelectedFilterOption.Type, + // SelectedSortingOption.Type, + // PreferencesService.IsThreadingEnabled, + // SelectedFolderPivot.IsFocused, + // IsInSearchMode ? SearchQuery : string.Empty, + // MailCollection.MailCopyIdHashSet); - var items = await _mailService.FetchMailsAsync(initializationOptions).ConfigureAwait(false); + //var items = await _mailService.FetchMailsAsync(initializationOptions).ConfigureAwait(false); - var viewModels = PrepareMailViewModels(items); + //var viewModels = PrepareMailViewModels(items); - await ExecuteUIThread(() => { MailCollection.AddRange(viewModels, clearIdCache: false); }); - await ExecuteUIThread(() => { IsInitializingFolder = false; }); + //await ExecuteUIThread(() => { MailCollection.AddRange(viewModels, clearIdCache: false); }); + //await ExecuteUIThread(() => { IsInitializingFolder = false; }); } #endregion public Task ExecuteMailOperationAsync(MailOperationPreperationRequest package) => _winoRequestDelegator.ExecuteAsync(package); - public IEnumerable GetTargetMailItemViewModels(IMailItem clickedItem) - { - // Threat threads as a whole and include everything in the group. Except single selections outside of the thread. - IEnumerable contextMailItems = null; + public IEnumerable GetAvailableMailActions(IEnumerable contextMailItems) + => _contextMenuItemService.GetMailItemContextMenuActions(contextMailItems.Select(a => a.MailCopy)); - if (clickedItem is ThreadMailItemViewModel clickedThreadItem) - { - // Clicked item is a thread. - clickedThreadItem.IsThreadExpanded = true; - contextMailItems = clickedThreadItem.ThreadItems.Cast(); - - // contextMailItems = clickedThreadItem.GetMailCopies(); - } - else if (clickedItem is MailItemViewModel clickedMailItemViewModel) - { - // If the clicked item is included in SelectedItems, then we need to thing them as whole. - // If there are selected items, but clicked item is not one of them, then it's a single context menu. - - bool includedInSelectedItems = SelectedItems.Contains(clickedItem); - - if (includedInSelectedItems) - contextMailItems = SelectedItems; - else - contextMailItems = [clickedMailItemViewModel]; - } - - return contextMailItems; - } - - public IEnumerable GetAvailableMailActions(IEnumerable contextMailItems) - => _contextMenuItemService.GetMailItemContextMenuActions(contextMailItems); - - public void ChangeCustomFocusedState(IEnumerable mailItems, bool isFocused) - => mailItems.OfType().ForEach(a => a.IsCustomFocused = isFocused); - - private bool ShouldPreventItemAdd(IMailItem mailItem) + private bool ShouldPreventItemAdd(MailCopy mailItem) { bool condition = mailItem.IsRead && SelectedFilterOption.Type == FilterOptionType.Unread @@ -660,7 +625,7 @@ public partial class MailListPageViewModel : MailBaseViewModel, await listManipulationSemepahore.WaitAsync(); - await MailCollection.AddAsync(addedMail); + // await MailCollection.AddAsync(addedMail); await ExecuteUIThread(() => { NotifyItemFoundState(); }); } @@ -677,7 +642,7 @@ public partial class MailListPageViewModel : MailBaseViewModel, Debug.WriteLine($"Updating {updatedMail.Id}-> {updatedMail.UniqueId}"); - await MailCollection.UpdateMailCopy(updatedMail); + // await MailCollection.UpdateMailCopy(updatedMail); await ExecuteUIThread(() => { SetupTopBarActions(); }); } @@ -712,12 +677,12 @@ public partial class MailListPageViewModel : MailBaseViewModel, { await ExecuteUIThread(() => { - nextItem = MailCollection.GetNextItem(removedMail); + // nextItem = MailCollection.GetNextItem(removedMail); }); } // Remove the deleted item from the list. - await MailCollection.RemoveAsync(removedMail); + // await MailCollection.RemoveAsync(removedMail); if (nextItem != null) WeakReferenceMessenger.Default.Send(new SelectMailItemContainerEvent(nextItem, ScrollToItem: true)); @@ -748,11 +713,11 @@ public partial class MailListPageViewModel : MailBaseViewModel, // Otherwise the draft mail item will be duplicated on the next add execution. await listManipulationSemepahore.WaitAsync(); - // Create the item. Draft folder navigation is already done at this point. - await MailCollection.AddAsync(draftMail); - await ExecuteUIThread(() => { + // Create the item. Draft folder navigation is already done at this point. + MailCollection.AddEmail(new MailItemViewModel(draftMail)); + // New draft is created by user. Select the item. Messenger.Send(new MailItemNavigationRequested(draftMail.UniqueId, ScrollToItem: true)); @@ -765,15 +730,16 @@ public partial class MailListPageViewModel : MailBaseViewModel, } } - private IEnumerable PrepareMailViewModels(IEnumerable mailItems) + private IEnumerable PrepareMailViewModels(IEnumerable mailItems) { - foreach (var item in mailItems) - { - if (item is MailCopy singleMailItem) - yield return new MailItemViewModel(singleMailItem); - else if (item is ThreadMailItem threadMailItem) - yield return new ThreadMailItemViewModel(threadMailItem); - } + return mailItems.Select(a => new MailItemViewModel(a)); + //foreach (var item in mailItems) + //{ + // if (item is MailCopy singleMailItem) + // yield return new MailItemViewModel(singleMailItem); + // else if (item is ThreadMailItem threadMailItem) + // yield return new ThreadMailItemViewModel(threadMailItem); + //} } [RelayCommand] @@ -793,7 +759,6 @@ public partial class MailListPageViewModel : MailBaseViewModel, try { MailCollection.Clear(); - MailCollection.MailCopyIdHashSet.Clear(); SelectedItems.Clear(); @@ -816,17 +781,9 @@ public partial class MailListPageViewModel : MailBaseViewModel, await listManipulationSemepahore.WaitAsync(cancellationToken); - // Setup MailCollection configuration. - - // Don't pass any threading strategy if disabled in settings. - MailCollection.ThreadingStrategyProvider = PreferencesService.IsThreadingEnabled ? _threadingStrategyProvider : null; - - // TODO: This should go inside - MailCollection.PruneSingleNonDraftItems = ActiveFolder.SpecialFolderType == SpecialFolderType.Draft; - // Here items are sorted and filtered. - List items = null; + List items = null; List onlineSearchItems = null; bool isDoingSearch = !string.IsNullOrEmpty(SearchQuery); @@ -894,7 +851,7 @@ public partial class MailListPageViewModel : MailBaseViewModel, PreferencesService.IsThreadingEnabled, SelectedFolderPivot.IsFocused, SearchQuery, - MailCollection.MailCopyIdHashSet, + default, onlineSearchItems); items = await _mailService.FetchMailsAsync(initializationOptions, cancellationToken).ConfigureAwait(false); @@ -909,7 +866,7 @@ public partial class MailListPageViewModel : MailBaseViewModel, await ExecuteUIThread(() => { - MailCollection.AddRange(viewModels, true); + MailCollection.AddEmails(viewModels); if (isDoingSearch && !isDoingOnlineSearch) { @@ -1055,15 +1012,16 @@ public partial class MailListPageViewModel : MailBaseViewModel, for (int i = 0; i < 3; i++) { - var mailContainer = MailCollection.GetMailItemContainer(message.UniqueMailId); + // TODO: Get container. + //var mailContainer = MailCollection.GetMailItemContainer(message.UniqueMailId); - if (mailContainer != null) - { - navigatingMailItem = mailContainer.ItemViewModel; - threadMailItemViewModel = mailContainer.ThreadViewModel; + //if (mailContainer != null) + //{ + // navigatingMailItem = mailContainer.ItemViewModel; + // threadMailItemViewModel = mailContainer.ThreadViewModel; - break; - } + // break; + //} } if (threadMailItemViewModel != null) @@ -1138,5 +1096,8 @@ public partial class MailListPageViewModel : MailBaseViewModel, } } - public void Receive(ThumbnailAdded message) => MailCollection.UpdateThumbnails(message.Email); + public void Receive(ThumbnailAdded message) + { + // MailCollection.UpdateThumbnails(message.Email); + } } diff --git a/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs b/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs index 6be1fe8b..2e230a18 100644 --- a/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs +++ b/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs @@ -60,7 +60,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, public bool ShouldDisplayDownloadProgress => IsIndetermineProgress || (CurrentDownloadPercentage > 0 && CurrentDownloadPercentage <= 100); public bool CanUnsubscribe => CurrentRenderModel?.UnsubscribeInfo?.CanUnsubscribe ?? false; - public bool IsJunkMail => initializedMailItemViewModel?.AssignedFolder != null && initializedMailItemViewModel.AssignedFolder.SpecialFolderType == SpecialFolderType.Junk; + public bool IsJunkMail => initializedMailItemViewModel?.MailCopy.AssignedFolder != null && initializedMailItemViewModel.MailCopy.AssignedFolder.SpecialFolderType == SpecialFolderType.Junk; public bool IsImageRenderingDisabled { @@ -283,9 +283,9 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, } }; - var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(initializedMailItemViewModel.AssignedAccount.Id, draftOptions).ConfigureAwait(false); + var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(initializedMailItemViewModel.MailCopy.AssignedAccount.Id, draftOptions).ConfigureAwait(false); - var draftPreparationRequest = new DraftPreparationRequest(initializedMailItemViewModel.AssignedAccount, draftMailCopy, draftBase64MimeMessage, draftOptions.Reason, initializedMailItemViewModel.MailCopy); + var draftPreparationRequest = new DraftPreparationRequest(initializedMailItemViewModel.MailCopy.AssignedAccount, draftMailCopy, draftBase64MimeMessage, draftOptions.Reason, initializedMailItemViewModel.MailCopy); await _requestDelegator.ExecuteAsync(draftPreparationRequest); @@ -375,7 +375,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, private async Task RenderAsync(MailItemViewModel mailItemViewModel, CancellationToken cancellationToken = default) { ResetProgress(); - var isMimeExists = await _mimeFileService.IsMimeExistAsync(mailItemViewModel.AssignedAccount.Id, mailItemViewModel.MailCopy.FileId); + var isMimeExists = await _mimeFileService.IsMimeExistAsync(mailItemViewModel.MailCopy.AssignedAccount.Id, mailItemViewModel.MailCopy.FileId); if (!isMimeExists) { @@ -384,7 +384,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, // Find the MIME for this item and render it. var mimeMessageInformation = await _mimeFileService.GetMimeMessageInformationAsync(mailItemViewModel.MailCopy.FileId, - mailItemViewModel.AssignedAccount.Id, + mailItemViewModel.MailCopy.AssignedAccount.Id, cancellationToken).ConfigureAwait(false); if (mimeMessageInformation == null) @@ -436,12 +436,12 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, FromAddress = message.From.Mailboxes.FirstOrDefault()?.Address ?? Translator.UnknownAddress; FromName = message.From.Mailboxes.FirstOrDefault()?.Name ?? Translator.UnknownSender; CreationDate = message.Date.DateTime; - ContactPicture = initializedMailItemViewModel?.SenderContact?.Base64ContactPicture; + ContactPicture = initializedMailItemViewModel?.MailCopy.SenderContact?.Base64ContactPicture; // Automatically disable images for Junk folder to prevent pixel tracking. // This can only work for selected mail item rendering, not for EML file rendering. if (initializedMailItemViewModel != null && - initializedMailItemViewModel.AssignedFolder.SpecialFolderType == SpecialFolderType.Junk) + initializedMailItemViewModel.MailCopy.AssignedFolder.SpecialFolderType == SpecialFolderType.Junk) { renderingOptions.LoadImages = false; } @@ -480,7 +480,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, var contactViewModel = new AccountContactViewModel(foundContact); // Make sure that user account first in the list. - if (string.Equals(contactViewModel.Address, initializedMailItemViewModel?.AssignedAccount?.Address, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(contactViewModel.Address, initializedMailItemViewModel?.MailCopy.AssignedAccount?.Address, StringComparison.OrdinalIgnoreCase)) { contactViewModel.IsMe = true; accounts.Insert(0, contactViewModel); @@ -563,7 +563,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, } // Archive - Unarchive - if (initializedMailItemViewModel.AssignedFolder.SpecialFolderType == SpecialFolderType.Archive) + if (initializedMailItemViewModel.MailCopy.AssignedFolder.SpecialFolderType == SpecialFolderType.Archive) MenuItems.Add(MailOperationMenuItem.Create(MailOperation.UnArchive)); else MenuItems.Add(MailOperationMenuItem.Create(MailOperation.Archive)); @@ -594,7 +594,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, // Check if the updated mail is the same mail item we are rendering. // This is done with UniqueId to include FolderId into calculations. - if (initializedMailItemViewModel.UniqueId != updatedMail.UniqueId) return; + if (initializedMailItemViewModel.MailCopy.UniqueId != updatedMail.UniqueId) return; // Mail operation might change the mail item like mark read/unread or change flag. // So we need to update the mail item view model when this happens. diff --git a/Wino.Mail.WinUI/AppShell.xaml.cs b/Wino.Mail.WinUI/AppShell.xaml.cs index 621854f7..a2015171 100644 --- a/Wino.Mail.WinUI/AppShell.xaml.cs +++ b/Wino.Mail.WinUI/AppShell.xaml.cs @@ -20,7 +20,6 @@ using Wino.Core.Domain.Models.Navigation; using Wino.Core.WinUI; using Wino.Core.WinUI.Controls; using Wino.Extensions; -using Wino.Mail.ViewModels.Data; using Wino.MenuFlyouts; using Wino.MenuFlyouts.Context; using Wino.Messaging.Client.Accounts; @@ -67,14 +66,14 @@ public sealed partial class AppShell : AppShellAbstract, foreach (var item in dragPackage.DraggingMails) { - if (item is MailItemViewModel singleMailItemViewModel) - { - mailCopies.Add(singleMailItemViewModel.MailCopy); - } - else if (item is ThreadMailItemViewModel threadViewModel) - { - mailCopies.AddRange(threadViewModel.GetMailCopies()); - } + //if (item is MailItemViewModel singleMailItemViewModel) + //{ + // mailCopies.Add(singleMailItemViewModel.MailCopy); + //} + //else if (item is ThreadMailItemViewModel threadViewModel) + //{ + // mailCopies.AddRange(threadViewModel.GetMailCopies()); + //} } await ViewModel.PerformMoveOperationAsync(mailCopies, draggingFolder); diff --git a/Wino.Mail.WinUI/Controls/Advanced/WinoItemsView.cs b/Wino.Mail.WinUI/Controls/Advanced/WinoItemsView.cs new file mode 100644 index 00000000..5b972551 --- /dev/null +++ b/Wino.Mail.WinUI/Controls/Advanced/WinoItemsView.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using Microsoft.UI.Xaml.Controls; + +namespace Wino.Mail.WinUI.Controls.Advanced; + +public partial class WinoItemsView : ItemsView +{ + public IEnumerable? CastedItemsSource => ItemsSource as IEnumerable; + + public WinoItemsView() + { + DefaultStyleKey = typeof(ItemsView); + } +} diff --git a/Wino.Mail.WinUI/Controls/Advanced/WinoListView.cs b/Wino.Mail.WinUI/Controls/Advanced/WinoListView.cs deleted file mode 100644 index f6dbcdbc..00000000 --- a/Wino.Mail.WinUI/Controls/Advanced/WinoListView.cs +++ /dev/null @@ -1,418 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using System.Windows.Input; -using CommunityToolkit.Mvvm.Messaging; -using MoreLinq; -using Serilog; -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Models.MailItem; -using Wino.Extensions; -using Wino.Mail.ViewModels.Data; -using Wino.Mail.ViewModels.Messages; - -namespace Wino.Controls.Advanced; - -/// -/// Custom ListView control that handles multiple selection with Extended/Multiple selection mode -/// and supports threads. -/// -public partial class WinoListView : ListView, IDisposable -{ - private ILogger logger = Log.ForContext(); - - private const string PART_ScrollViewer = "ScrollViewer"; - private ScrollViewer internalScrollviewer; - - /// - /// Gets or sets whether this ListView belongs to thread items. - /// This is important for detecting selected items etc. - /// - public bool IsThreadListView - { - get { return (bool)GetValue(IsThreadListViewProperty); } - set { SetValue(IsThreadListViewProperty, value); } - } - - public ICommand ItemDeletedCommand - { - get { return (ICommand)GetValue(ItemDeletedCommandProperty); } - set { SetValue(ItemDeletedCommandProperty, value); } - } - - public ICommand LoadMoreCommand - { - get { return (ICommand)GetValue(LoadMoreCommandProperty); } - set { SetValue(LoadMoreCommandProperty, value); } - } - - public bool IsThreadScrollingEnabled - { - get { return (bool)GetValue(IsThreadScrollingEnabledProperty); } - set { SetValue(IsThreadScrollingEnabledProperty, value); } - } - - public static readonly DependencyProperty IsThreadScrollingEnabledProperty = DependencyProperty.Register(nameof(IsThreadScrollingEnabled), typeof(bool), typeof(WinoListView), new PropertyMetadata(false)); - public static readonly DependencyProperty LoadMoreCommandProperty = DependencyProperty.Register(nameof(LoadMoreCommand), typeof(ICommand), typeof(WinoListView), new PropertyMetadata(null)); - public static readonly DependencyProperty IsThreadListViewProperty = DependencyProperty.Register(nameof(IsThreadListView), typeof(bool), typeof(WinoListView), new PropertyMetadata(false, new PropertyChangedCallback(OnIsThreadViewChanged))); - public static readonly DependencyProperty ItemDeletedCommandProperty = DependencyProperty.Register(nameof(ItemDeletedCommand), typeof(ICommand), typeof(WinoListView), new PropertyMetadata(null)); - - public WinoListView() - { - CanDragItems = true; - IsItemClickEnabled = true; - IsMultiSelectCheckBoxEnabled = true; - IsRightTapEnabled = true; - SelectionMode = ListViewSelectionMode.Extended; - ShowsScrollingPlaceholders = false; - SingleSelectionFollowsFocus = true; - - DragItemsCompleted += ItemDragCompleted; - DragItemsStarting += ItemDragStarting; - SelectionChanged += SelectedItemsChanged; - ProcessKeyboardAccelerators += ProcessDelKey; - } - - protected override void OnApplyTemplate() - { - base.OnApplyTemplate(); - - internalScrollviewer = GetTemplateChild(PART_ScrollViewer) as ScrollViewer; - - if (internalScrollviewer == null) - { - logger.Warning("WinoListView does not have an internal ScrollViewer. Infinite scrolling behavior might be effected."); - return; - } - - internalScrollviewer.ViewChanged -= InternalScrollVeiwerViewChanged; - internalScrollviewer.ViewChanged += InternalScrollVeiwerViewChanged; - } - - private static void OnIsThreadViewChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args) - { - if (obj is WinoListView winoListView) - { - winoListView.AdjustThreadViewContainerVisuals(); - } - } - - private void AdjustThreadViewContainerVisuals() - { - if (IsThreadListView) - { - ItemContainerTransitions.Clear(); - } - } - - private double lastestRaisedOffset = 0; - private int lastItemSize = 0; - - // TODO: This is buggy. Does not work all the time. Debug. - - private void InternalScrollVeiwerViewChanged(object sender, ScrollViewerViewChangedEventArgs e) - { - if (internalScrollviewer == null) return; - - // No need to raise init request if there are no items in the list. - if (Items.Count == 0) return; - - // If the scrolling is finished, check the current viewport height. - if (e.IsIntermediate) - { - var currentOffset = internalScrollviewer.VerticalOffset; - var maxOffset = internalScrollviewer.ScrollableHeight; - - if (currentOffset + 10 >= maxOffset && lastestRaisedOffset != maxOffset && Items.Count != lastItemSize) - { - // We must load more. - lastestRaisedOffset = maxOffset; - lastItemSize = Items.Count; - - LoadMoreCommand?.Execute(null); - } - } - } - - private void ProcessDelKey(UIElement sender, Microsoft.UI.Xaml.Input.ProcessKeyboardAcceleratorEventArgs args) - { - if (args.Key == Windows.System.VirtualKey.Delete) - { - args.Handled = true; - - ItemDeletedCommand?.Execute(MailOperation.SoftDelete); - } - } - - private void ItemDragCompleted(ListViewBase sender, DragItemsCompletedEventArgs args) - { - if (args.Items.Any(a => a is MailItemViewModel)) - { - args.Items.Cast().ForEach(a => a.IsCustomFocused = false); - } - } - - private void ItemDragStarting(object sender, DragItemsStartingEventArgs args) - { - // Dragging multiple mails from different accounts/folders are supported with the condition below: - // All mails belongs to the drag will be matched on the dropped folder's account. - // Meaning that if users drag 1 mail from Account A/Inbox and 1 mail from Account B/Inbox, - // and drop to Account A/Inbox, the mail from Account B/Inbox will NOT be moved. - - if (IsThreadListView) - { - var allItems = args.Items.Cast(); - - // Highlight all items - allItems.ForEach(a => a.IsCustomFocused = true); - - // Set native drag arg properties. - - var dragPackage = new MailDragPackage(allItems.Cast()); - - args.Data.Properties.Add(nameof(MailDragPackage), dragPackage); - } - else - { - var dragPackage = new MailDragPackage(args.Items.Cast()); - - args.Data.Properties.Add(nameof(MailDragPackage), dragPackage); - } - } - - public void ChangeSelectionMode(ListViewSelectionMode selectionMode) - { - SelectionMode = selectionMode; - - if (!IsThreadListView) - { - Items.Where(a => a is ThreadMailItemViewModel).Cast().ForEach(c => - { - var threadListView = GetThreadInternalListView(c); - - if (threadListView != null) - { - threadListView.SelectionMode = selectionMode; - } - }); - } - } - - /// - /// Finds the container for given mail item and adds it to selected items. - /// - /// Mail to be added to selected items. - /// Whether selection was successful or not. - public bool SelectMailItemContainer(MailItemViewModel mailItemViewModel) - { - var itemContainer = ContainerFromItem(mailItemViewModel); - - // This item might be in thread container. - if (itemContainer == null) - { - bool found = false; - - Items.OfType().ForEach(c => - { - if (!found) - { - var threadListView = GetThreadInternalListView(c); - - if (threadListView != null) - found = threadListView.SelectMailItemContainer(mailItemViewModel); - } - }); - - return found; - } - - SelectedItems.Add(mailItemViewModel); - return true; - } - - /// - /// Recursively clears all selections except the given mail. - /// - /// Exceptional mail item to be not unselected. - /// Whether expansion states of thread containers should stay as it is or not. - public void ClearSelections(MailItemViewModel exceptViewModel = null, bool preserveThreadExpanding = false) - { - SelectedItems.Clear(); - - Items.Where(a => a is ThreadMailItemViewModel).Cast().ForEach(c => - { - var threadListView = GetThreadInternalListView(c); - - if (threadListView == null) - return; - - if (exceptViewModel != null) - { - if (!threadListView.SelectedItems.Contains(exceptViewModel)) - { - if (!preserveThreadExpanding) - { - c.IsThreadExpanded = false; - } - - threadListView.SelectedItems.Clear(); - } - } - else - { - if (!preserveThreadExpanding) - { - c.IsThreadExpanded = false; - } - - threadListView.SelectedItems.Clear(); - } - }); - } - - /// - /// Recursively selects all mails, including thread items. - /// - public void SelectAllWino() - { - SelectAll(); - - Items.Where(a => a is ThreadMailItemViewModel).Cast().ForEach(c => - { - c.IsThreadExpanded = true; - - var threadListView = GetThreadInternalListView(c); - - threadListView?.SelectAll(); - }); - } - - // SelectedItems changed. - private void SelectedItemsChanged(object sender, SelectionChangedEventArgs e) - { - if (e.RemovedItems != null) - { - foreach (var removedItem in e.RemovedItems) - { - if (removedItem is MailItemViewModel removedMailItemViewModel) - { - // Mail item un-selected. - - removedMailItemViewModel.IsSelected = false; - WeakReferenceMessenger.Default.Send(new MailItemSelectionRemovedEvent(removedMailItemViewModel)); - } - else if (removedItem is ThreadMailItemViewModel removedThreadItemViewModel) - { - removedThreadItemViewModel.IsThreadExpanded = false; - } - } - } - - if (e.AddedItems != null) - { - foreach (var addedItem in e.AddedItems) - { - if (addedItem is MailItemViewModel addedMailItemViewModel) - { - // Mail item selected. - - addedMailItemViewModel.IsSelected = true; - - WeakReferenceMessenger.Default.Send(new MailItemSelectedEvent(addedMailItemViewModel)); - } - else if (addedItem is ThreadMailItemViewModel threadMailItemViewModel) - { - if (IsThreadScrollingEnabled) - { - if (internalScrollviewer != null && ContainerFromItem(threadMailItemViewModel) is FrameworkElement threadFrameworkElement) - { - internalScrollviewer.ScrollToElement(threadFrameworkElement, true, true, bringToTopOrLeft: true); - } - } - - // Try to select first item. - if (GetThreadInternalListView(threadMailItemViewModel) is WinoListView internalListView) - { - internalListView.SelectFirstItem(); - } - } - } - } - - if (!IsThreadListView) - { - if (SelectionMode == ListViewSelectionMode.Extended && SelectedItems.Count == 1) - { - // Only 1 single item is selected in extended mode for main list view. - // We should un-select all thread items. - - Items.Where(a => a is ThreadMailItemViewModel).Cast().ForEach(c => - { - // c.IsThreadExpanded = false; - - var threadListView = GetThreadInternalListView(c); - - threadListView?.SelectedItems.Clear(); - }); - } - } - } - - public async void SelectFirstItem() - { - if (Items.Count > 0) - { - if (Items[0] is MailItemViewModel firstMailItemViewModel) - { - // Make sure the invisible container is realized. - await Task.Delay(250); - - if (ContainerFromItem(firstMailItemViewModel) is ListViewItem firstItemContainer) - { - firstItemContainer.IsSelected = true; - } - - firstMailItemViewModel.IsSelected = true; - } - } - } - - private WinoListView GetThreadInternalListView(ThreadMailItemViewModel threadMailItemViewModel) - { - var itemContainer = ContainerFromItem(threadMailItemViewModel); - - if (itemContainer is ListViewItem listItem) - { - var expander = listItem.GetChildByName("ThreadExpander"); - - if (expander != null) - return expander.Content as WinoListView; - } - - return null; - } - - public void Dispose() - { - DragItemsCompleted -= ItemDragCompleted; - DragItemsStarting -= ItemDragStarting; - SelectionChanged -= SelectedItemsChanged; - ProcessKeyboardAccelerators -= ProcessDelKey; - - if (internalScrollviewer != null) - { - internalScrollviewer.ViewChanged -= InternalScrollVeiwerViewChanged; - } - - foreach (var item in Items) - { - if (item is ThreadMailItemViewModel threadMailItemViewModel) - { - var threadListView = GetThreadInternalListView(threadMailItemViewModel); - threadListView?.Dispose(); - } - } - } -} diff --git a/Wino.Mail.WinUI/Controls/MailItemDisplayInformationControl.xaml.cs b/Wino.Mail.WinUI/Controls/MailItemDisplayInformationControl.xaml.cs index c3a10c83..c9ce1fdc 100644 --- a/Wino.Mail.WinUI/Controls/MailItemDisplayInformationControl.xaml.cs +++ b/Wino.Mail.WinUI/Controls/MailItemDisplayInformationControl.xaml.cs @@ -1,5 +1,4 @@ -using System.Linq; -using System.Numerics; +using System.Numerics; using System.Windows.Input; using CommunityToolkit.WinUI; using Microsoft.UI.Xaml; @@ -9,7 +8,6 @@ using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Models.MailItem; using Wino.Extensions; -using Wino.Mail.ViewModels.Data; namespace Wino.Controls; @@ -21,7 +19,6 @@ public sealed partial class MailItemDisplayInformationControl : UserControl public static readonly DependencyProperty DisplayModeProperty = DependencyProperty.Register(nameof(DisplayMode), typeof(MailListDisplayMode), typeof(MailItemDisplayInformationControl), new PropertyMetadata(MailListDisplayMode.Spacious)); public static readonly DependencyProperty ShowPreviewTextProperty = DependencyProperty.Register(nameof(ShowPreviewText), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(true)); - public static readonly DependencyProperty IsCustomFocusedProperty = DependencyProperty.Register(nameof(IsCustomFocused), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(false)); public static readonly DependencyProperty IsAvatarVisibleProperty = DependencyProperty.Register(nameof(IsAvatarVisible), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(true)); public static readonly DependencyProperty IsSubjectVisibleProperty = DependencyProperty.Register(nameof(IsSubjectVisible), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(true)); public static readonly DependencyProperty ConnectedExpanderProperty = DependencyProperty.Register(nameof(ConnectedExpander), typeof(WinoExpander), typeof(MailItemDisplayInformationControl), new PropertyMetadata(null)); @@ -29,7 +26,7 @@ public sealed partial class MailItemDisplayInformationControl : UserControl public static readonly DependencyProperty CenterHoverActionProperty = DependencyProperty.Register(nameof(CenterHoverAction), typeof(MailOperation), typeof(MailItemDisplayInformationControl), new PropertyMetadata(MailOperation.None)); public static readonly DependencyProperty RightHoverActionProperty = DependencyProperty.Register(nameof(RightHoverAction), typeof(MailOperation), typeof(MailItemDisplayInformationControl), new PropertyMetadata(MailOperation.None)); public static readonly DependencyProperty HoverActionExecutedCommandProperty = DependencyProperty.Register(nameof(HoverActionExecutedCommand), typeof(ICommand), typeof(MailItemDisplayInformationControl), new PropertyMetadata(null)); - public static readonly DependencyProperty MailItemProperty = DependencyProperty.Register(nameof(MailItem), typeof(IMailItem), typeof(MailItemDisplayInformationControl), new PropertyMetadata(null, new PropertyChangedCallback(OnMailItemChanged))); + public static readonly DependencyProperty MailItemProperty = DependencyProperty.Register(nameof(MailItem), typeof(MailCopy), typeof(MailItemDisplayInformationControl), new PropertyMetadata(null, new PropertyChangedCallback(OnMailItemChanged))); public static readonly DependencyProperty IsHoverActionsEnabledProperty = DependencyProperty.Register(nameof(IsHoverActionsEnabled), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(true)); public static readonly DependencyProperty Prefer24HourTimeFormatProperty = DependencyProperty.Register(nameof(Prefer24HourTimeFormat), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(false)); public static readonly DependencyProperty IsThreadExpanderVisibleProperty = DependencyProperty.Register(nameof(IsThreadExpanderVisible), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(false)); @@ -66,9 +63,9 @@ public sealed partial class MailItemDisplayInformationControl : UserControl set { SetValue(IsHoverActionsEnabledProperty, value); } } - public IMailItem MailItem + public MailCopy MailItem { - get { return (IMailItem)GetValue(MailItemProperty); } + get { return (MailCopy)GetValue(MailItemProperty); } set { SetValue(MailItemProperty, value); } } @@ -114,11 +111,6 @@ public sealed partial class MailItemDisplayInformationControl : UserControl set { SetValue(IsAvatarVisibleProperty, value); } } - public bool IsCustomFocused - { - get { return (bool)GetValue(IsCustomFocusedProperty); } - set { SetValue(IsCustomFocusedProperty, value); } - } public bool ShowPreviewText { @@ -189,12 +181,12 @@ public sealed partial class MailItemDisplayInformationControl : UserControl MailOperationPreperationRequest package = null; - if (MailItem is MailCopy mailCopy) - package = new MailOperationPreperationRequest(operation, mailCopy, toggleExecution: true); - else if (MailItem is ThreadMailItemViewModel threadMailItemViewModel) - package = new MailOperationPreperationRequest(operation, threadMailItemViewModel.GetMailCopies(), toggleExecution: true); - else if (MailItem is ThreadMailItem threadMailItem) - package = new MailOperationPreperationRequest(operation, threadMailItem.ThreadItems.Cast().Select(a => a.MailCopy), toggleExecution: true); + //if (MailItem is MailCopy mailCopy) + // package = new MailOperationPreperationRequest(operation, mailCopy, toggleExecution: true); + //else if (MailItem is ThreadMailItemViewModel threadMailItemViewModel) + // package = new MailOperationPreperationRequest(operation, threadMailItemViewModel.GetMailCopies(), toggleExecution: true); + //else if (MailItem is ThreadMailItem threadMailItem) + // package = new MailOperationPreperationRequest(operation, threadMailItem.ThreadItems.Cast().Select(a => a.MailCopy), toggleExecution: true); if (package == null) return; diff --git a/Wino.Mail.WinUI/Controls/WinoSwipeControlItems.cs b/Wino.Mail.WinUI/Controls/WinoSwipeControlItems.cs deleted file mode 100644 index 3d6e836e..00000000 --- a/Wino.Mail.WinUI/Controls/WinoSwipeControlItems.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System.Linq; -using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml; -using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Models.MailItem; -using Wino.Helpers; -using Wino.Mail.ViewModels.Data; - -namespace Wino.Controls; - -public partial class WinoSwipeControlItems : SwipeItems -{ - public static readonly DependencyProperty SwipeOperationProperty = DependencyProperty.Register(nameof(SwipeOperation), typeof(MailOperation), typeof(WinoSwipeControlItems), new PropertyMetadata(default(MailOperation), new PropertyChangedCallback(OnItemsChanged))); - public static readonly DependencyProperty MailItemProperty = DependencyProperty.Register(nameof(MailItem), typeof(IMailItem), typeof(WinoSwipeControlItems), new PropertyMetadata(null)); - - public IMailItem MailItem - { - get { return (IMailItem)GetValue(MailItemProperty); } - set { SetValue(MailItemProperty, value); } - } - - - public MailOperation SwipeOperation - { - get { return (MailOperation)GetValue(SwipeOperationProperty); } - set { SetValue(SwipeOperationProperty, value); } - } - - private static void OnItemsChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args) - { - if (obj is WinoSwipeControlItems control) - { - control.BuildSwipeItems(); - } - } - - private void BuildSwipeItems() - { - this.Clear(); - - var swipeItem = GetSwipeItem(SwipeOperation); - - this.Add(swipeItem); - } - - private SwipeItem GetSwipeItem(MailOperation operation) - { - if (MailItem == null) return null; - - var finalOperation = operation; - - bool isSingleItem = MailItem is MailItemViewModel; - - if (isSingleItem) - { - var singleItem = MailItem as MailItemViewModel; - - if (operation == MailOperation.MarkAsRead && singleItem.IsRead) - finalOperation = MailOperation.MarkAsUnread; - else if (operation == MailOperation.MarkAsUnread && !singleItem.IsRead) - finalOperation = MailOperation.MarkAsRead; - } - else - { - var threadItem = MailItem as ThreadMailItemViewModel; - - if (operation == MailOperation.MarkAsRead && threadItem.ThreadItems.All(a => a.IsRead)) - finalOperation = MailOperation.MarkAsUnread; - else if (operation == MailOperation.MarkAsUnread && threadItem.ThreadItems.All(a => !a.IsRead)) - finalOperation = MailOperation.MarkAsRead; - } - - var item = new SwipeItem() - { - IconSource = new WinoFontIconSource() { Icon = XamlHelpers.GetWinoIconGlyph(finalOperation) }, - Text = XamlHelpers.GetOperationString(finalOperation), - }; - - return item; - } -} diff --git a/Wino.Mail.WinUI/Selectors/MailItemContainerSelector.cs b/Wino.Mail.WinUI/Selectors/MailItemContainerSelector.cs new file mode 100644 index 00000000..f67ba578 --- /dev/null +++ b/Wino.Mail.WinUI/Selectors/MailItemContainerSelector.cs @@ -0,0 +1,27 @@ +using System; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Wino.Mail.ViewModels.Collections; +using Wino.Mail.ViewModels.Data; + +namespace Wino.Mail.WinUI.Selectors; + +public partial class MailItemContainerSelector : DataTemplateSelector +{ + public DataTemplate? EmailTemplate { get; set; } + public DataTemplate? ThreadExpanderTemplate { get; set; } + public DataTemplate? DateGroupHeaderTemplate { get; set; } + public DataTemplate? SenderGroupHeaderTemplate { get; set; } + + protected override DataTemplate SelectTemplateCore(object item) + { + return item switch + { + MailItemViewModel => EmailTemplate ?? throw new ArgumentNullException(nameof(EmailTemplate)), + ThreadMailItemViewModel => ThreadExpanderTemplate ?? throw new ArgumentNullException(nameof(ThreadExpanderTemplate)), + DateGroupHeader => DateGroupHeaderTemplate ?? throw new ArgumentNullException(nameof(DateGroupHeaderTemplate)), + SenderGroupHeader => SenderGroupHeaderTemplate ?? throw new ArgumentNullException(nameof(SenderGroupHeaderTemplate)), + _ => base.SelectTemplateCore(item) + }; + } +} diff --git a/Wino.Mail.WinUI/Views/MailListPage.xaml b/Wino.Mail.WinUI/Views/MailListPage.xaml index eb99052e..308f6d93 100644 --- a/Wino.Mail.WinUI/Views/MailListPage.xaml +++ b/Wino.Mail.WinUI/Views/MailListPage.xaml @@ -3,23 +3,26 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:abstract="using:Wino.Views.Abstract" + xmlns:advanced="using:Wino.Mail.WinUI.Controls.Advanced" xmlns:animatedvisuals="using:Microsoft.UI.Xaml.Controls.AnimatedVisuals" + xmlns:animations="using:CommunityToolkit.WinUI.Animations" xmlns:collections="using:CommunityToolkit.Mvvm.Collections" xmlns:controls="using:Wino.Controls" xmlns:converters="using:Wino.Converters" xmlns:coreControls="using:Wino.Core.WinUI.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:data="using:Wino.Mail.ViewModels.Collections" xmlns:domain="using:Wino.Core.Domain" xmlns:enums="using:Wino.Core.Domain.Enums" xmlns:helpers="using:Wino.Helpers" xmlns:i="using:Microsoft.Xaml.Interactivity" xmlns:interactivity="using:Microsoft.Xaml.Interactivity" - xmlns:listview="using:Wino.Controls.Advanced" xmlns:local="using:Wino.Behaviors" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:menuflyouts="using:Wino.MenuFlyouts" xmlns:muxc="using:Microsoft.UI.Xaml.Controls" xmlns:selectors="using:Wino.Selectors" + xmlns:selectors1="using:Wino.Mail.WinUI.Selectors" xmlns:toolkit="using:CommunityToolkit.WinUI.Controls" xmlns:toolkitExt="using:CommunityToolkit.WinUI" xmlns:viewModelData="using:Wino.Mail.ViewModels.Data" @@ -30,17 +33,11 @@ mc:Ignorable="d"> - 0,0,0,0 0,0,12,0 0,0,0,0 - Transparent - Transparent Transparent @@ -49,155 +46,210 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Transparent + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -303,8 +355,7 @@ VerticalAlignment="Center" Canvas.ZIndex="100" Checked="SelectAllCheckboxChecked" - Unchecked="SelectAllCheckboxUnchecked" - Visibility="{x:Bind helpers:XamlHelpers.IsSelectionModeMultiple(MailListView.SelectionMode), Mode=OneWay}" /> + Unchecked="SelectAllCheckboxUnchecked" /> @@ -452,70 +503,14 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/Wino.Mail.WinUI/Views/MailListPage.xaml.cs b/Wino.Mail.WinUI/Views/MailListPage.xaml.cs index 233cb411..b9938303 100644 --- a/Wino.Mail.WinUI/Views/MailListPage.xaml.cs +++ b/Wino.Mail.WinUI/Views/MailListPage.xaml.cs @@ -7,19 +7,19 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Hosting; using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Media.Animation; using Microsoft.UI.Xaml.Navigation; using MoreLinq; using Windows.Foundation; -using Wino.Controls; -using Wino.Controls.Advanced; 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.Menus; using Wino.Core.Domain.Models.Navigation; +using Wino.Helpers; using Wino.Mail.ViewModels.Data; using Wino.Mail.ViewModels.Messages; using Wino.Mail.WinUI; @@ -91,7 +91,7 @@ public sealed partial class MailListPage : MailListPageAbstract, SelectAllCheckbox.Checked -= SelectAllCheckboxChecked; SelectAllCheckbox.Unchecked -= SelectAllCheckboxUnchecked; - SelectAllCheckbox.IsChecked = MailListView.Items.Count > 0 && MailListView.SelectedItems.Count == MailListView.Items.Count; + SelectAllCheckbox.IsChecked = MailListView.CastedItemsSource?.Count() > 0 && MailListView.SelectedItems.Count == MailListView.CastedItemsSource.Count(); SelectAllCheckbox.Checked += SelectAllCheckboxChecked; SelectAllCheckbox.Unchecked += SelectAllCheckboxUnchecked; @@ -120,10 +120,10 @@ public sealed partial class MailListPage : MailListPageAbstract, } } - SelectAllCheckbox.IsChecked = false; + // SelectAllCheckbox.IsChecked = false; SelectionModeToggle.IsChecked = false; - MailListView.ClearSelections(); + // MailListView.ClearSelections(); UpdateSelectAllButtonStatus(); ViewModel.SelectedPivotChangedCommand.Execute(null); @@ -131,7 +131,8 @@ public sealed partial class MailListPage : MailListPageAbstract, private void ChangeSelectionMode(ListViewSelectionMode mode) { - MailListView.ChangeSelectionMode(mode); + // ItemsView doesn't have a ChangeSelectionMode method like ListView + // The selection mode is set in XAML and doesn't need to change dynamically for ItemsView if (ViewModel?.PivotFolders != null) { @@ -146,44 +147,46 @@ public sealed partial class MailListPage : MailListPageAbstract, private void SelectAllCheckboxChecked(object sender, RoutedEventArgs e) { - MailListView.SelectAllWino(); + // MailListView.SelectAllWino(); } private void SelectAllCheckboxUnchecked(object sender, RoutedEventArgs e) { - MailListView.ClearSelections(); + // MailListView.ClearSelections(); } private async void MailItemContextRequested(UIElement sender, ContextRequestedEventArgs args) { + // TODO: New ItemsView implementation. + // Context is requested from a single mail point, but we might have multiple selected items. // This menu should be calculated based on all selected items by providers. - if (sender is MailItemDisplayInformationControl control && args.TryGetPosition(sender, out Point p)) - { - await FocusManager.TryFocusAsync(control, FocusState.Keyboard); + //if (sender is MailItemDisplayInformationControl control && args.TryGetPosition(sender, out Point p)) + //{ + // await FocusManager.TryFocusAsync(control, FocusState.Keyboard); - if (control.DataContext is IMailItem clickedMailItemContext) - { - var targetItems = ViewModel.GetTargetMailItemViewModels(clickedMailItemContext); - var availableActions = ViewModel.GetAvailableMailActions(targetItems); + // if (control.DataContext is IMailItem clickedMailItemContext) + // { + // var targetItems = ViewModel.GetTargetMailItemViewModels(clickedMailItemContext); + // var availableActions = ViewModel.GetAvailableMailActions(targetItems); - if (!availableActions?.Any() ?? false) return; - var t = targetItems.ElementAt(0); + // if (!availableActions?.Any() ?? false) return; + // var t = targetItems.ElementAt(0); - ViewModel.ChangeCustomFocusedState(targetItems, true); + // ViewModel.ChangeCustomFocusedState(targetItems, true); - var clickedOperation = await GetMailOperationFromFlyoutAsync(availableActions, control, p.X, p.Y); + // var clickedOperation = await GetMailOperationFromFlyoutAsync(availableActions, control, p.X, p.Y); - ViewModel.ChangeCustomFocusedState(targetItems, false); + // ViewModel.ChangeCustomFocusedState(targetItems, false); - if (clickedOperation == null) return; + // if (clickedOperation == null) return; - var prepRequest = new MailOperationPreperationRequest(clickedOperation.Operation, targetItems.Select(a => a.MailCopy)); + // var prepRequest = new MailOperationPreperationRequest(clickedOperation.Operation, targetItems.Select(a => a.MailCopy)); - await ViewModel.ExecuteMailOperationAsync(prepRequest); - } - } + // await ViewModel.ExecuteMailOperationAsync(prepRequest); + // } + //} } private async Task GetMailOperationFromFlyoutAsync(IEnumerable availableActions, @@ -206,7 +209,7 @@ public sealed partial class MailListPage : MailListPageAbstract, void IRecipient.Receive(ClearMailSelectionsRequested message) { - MailListView.ClearSelections(null, preserveThreadExpanding: true); + // MailListView.ClearSelections(null, preserveThreadExpanding: true); } void IRecipient.Receive(ActiveMailItemChangedEvent message) @@ -309,46 +312,46 @@ public sealed partial class MailListPage : MailListPageAbstract, { if (message.SelectedMailViewModel == null) return; - await ViewModel.ExecuteUIThread(async () => - { - MailListView.ClearSelections(message.SelectedMailViewModel, true); + //await ViewModel.ExecuteUIThread(async () => + //{ + // MailListView.ClearSelections(message.SelectedMailViewModel, true); - int retriedSelectionCount = 0; - trySelection: + // int retriedSelectionCount = 0; + //trySelection: - bool isSelected = MailListView.SelectMailItemContainer(message.SelectedMailViewModel); + // bool isSelected = MailListView.SelectMailItemContainer(message.SelectedMailViewModel); - if (!isSelected) - { - for (int i = retriedSelectionCount; i < 5;) - { - // Retry with delay until the container is realized. Max 1 second. - await Task.Delay(200); + // if (!isSelected) + // { + // for (int i = retriedSelectionCount; i < 5;) + // { + // // Retry with delay until the container is realized. Max 1 second. + // await Task.Delay(200); - retriedSelectionCount++; + // retriedSelectionCount++; - goto trySelection; - } - } + // goto trySelection; + // } + // } - // Automatically scroll to the selected item. - // This is useful when creating draft. - if (isSelected && message.ScrollToItem) - { - var collectionContainer = ViewModel.MailCollection.GetMailItemContainer(message.SelectedMailViewModel.UniqueId); + // // Automatically scroll to the selected item. + // // This is useful when creating draft. + // if (isSelected && message.ScrollToItem) + // { + // var collectionContainer = ViewModel.MailCollection.GetMailItemContainer(message.SelectedMailViewModel.UniqueId); - // Scroll to thread if available. - if (collectionContainer.ThreadViewModel != null) - { - MailListView.ScrollIntoView(collectionContainer.ThreadViewModel, ScrollIntoViewAlignment.Default); - } - else if (collectionContainer.ItemViewModel != null) - { - MailListView.ScrollIntoView(collectionContainer.ItemViewModel, ScrollIntoViewAlignment.Default); - } + // // Scroll to thread if available. + // if (collectionContainer.ThreadViewModel != null) + // { + // MailListView.StartBringItemIntoView(collectionContainer.ThreadViewModel, new BringIntoViewOptions()); + // } + // else if (collectionContainer.ItemViewModel != null) + // { + // MailListView.StartBringItemIntoView(collectionContainer.ItemViewModel, new BringIntoViewOptions()); + // } - } - }); + // } + //}); } private void SearchBoxFocused(object sender, RoutedEventArgs e) @@ -367,32 +370,29 @@ public sealed partial class MailListPage : MailListPageAbstract, /// private void ThreadHeaderDragStart(UIElement sender, DragStartingEventArgs args) { - if (sender is MailItemDisplayInformationControl control - && control.ConnectedExpander?.Content is WinoListView contentListView) - { - var allItems = contentListView.Items.Where(a => a is IMailItem); + //if (sender is MailItemDisplayInformationControl control + // && control.ConnectedExpander?.Content is WinoListView contentListView) + //{ + // var allItems = contentListView.Items.Where(a => a is MailCopy); - // Highlight all items. - allItems.Cast().ForEach(a => a.IsCustomFocused = true); + // // Highlight all items. + // allItems.Cast().ForEach(a => a.IsCustomFocused = true); - // Set native drag arg properties. - args.AllowedOperations = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move; + // // Set native drag arg properties. + // args.AllowedOperations = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move; - var dragPackage = new MailDragPackage(allItems.Cast()); + // var dragPackage = new MailDragPackage(allItems.Cast()); - args.Data.Properties.Add(nameof(MailDragPackage), dragPackage); - args.DragUI.SetContentFromDataPackage(); + // args.Data.Properties.Add(nameof(MailDragPackage), dragPackage); + // args.DragUI.SetContentFromDataPackage(); - control.ConnectedExpander.IsExpanded = true; - } + // control.ConnectedExpander.IsExpanded = true; + //} } private void ThreadHeaderDragFinished(UIElement sender, DropCompletedEventArgs args) { - if (sender is MailItemDisplayInformationControl control && control.ConnectedExpander != null && control.ConnectedExpander.Content is WinoListView contentListView) - { - contentListView.Items.Where(a => a is MailItemViewModel).Cast().ForEach(a => a.IsCustomFocused = false); - } + } private async void LeftSwipeItemInvoked(Microsoft.UI.Xaml.Controls.SwipeItem sender, Microsoft.UI.Xaml.Controls.SwipeItemInvokedEventArgs args) @@ -410,7 +410,7 @@ public sealed partial class MailListPage : MailListPageAbstract, } else if (swipeControl.Tag is ThreadMailItemViewModel threadMailItemViewModel) { - var package = new MailOperationPreperationRequest(MailOperation.SoftDelete, threadMailItemViewModel.GetMailCopies()); + var package = new MailOperationPreperationRequest(MailOperation.SoftDelete, threadMailItemViewModel.ThreadEmails.Select(a => a.MailCopy)); await ViewModel.ExecuteMailOperationAsync(package); } } @@ -432,19 +432,15 @@ public sealed partial class MailListPage : MailListPageAbstract, } else if (swipeControl.Tag is ThreadMailItemViewModel threadMailItemViewModel) { - bool isAllRead = threadMailItemViewModel.ThreadItems.All(a => a.IsRead); + bool isAllRead = threadMailItemViewModel.ThreadEmails.All(a => a.IsRead); var operation = isAllRead ? MailOperation.MarkAsUnread : MailOperation.MarkAsRead; - var package = new MailOperationPreperationRequest(operation, threadMailItemViewModel.GetMailCopies()); + var package = new MailOperationPreperationRequest(operation, threadMailItemViewModel.ThreadEmails.Select(a => a.MailCopy)); await ViewModel.ExecuteMailOperationAsync(package); } } - private void PullToRefreshRequested(Microsoft.UI.Xaml.Controls.RefreshContainer sender, Microsoft.UI.Xaml.Controls.RefreshRequestedEventArgs args) - { - ViewModel.SyncFolderCommand?.Execute(null); - } private async void SearchBar_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args) { @@ -504,8 +500,65 @@ public sealed partial class MailListPage : MailListPageAbstract, } private void SelectAllInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args) - => MailListView.SelectAllWino(); + { + // MailListView.SelectAllWino(); + } private void DeleteAllInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args) => ViewModel.ExecuteMailOperationCommand.Execute(MailOperation.SoftDelete); + + private void ThreadContainerTapped(object sender, TappedRoutedEventArgs e) + { + if (sender is ItemContainer container && container.Tag is ThreadMailItemViewModel expander) + { + // Toggle expansion state + expander.IsThreadExpanded = !expander.IsThreadExpanded; + + // Find the expander icon and animate its rotation using Composition APIs + var expanderIcon = WinoVisualTreeHelper.GetChildObject(container, "ExpanderIcon"); + if (expanderIcon != null) + { + var targetAngle = expander.IsThreadExpanded ? 90f : 0f; + AnimateRotationWithComposition(expanderIcon, targetAngle); + } + } + } + + /// + /// Animates the rotation using high-performance Composition APIs + /// + private void AnimateRotationWithComposition(FrameworkElement element, float targetAngleInDegrees) + { + // Get the element's visual from the composition layer + var visual = ElementCompositionPreview.GetElementVisual(element); + var compositor = visual.Compositor; + + // Set the center point for rotation (center of the element) + visual.CenterPoint = new System.Numerics.Vector3( + (float)element.ActualWidth / 2f, + (float)element.ActualHeight / 2f, + 0f); + + // Create a rotation animation + var rotationAnimation = compositor.CreateScalarKeyFrameAnimation(); + rotationAnimation.Target = "RotationAngleInDegrees"; + rotationAnimation.Duration = TimeSpan.FromMilliseconds(200); + + // Add easing function for smooth animation + var easingFunction = compositor.CreateCubicBezierEasingFunction( + new System.Numerics.Vector2(0.25f, 0.1f), // Control point 1 + new System.Numerics.Vector2(0.25f, 1f)); // Control point 2 (similar to CircleEase) + + // Insert keyframe with the target angle and easing + rotationAnimation.InsertKeyFrame(1.0f, targetAngleInDegrees, easingFunction); + + // Start the animation + visual.StartAnimation("RotationAngleInDegrees", rotationAnimation); + } + + private void ListSelectionChanged(ItemsView sender, ItemsViewSelectionChangedEventArgs args) + { + UpdateSelectAllButtonStatus(); + UpdateAdaptiveness(); + } } diff --git a/Wino.Messages/Client/Accounts/AccountMenuItemExtended.cs b/Wino.Messages/Client/Accounts/AccountMenuItemExtended.cs index e4d3b5bb..6f518b7a 100644 --- a/Wino.Messages/Client/Accounts/AccountMenuItemExtended.cs +++ b/Wino.Messages/Client/Accounts/AccountMenuItemExtended.cs @@ -1,5 +1,5 @@ using System; -using Wino.Core.Domain.Models.MailItem; +using Wino.Core.Domain.Entities.Mail; namespace Wino.Messaging.Client.Accounts; @@ -10,4 +10,4 @@ namespace Wino.Messaging.Client.Accounts; /// Account to extend menu item for. /// Folder to select after expansion. /// Mail item to select if possible in the expanded folder. -public record AccountMenuItemExtended(Guid FolderId, IMailItem NavigateMailItem); +public record AccountMenuItemExtended(Guid FolderId, MailCopy NavigateMailItem); diff --git a/Wino.Messages/Server/DownloadMissingMessageRequested.cs b/Wino.Messages/Server/DownloadMissingMessageRequested.cs index 5cbe8eae..7c994480 100644 --- a/Wino.Messages/Server/DownloadMissingMessageRequested.cs +++ b/Wino.Messages/Server/DownloadMissingMessageRequested.cs @@ -1,6 +1,6 @@ using System; +using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.MailItem; namespace Wino.Messaging.Server; @@ -10,4 +10,4 @@ namespace Wino.Messaging.Server; /// /// Account id for corresponding synchronizer. /// Mail copy id to download. -public record DownloadMissingMessageRequested(Guid AccountId, IMailItem MailItem) : IClientMessage; +public record DownloadMissingMessageRequested(Guid AccountId, MailCopy MailItem) : IClientMessage; diff --git a/Wino.Messages/Wino.Messaging.csproj b/Wino.Messages/Wino.Messaging.csproj index c189f718..3c71bc13 100644 --- a/Wino.Messages/Wino.Messaging.csproj +++ b/Wino.Messages/Wino.Messaging.csproj @@ -9,9 +9,6 @@ - - - diff --git a/Wino.Services/ContextMenuItemService.cs b/Wino.Services/ContextMenuItemService.cs index a0847fe9..c50c8716 100644 --- a/Wino.Services/ContextMenuItemService.cs +++ b/Wino.Services/ContextMenuItemService.cs @@ -1,9 +1,9 @@ using System.Collections.Generic; using System.Linq; +using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Folders; -using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.Menus; namespace Wino.Services; @@ -35,7 +35,7 @@ public class ContextMenuItemService : IContextMenuItemService return list; } - public virtual IEnumerable GetMailItemContextMenuActions(IEnumerable selectedMailItems) + public virtual IEnumerable GetMailItemContextMenuActions(IEnumerable selectedMailItems) { if (selectedMailItems == null) return default; @@ -50,7 +50,7 @@ public class ContextMenuItemService : IContextMenuItemService bool isSingleItem = selectedMailItems.Count() == 1; - IMailItem singleItem = selectedMailItems.FirstOrDefault(); + MailCopy singleItem = selectedMailItems.FirstOrDefault(); // Archive button. @@ -125,7 +125,7 @@ public class ContextMenuItemService : IContextMenuItemService return operationList; } - public virtual IEnumerable GetMailItemRenderMenuActions(IMailItem mailItem, bool isDarkEditor) + public virtual IEnumerable GetMailItemRenderMenuActions(MailCopy mailItem, bool isDarkEditor) { var actionList = new List(); diff --git a/Wino.Services/MailService.cs b/Wino.Services/MailService.cs index bcc9739a..f3489f4a 100644 --- a/Wino.Services/MailService.cs +++ b/Wino.Services/MailService.cs @@ -14,7 +14,6 @@ using Wino.Core.Domain.Enums; using Wino.Core.Domain.Exceptions; using Wino.Core.Domain.Extensions; using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.Comparers; using Wino.Core.Domain.Models.MailItem; using Wino.Messaging.UI; using Wino.Services.Extensions; @@ -29,7 +28,6 @@ public class MailService : BaseDatabaseService, IMailService private readonly IContactService _contactService; private readonly IAccountService _accountService; private readonly ISignatureService _signatureService; - private readonly IThreadingStrategyProvider _threadingStrategyProvider; private readonly IMimeFileService _mimeFileService; private readonly IPreferencesService _preferencesService; @@ -40,7 +38,6 @@ public class MailService : BaseDatabaseService, IMailService IContactService contactService, IAccountService accountService, ISignatureService signatureService, - IThreadingStrategyProvider threadingStrategyProvider, IMimeFileService mimeFileService, IPreferencesService preferencesService) : base(databaseService) { @@ -48,7 +45,6 @@ public class MailService : BaseDatabaseService, IMailService _contactService = contactService; _accountService = accountService; _signatureService = signatureService; - _threadingStrategyProvider = threadingStrategyProvider; _mimeFileService = mimeFileService; _preferencesService = preferencesService; } @@ -209,7 +205,7 @@ public class MailService : BaseDatabaseService, IMailService return query.GetRawQuery(); } - public async Task> FetchMailsAsync(MailListInitializationOptions options, CancellationToken cancellationToken = default) + public async Task> FetchMailsAsync(MailListInitializationOptions options, CancellationToken cancellationToken = default) { List mails = null; @@ -241,68 +237,20 @@ public class MailService : BaseDatabaseService, IMailService // Remove items that has no assigned account or folder. mails.RemoveAll(a => a.AssignedAccount == null || a.AssignedFolder == null); - if (!options.CreateThreads) - { - cancellationToken.ThrowIfCancellationRequested(); - - // Threading is disabled. Just return everything as it is. - mails.Sort(options.SortingOptionType == SortingOptionType.ReceiveDate ? new DateComparer() : new NameComparer()); - - return [.. mails]; - } - - // Populate threaded items. - - List threadedItems = []; - - // Each account items must be threaded separately. - foreach (var group in mails.GroupBy(a => a.AssignedAccount.Id)) - { - cancellationToken.ThrowIfCancellationRequested(); - - var accountId = group.Key; - var groupAccount = mails.First(a => a.AssignedAccount.Id == accountId).AssignedAccount; - - var threadingStrategy = _threadingStrategyProvider.GetStrategy(groupAccount.ProviderType); - - // Only thread items from Draft and Sent folders must present here. - // Otherwise this strategy will fetch the items that are in Deleted folder as well. - var accountThreadedItems = await threadingStrategy.ThreadItemsAsync([.. group], options.Folders.First()); - - // Populate threaded items with folder and account assignments. - // Almost everything already should be in cache from initial population. - foreach (var mail in accountThreadedItems) - { - cancellationToken.ThrowIfCancellationRequested(); - await LoadAssignedPropertiesWithCacheAsync(mail, folderCache, accountCache, contactCache).ConfigureAwait(false); - } - - if (accountThreadedItems != null) - { - threadedItems.AddRange(accountThreadedItems); - } - } - - threadedItems.Sort(options.SortingOptionType == SortingOptionType.ReceiveDate ? new DateComparer() : new NameComparer()); cancellationToken.ThrowIfCancellationRequested(); - return threadedItems; + // Threading is disabled. Just return everything as it is. + // mails.Sort(options.SortingOptionType == SortingOptionType.ReceiveDate ? new DateComparer() : new NameComparer()); + + return [.. mails]; } /// /// This method should used for operations with multiple mailItems. Don't use this for single mail items. /// Called method should provide own instances for caches. /// - private async Task LoadAssignedPropertiesWithCacheAsync(IMailItem mail, Dictionary folderCache, Dictionary accountCache, Dictionary contactCache) + private async Task LoadAssignedPropertiesWithCacheAsync(MailCopy mail, Dictionary folderCache, Dictionary accountCache, Dictionary contactCache) { - if (mail is ThreadMailItem threadMailItem) - { - foreach (var childMail in threadMailItem.ThreadItems) - { - await LoadAssignedPropertiesWithCacheAsync(childMail, folderCache, accountCache, contactCache).ConfigureAwait(false); - } - } - if (mail is MailCopy mailCopy) { var isFolderCached = folderCache.TryGetValue(mailCopy.FolderId, out MailItemFolder folderAssignment); diff --git a/Wino.Services/ServicesContainerSetup.cs b/Wino.Services/ServicesContainerSetup.cs index 894edc60..f8cea686 100644 --- a/Wino.Services/ServicesContainerSetup.cs +++ b/Wino.Services/ServicesContainerSetup.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.DependencyInjection; using Wino.Core.Domain.Interfaces; -using Wino.Services.Threading; namespace Wino.Services; @@ -24,12 +23,5 @@ public static class ServicesContainerSetup services.AddTransient(); services.AddTransient(); services.AddTransient(); - - services.AddSingleton(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - - } } diff --git a/Wino.Services/Threading/APIThreadingStrategy.cs b/Wino.Services/Threading/APIThreadingStrategy.cs deleted file mode 100644 index 28a97544..00000000 --- a/Wino.Services/Threading/APIThreadingStrategy.cs +++ /dev/null @@ -1,137 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Wino.Core.Domain.Entities.Mail; -using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.Folders; -using Wino.Core.Domain.Models.MailItem; -using Wino.Services; - -namespace Wino.Services.Threading; - -public class APIThreadingStrategy : IThreadingStrategy -{ - private readonly IDatabaseService _databaseService; - private readonly IFolderService _folderService; - - public APIThreadingStrategy(IDatabaseService databaseService, IFolderService folderService) - { - _databaseService = databaseService; - _folderService = folderService; - } - - public virtual bool ShouldThreadWithItem(IMailItem originalItem, IMailItem targetItem) - { - return originalItem.ThreadId != null && originalItem.ThreadId == targetItem.ThreadId; - } - - /// - public async Task> ThreadItemsAsync(List items, IMailItemFolder threadingForFolder) - { - var assignedAccount = items[0].AssignedAccount; - - var sentFolder = await _folderService.GetSpecialFolderByAccountIdAsync(assignedAccount.Id, SpecialFolderType.Sent); - var draftFolder = await _folderService.GetSpecialFolderByAccountIdAsync(assignedAccount.Id, SpecialFolderType.Draft); - - if (sentFolder == null || draftFolder == null) return default; - - // True: Non threaded items. - // False: Potentially threaded items. - var nonThreadedOrThreadedMails = items - .Distinct() - .GroupBy(x => string.IsNullOrEmpty(x.ThreadId)) - .ToDictionary(x => x.Key, x => x); - - _ = nonThreadedOrThreadedMails.TryGetValue(true, out var nonThreadedMails); - var isThreadedItems = nonThreadedOrThreadedMails.TryGetValue(false, out var potentiallyThreadedMails); - - List resultList = nonThreadedMails is null ? [] : [.. nonThreadedMails]; - - if (isThreadedItems) - { - var threadItems = (await GetThreadItemsAsync(potentiallyThreadedMails.Select(x => (x.ThreadId, x.AssignedFolder)).ToList(), assignedAccount.Id, sentFolder.Id, draftFolder.Id)) - .GroupBy(x => x.ThreadId); - - foreach (var threadItem in threadItems) - { - if (threadItem.Count() == 1) - { - resultList.Add(threadItem.First()); - continue; - } - - var thread = new ThreadMailItem(); - - foreach (var childThreadItem in threadItem) - { - if (thread.ThreadItems.Any(a => a.Id == childThreadItem.Id)) - { - // Mail already exist in the thread. - // There should be only 1 instance of the mail in the thread. - // Make sure we add the correct one. - - // Add the one with threading folder. - var threadingFolderItem = threadItem.FirstOrDefault(a => a.Id == childThreadItem.Id && a.FolderId == threadingForFolder.Id); - - if (threadingFolderItem == null) continue; - - // Remove the existing one. - thread.ThreadItems.Remove(thread.ThreadItems.First(a => a.Id == childThreadItem.Id)); - - // Add the correct one for listing. - thread.AddThreadItem(threadingFolderItem); - } - else - { - thread.AddThreadItem(childThreadItem); - } - } - - if (thread.ThreadItems.Count > 1) - { - resultList.Add(thread); - } - else - { - // Don't make threads if the thread has only one item. - // Gmail has may have multiple assignments for the same item. - - resultList.Add(thread.ThreadItems.First()); - } - } - } - - return resultList; - } - - private async Task> GetThreadItemsAsync(List<(string threadId, MailItemFolder threadingFolder)> potentialThread, - Guid accountId, - Guid sentFolderId, - Guid draftFolderId) - { - // Only items from the folder that we are threading for, sent and draft folder items must be included. - // This is important because deleted items or item assignments that belongs to different folder is - // affecting the thread creation here. - - // If the threading is done from Sent or Draft folder, include everything... - - // TODO: Convert to SQLKata query. - - var query = @$"SELECT DISTINCT MC.* FROM MailCopy MC - INNER JOIN MailItemFolder MF on MF.Id = MC.FolderId - WHERE MF.MailAccountId == '{accountId}' AND - ({string.Join(" OR ", potentialThread.Select(x => ConditionForItem(x, sentFolderId, draftFolderId)))})"; - - return await _databaseService.Connection.QueryAsync(query); - - static string ConditionForItem((string threadId, MailItemFolder threadingFolder) potentialThread, Guid sentFolderId, Guid draftFolderId) - { - if (potentialThread.threadingFolder.SpecialFolderType == SpecialFolderType.Draft || potentialThread.threadingFolder.SpecialFolderType == SpecialFolderType.Sent) - return $"(MC.ThreadId = '{potentialThread.threadId}')"; - - return $"(MC.ThreadId = '{potentialThread.threadId}' AND MC.FolderId IN ('{potentialThread.threadingFolder.Id}','{sentFolderId}','{draftFolderId}'))"; - } - } -} diff --git a/Wino.Services/Threading/GmailThreadingStrategy.cs b/Wino.Services/Threading/GmailThreadingStrategy.cs deleted file mode 100644 index b0e41794..00000000 --- a/Wino.Services/Threading/GmailThreadingStrategy.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Wino.Core.Domain.Interfaces; - -namespace Wino.Services.Threading; - -public class GmailThreadingStrategy : APIThreadingStrategy, IGmailThreadingStrategy -{ - public GmailThreadingStrategy(IDatabaseService databaseService, IFolderService folderService) : base(databaseService, folderService) { } -} diff --git a/Wino.Services/Threading/ImapThreadingStrategy.cs b/Wino.Services/Threading/ImapThreadingStrategy.cs deleted file mode 100644 index 8c5cacfb..00000000 --- a/Wino.Services/Threading/ImapThreadingStrategy.cs +++ /dev/null @@ -1,178 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using SqlKata; -using Wino.Core.Domain.Entities.Mail; -using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.Folders; -using Wino.Core.Domain.Models.MailItem; -using Wino.Services.Extensions; - -namespace Wino.Services.Threading; - -public class ImapThreadingStrategy : BaseDatabaseService, IImapThreadingStrategy -{ - private readonly IFolderService _folderService; - - public ImapThreadingStrategy(IDatabaseService databaseService, IFolderService folderService) : base(databaseService) - { - _folderService = folderService; - } - - private Task GetReplyParentAsync(IMailItem replyItem, Guid accountId, Guid threadingFolderId, Guid sentFolderId, Guid draftFolderId) - { - if (string.IsNullOrEmpty(replyItem?.MessageId)) return Task.FromResult(null); - - var query = new Query("MailCopy") - .Distinct() - .Take(1) - .Join("MailItemFolder", "MailItemFolder.Id", "MailCopy.FolderId") - .Where("MailItemFolder.MailAccountId", accountId) - .WhereIn("MailItemFolder.Id", new List { threadingFolderId, sentFolderId, draftFolderId }) - .Where("MailCopy.MessageId", replyItem.InReplyTo) - .WhereNot("MailCopy.Id", replyItem.Id) - .Select("MailCopy.*"); - - return Connection.FindWithQueryAsync(query.GetRawQuery()); - } - - private Task GetInReplyToReplyAsync(IMailItem originalItem, Guid accountId, Guid threadingFolderId, Guid sentFolderId, Guid draftFolderId) - { - if (string.IsNullOrEmpty(originalItem?.MessageId)) return Task.FromResult(null); - - var query = new Query("MailCopy") - .Distinct() - .Take(1) - .Join("MailItemFolder", "MailItemFolder.Id", "MailCopy.FolderId") - .WhereNot("MailCopy.Id", originalItem.Id) - .Where("MailItemFolder.MailAccountId", accountId) - .Where("MailCopy.InReplyTo", originalItem.MessageId) - .WhereIn("MailItemFolder.Id", new List { threadingFolderId, sentFolderId, draftFolderId }) - .Select("MailCopy.*"); - - var raq = query.GetRawQuery(); - - return Connection.FindWithQueryAsync(query.GetRawQuery()); - } - - public async Task> ThreadItemsAsync(List items, IMailItemFolder threadingForFolder) - { - var threads = new List(); - - var account = items.First().AssignedAccount; - var accountId = account.Id; - - // Child -> Parent approach. - - var mailLookupTable = new Dictionary(); - - // Fill up the mail lookup table to prevent double thread creation. - foreach (var mail in items) - if (!mailLookupTable.ContainsKey(mail.Id)) - mailLookupTable.Add(mail.Id, false); - - var sentFolder = await _folderService.GetSpecialFolderByAccountIdAsync(accountId, SpecialFolderType.Sent); - var draftFolder = await _folderService.GetSpecialFolderByAccountIdAsync(accountId, SpecialFolderType.Draft); - - // Threading is not possible. Return items as it is. - - if (sentFolder == null || draftFolder == null) return new List(items); - - foreach (var replyItem in items) - { - if (mailLookupTable[replyItem.Id]) - continue; - - mailLookupTable[replyItem.Id] = true; - - var threadItem = new ThreadMailItem(); - - threadItem.AddThreadItem(replyItem); - - var replyToChild = await GetReplyParentAsync(replyItem, accountId, replyItem.AssignedFolder.Id, sentFolder.Id, draftFolder.Id); - - // Build up - while (replyToChild != null) - { - replyToChild.AssignedAccount = account; - - if (replyToChild.FolderId == draftFolder.Id) - replyToChild.AssignedFolder = draftFolder; - - if (replyToChild.FolderId == sentFolder.Id) - replyToChild.AssignedFolder = sentFolder; - - if (replyToChild.FolderId == replyItem.AssignedFolder.Id) - replyToChild.AssignedFolder = replyItem.AssignedFolder; - - threadItem.AddThreadItem(replyToChild); - - if (mailLookupTable.ContainsKey(replyToChild.Id)) - mailLookupTable[replyToChild.Id] = true; - - replyToChild = await GetReplyParentAsync(replyToChild, accountId, replyToChild.AssignedFolder.Id, sentFolder.Id, draftFolder.Id); - } - - // Build down - var replyToParent = await GetInReplyToReplyAsync(replyItem, accountId, replyItem.AssignedFolder.Id, sentFolder.Id, draftFolder.Id); - - while (replyToParent != null) - { - replyToParent.AssignedAccount = account; - - if (replyToParent.FolderId == draftFolder.Id) - replyToParent.AssignedFolder = draftFolder; - - if (replyToParent.FolderId == sentFolder.Id) - replyToParent.AssignedFolder = sentFolder; - - if (replyToParent.FolderId == replyItem.AssignedFolder.Id) - replyToParent.AssignedFolder = replyItem.AssignedFolder; - - threadItem.AddThreadItem(replyToParent); - - if (mailLookupTable.ContainsKey(replyToParent.Id)) - mailLookupTable[replyToParent.Id] = true; - - replyToParent = await GetInReplyToReplyAsync(replyToParent, accountId, replyToParent.AssignedFolder.Id, sentFolder.Id, draftFolder.Id); - } - - // It's a thread item. - - if (threadItem.ThreadItems.Count > 1 && !threads.Exists(a => a.Id == threadItem.Id)) - { - threads.Add(threadItem); - } - else - { - // False alert. This is not a thread item. - mailLookupTable[replyItem.Id] = false; - - // TODO: Here potentially check other algorithms for threading like References. - } - } - - // At this points all mails in the list belong to single items. - // Merge with threads. - // Last sorting will be done later on in MailService. - - // Remove single mails that are included in thread. - items.RemoveAll(a => mailLookupTable.ContainsKey(a.Id) && mailLookupTable[a.Id]); - - var finalList = new List(items); - - finalList.AddRange(threads); - - return finalList; - } - - public bool ShouldThreadWithItem(IMailItem originalItem, IMailItem targetItem) - { - bool isChild = originalItem.InReplyTo != null && originalItem.InReplyTo == targetItem.MessageId; - bool isParent = originalItem.MessageId != null && originalItem.MessageId == targetItem.InReplyTo; - - return isChild || isParent; - } -} diff --git a/Wino.Services/Threading/OutlookThreadingStrategy.cs b/Wino.Services/Threading/OutlookThreadingStrategy.cs deleted file mode 100644 index 81ed5ab6..00000000 --- a/Wino.Services/Threading/OutlookThreadingStrategy.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Wino.Core.Domain.Interfaces; - -namespace Wino.Services.Threading; - -// Outlook and Gmail is using the same threading strategy. -// Outlook: ConversationId -> it's set as ThreadId -// Gmail: ThreadId - -public class OutlookThreadingStrategy : APIThreadingStrategy, IOutlookThreadingStrategy -{ - public OutlookThreadingStrategy(IDatabaseService databaseService, IFolderService folderService) : base(databaseService, folderService) { } -} diff --git a/Wino.Services/ThreadingStrategyProvider.cs b/Wino.Services/ThreadingStrategyProvider.cs deleted file mode 100644 index c9824742..00000000 --- a/Wino.Services/ThreadingStrategyProvider.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Interfaces; - -namespace Wino.Services; - -public class ThreadingStrategyProvider : IThreadingStrategyProvider -{ - private readonly IOutlookThreadingStrategy _outlookThreadingStrategy; - private readonly IGmailThreadingStrategy _gmailThreadingStrategy; - private readonly IImapThreadingStrategy _imapThreadStrategy; - - public ThreadingStrategyProvider(IOutlookThreadingStrategy outlookThreadingStrategy, - IGmailThreadingStrategy gmailThreadingStrategy, - IImapThreadingStrategy imapThreadStrategy) - { - _outlookThreadingStrategy = outlookThreadingStrategy; - _gmailThreadingStrategy = gmailThreadingStrategy; - _imapThreadStrategy = imapThreadStrategy; - } - - public IThreadingStrategy GetStrategy(MailProviderType mailProviderType) - { - return mailProviderType switch - { - MailProviderType.Outlook => _outlookThreadingStrategy, - MailProviderType.Gmail => _gmailThreadingStrategy, - _ => _imapThreadStrategy, - }; - } -}