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 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 da5281a0..095373f3 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
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 3425bcfa..00000000
--- a/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs
+++ /dev/null
@@ -1,512 +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 && 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 979fcf0a..a4ef0be8 100644
--- a/Wino.Mail.ViewModels/ComposePageViewModel.cs
+++ b/Wino.Mail.ViewModels/ComposePageViewModel.cs
@@ -17,7 +17,6 @@ using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Extensions;
-using Wino.Core.Services;
using Wino.Mail.ViewModels.Data;
using Wino.Messaging.Client.Mails;
using Wino.Messaging.Server;
@@ -214,7 +213,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();
@@ -226,8 +225,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);
@@ -358,7 +357,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);
@@ -412,7 +411,7 @@ public partial class ComposePageViewModel : MailBaseViewModel
{
downloadIfNeeded = false;
- var package = new DownloadMissingMessageRequested(CurrentMailDraftItem.AssignedAccount.Id, CurrentMailDraftItem.MailCopy);
+ var package = new DownloadMissingMessageRequested(CurrentMailDraftItem.MailCopy.AssignedAccount.Id, CurrentMailDraftItem.MailCopy);
var downloadResponse = await _winoServerConnectionManager.GetResponseAsync(package);
if (downloadResponse.IsSuccess)
@@ -560,7 +559,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 b4fd27ad..26449311 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;
@@ -155,7 +161,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
IMailService mailService,
IStatePersistanceService statePersistenceService,
IFolderService folderService,
- IThreadingStrategyProvider threadingStrategyProvider,
IContextMenuItemService contextMenuItemService,
IWinoRequestDelegator winoRequestDelegator,
IKeyPressService keyPressService,
@@ -164,7 +169,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
IWinoLogger winoLogger,
IWinoServerConnectionManager winoServerConnectionManager)
{
- MailCollection = new WinoMailCollection(threadingStrategyProvider);
PreferencesService = preferencesService;
ThemeService = themeService;
_winoLogger = winoLogger;
@@ -175,7 +179,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
_mailDialogService = mailDialogService;
_mailService = mailService;
_folderService = folderService;
- _threadingStrategyProvider = threadingStrategyProvider;
_contextMenuItemService = contextMenuItemService;
_winoRequestDelegator = winoRequestDelegator;
_keyPressService = keyPressService;
@@ -193,23 +196,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()
@@ -246,10 +249,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;
+ //}
}
}
}
@@ -321,16 +325,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)
{
@@ -356,13 +360,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(() =>
@@ -568,67 +565,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
@@ -663,7 +628,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
await listManipulationSemepahore.WaitAsync();
- await MailCollection.AddAsync(addedMail);
+ // await MailCollection.AddAsync(addedMail);
await ExecuteUIThread(() => { NotifyItemFoundState(); });
}
@@ -680,7 +645,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
Debug.WriteLine($"Updating {updatedMail.Id}-> {updatedMail.UniqueId}");
- await MailCollection.UpdateMailCopy(updatedMail);
+ // await MailCollection.UpdateMailCopy(updatedMail);
await ExecuteUIThread(() => { SetupTopBarActions(); });
}
@@ -715,12 +680,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));
@@ -751,11 +716,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));
@@ -768,15 +733,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]
@@ -796,7 +762,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
try
{
MailCollection.Clear();
- MailCollection.MailCopyIdHashSet.Clear();
SelectedItems.Clear();
@@ -819,17 +784,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);
@@ -896,7 +853,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
PreferencesService.IsThreadingEnabled,
SelectedFolderPivot.IsFocused,
SearchQuery,
- MailCollection.MailCopyIdHashSet,
+ default,
onlineSearchItems);
items = await _mailService.FetchMailsAsync(initializationOptions, cancellationToken).ConfigureAwait(false);
@@ -911,7 +868,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
await ExecuteUIThread(() =>
{
- MailCollection.AddRange(viewModels, true);
+ MailCollection.AddEmails(viewModels);
if (isDoingSearch && !isDoingOnlineSearch)
{
@@ -1057,15 +1014,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)
@@ -1142,5 +1100,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 3a2818c6..2b2287d7 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
{
@@ -285,9 +285,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);
@@ -355,7 +355,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
// To show the progress on the UI.
CurrentDownloadPercentage = 1;
- var package = new DownloadMissingMessageRequested(mailItemViewModel.AssignedAccount.Id, mailItemViewModel.MailCopy);
+ var package = new DownloadMissingMessageRequested(mailItemViewModel.MailCopy.AssignedAccount.Id, mailItemViewModel.MailCopy);
await _winoServerConnectionManager.GetResponseAsync(package);
}
catch (OperationCanceledException)
@@ -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 db0a259c..03886913 100644
--- a/Wino.Mail.WinUI/AppShell.xaml.cs
+++ b/Wino.Mail.WinUI/AppShell.xaml.cs
@@ -19,7 +19,6 @@ using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Navigation;
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;
@@ -61,14 +60,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 03822669..fc4a7c88 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -299,8 +351,7 @@
VerticalAlignment="Center"
Canvas.ZIndex="100"
Checked="SelectAllCheckboxChecked"
- Unchecked="SelectAllCheckboxUnchecked"
- Visibility="{x:Bind helpers:XamlHelpers.IsSelectionModeMultiple(MailListView.SelectionMode), Mode=OneWay}" />
+ Unchecked="SelectAllCheckboxUnchecked" />
@@ -448,70 +499,14 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/Wino.Mail.WinUI/Views/MailListPage.xaml.cs b/Wino.Mail.WinUI/Views/MailListPage.xaml.cs
index c6cd821e..b0c5b6d6 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;
@@ -62,10 +62,6 @@ public sealed partial class MailListPage : MailListPageAbstract,
{
base.OnNavigatedFrom(e);
- // Dispose all WinoListView items.
-
- MailListView.Dispose();
-
this.Bindings.StopTracking();
RenderingFrame.Navigate(typeof(IdlePage));
@@ -81,7 +77,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;
@@ -110,10 +106,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);
@@ -121,7 +117,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)
{
@@ -136,44 +133,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,
@@ -196,7 +195,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)
@@ -299,46 +298,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)
@@ -357,32 +356,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)
@@ -400,7 +396,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);
}
}
@@ -422,19 +418,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)
{
@@ -494,8 +486,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 c060d23e..a79f4bdf 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,
- };
- }
-}
diff --git a/Wino.Services/Wino.Services.csproj b/Wino.Services/Wino.Services.csproj
index bfe73869..896b0dbd 100644
--- a/Wino.Services/Wino.Services.csproj
+++ b/Wino.Services/Wino.Services.csproj
@@ -5,6 +5,11 @@
win-x86;win-x64;win-arm64
true
+
+
+
+
+
@@ -20,7 +25,4 @@
-
-
-
\ No newline at end of file