ItemsView thing.

This commit is contained in:
Burak Kaan Köse
2025-10-12 16:25:15 +02:00
59 changed files with 1802 additions and 2498 deletions
+1 -2
View File
@@ -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.
/// </summary>
public class MailCopy : IMailItem
public class MailCopy
{
/// <summary>
/// Unique Id of the mail.
@@ -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<FolderOperationMenuItem> GetFolderContextMenuActions(IBaseFolderMenuItem folderInformation);
IEnumerable<MailOperationMenuItem> GetMailItemContextMenuActions(IEnumerable<IMailItem> selectedMailItems);
IEnumerable<MailOperationMenuItem> GetMailItemRenderMenuActions(IMailItem mailItem, bool isDarkEditor);
IEnumerable<MailOperationMenuItem> GetMailItemContextMenuActions(IEnumerable<MailCopy> selectedMailItems);
IEnumerable<MailOperationMenuItem> GetMailItemRenderMenuActions(MailCopy mailItem, bool isDarkEditor);
}
@@ -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
/// </summary>
/// <param name="folderInformation">Current folder that asks for the menu items.</param>
/// <param name="selectedMailItems">Selected menu items in the given folder.</param>
IEnumerable<MailOperationMenuItem> GetMailItemContextMenuActions(IMailItemFolder folderInformation, IEnumerable<IMailItem> selectedMailItems);
IEnumerable<MailOperationMenuItem> GetMailItemContextMenuActions(IMailItemFolder folderInformation, IEnumerable<MailCopy> selectedMailItems);
/// <summary>
/// Calculates and returns available mail operations for mail rendering CommandBar.
/// </summary>
/// <param name="mailItem">Rendered mail item.</param>
/// <param name="activeFolder">Folder that mail item belongs to.</param>
IEnumerable<MailOperationMenuItem> GetMailItemRenderMenuActions(IMailItem mailItem, IMailItemFolder activeFolder, bool isDarkEditor);
IEnumerable<MailOperationMenuItem> GetMailItemRenderMenuActions(MailCopy mailItem, IMailItemFolder activeFolder, bool isDarkEditor);
}
@@ -1,3 +0,0 @@
namespace Wino.Core.Domain.Interfaces;
public interface IGmailThreadingStrategy : IThreadingStrategy { }
@@ -1,3 +0,0 @@
namespace Wino.Core.Domain.Interfaces;
public interface IImapThreadingStrategy : IThreadingStrategy { }
+1 -1
View File
@@ -25,7 +25,7 @@ public interface IMailService
/// Caution: This method is not safe. Use other overrides.
/// </summary>
Task<List<MailCopy>> GetMailItemsAsync(IEnumerable<string> mailCopyIds);
Task<List<IMailItem>> FetchMailsAsync(MailListInitializationOptions options, CancellationToken cancellationToken = default);
Task<List<MailCopy>> FetchMailsAsync(MailListInitializationOptions options, CancellationToken cancellationToken = default);
/// <summary>
/// Deletes all mail copies for all folders.
@@ -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
/// <summary>
/// Creates toast notifications for new mails.
/// </summary>
Task CreateNotificationsAsync(Guid inboxFolderId, IEnumerable<IMailItem> newMailItems);
Task CreateNotificationsAsync(Guid inboxFolderId, IEnumerable<MailCopy> newMailItems);
/// <summary>
/// Gets the unread Inbox messages for each account and updates the taskbar icon.
@@ -1,3 +0,0 @@
namespace Wino.Core.Domain.Interfaces;
public interface IOutlookThreadingStrategy : IThreadingStrategy { }
@@ -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
{
/// <summary>
/// Attach thread mails to the list.
/// </summary>
/// <param name="items">Original mails.</param>
/// <returns>Original mails with thread mails.</returns>
Task<List<IMailItem>> ThreadItemsAsync(List<MailCopy> items, IMailItemFolder threadingForFolder);
bool ShouldThreadWithItem(IMailItem originalItem, IMailItem targetItem);
}
@@ -1,12 +0,0 @@
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Interfaces;
public interface IThreadingStrategyProvider
{
/// <summary>
/// Returns corresponding threading strategy that applies to given provider type.
/// </summary>
/// <param name="mailProviderType">Provider type.</param>
IThreadingStrategy GetStrategy(MailProviderType mailProviderType);
}
@@ -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
/// <param name="mailItem">Mail item to download from server.</param>
/// <param name="transferProgress">Optional progress reporting for download operation.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task DownloadMissingMimeMessageAsync(IMailItem mailItem, ITransferProgress transferProgress, CancellationToken cancellationToken = default);
Task DownloadMissingMimeMessageAsync(MailCopy mailItem, ITransferProgress transferProgress, CancellationToken cancellationToken = default);
/// <summary>
/// 1. Cancel active synchronization.
@@ -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<IMailItem>, 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()
{
}
}
@@ -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<object>
{
public bool SortByName { get; set; }
public DateComparer DateComparer = new DateComparer();
public readonly NameComparer NameComparer = new NameComparer();
public int Compare(object x, object y)
{
if (x is IMailItem xMail && y is IMailItem yMail)
{
var itemComparer = GetItemComparer();
return itemComparer.Compare(xMail, yMail);
}
else if (x is DateTime dateX && y is DateTime dateY)
return DateTime.Compare(dateY, dateX);
else if (x is string stringX && y is string stringY)
return stringY.CompareTo(stringX);
return 0;
}
public IComparer<IMailItem> GetItemComparer()
{
if (SortByName)
return NameComparer;
else
return DateComparer;
}
}
@@ -1,12 +0,0 @@
using System.Collections.Generic;
using Wino.Core.Domain.Models.MailItem;
namespace Wino.Core.Domain.Models.Comparers;
public class NameComparer : IComparer<IMailItem>
{
public int Compare(IMailItem x, IMailItem y)
{
return string.Compare(x.FromName, y.FromName);
}
}
@@ -1,14 +0,0 @@
using System;
using System.Collections.Generic;
namespace Wino.Core.Domain.Models.MailItem;
/// <summary>
/// An interface that returns the UniqueId store for IMailItem.
/// For threads, it may be multiple items.
/// For single mails, it'll always be one item.
/// </summary>
public interface IMailHashContainer
{
IEnumerable<Guid> GetContainingIds();
}
@@ -1,34 +0,0 @@
using System;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared;
namespace Wino.Core.Domain.Models.MailItem;
/// <summary>
/// Interface of simplest representation of a MailCopy.
/// </summary>
public interface IMailItem : IMailHashContainer
{
Guid UniqueId { get; }
string Id { get; }
string Subject { get; }
string ThreadId { get; }
string MessageId { get; }
string References { get; }
string InReplyTo { get; }
string PreviewText { get; }
string FromName { get; }
DateTime CreationDate { get; }
string FromAddress { get; }
bool HasAttachments { get; }
bool IsFlagged { get; }
bool IsFocused { get; }
bool IsRead { get; }
string DraftId { get; }
bool IsDraft { get; }
Guid FileId { get; }
MailItemFolder AssignedFolder { get; }
MailAccount AssignedAccount { get; }
AccountContact SenderContact { get; }
}
@@ -1,15 +0,0 @@
using System.Collections.ObjectModel;
namespace Wino.Core.Domain.Models.MailItem;
/// <summary>
/// Interface that represents conversation threads.
/// Even though this type has 1 single UI representation most of the time,
/// it can contain multiple IMailItem.
/// </summary>
public interface IMailItemThread : IMailItem
{
ObservableCollection<IMailItem> ThreadItems { get; }
IMailItem LatestMailItem { get; }
IMailItem FirstMailItem { get; }
}
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using Wino.Core.Domain.Entities.Mail;
namespace Wino.Core.Domain.Models.MailItem;
@@ -7,12 +8,12 @@ namespace Wino.Core.Domain.Models.MailItem;
/// </summary>
public class MailDragPackage
{
public MailDragPackage(IEnumerable<IMailItem> draggingMails)
public MailDragPackage(IEnumerable<MailCopy> draggingMails)
{
DraggingMails = draggingMails;
}
public MailDragPackage(IMailItem draggingMail)
public MailDragPackage(MailCopy draggingMail)
{
DraggingMails =
[
@@ -20,5 +21,5 @@ public class MailDragPackage
];
}
public IEnumerable<IMailItem> DraggingMails { get; set; }
public IEnumerable<MailCopy> DraggingMails { get; set; }
}
@@ -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<IMailItem> ThreadItems { get; } = new ObservableCollection<IMailItem>();
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<Guid> 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
}
@@ -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;
/// <param name="SourceAction"></param>
/// <param name="TargetAction"></param>
/// <param name="Condition"></param>
public record ToggleRequestRule(MailOperation SourceAction, MailOperation TargetAction, Func<IMailItem, bool> Condition);
public record ToggleRequestRule(MailOperation SourceAction, MailOperation TargetAction, Func<MailCopy, bool> Condition);
@@ -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<IMailItem> Comparer
{
get
{
if (Type == SortingOptionType.ReceiveDate)
return new DateComparer();
else
return new NameComparer();
}
}
public SortingOption(string title, SortingOptionType type)
{
@@ -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.
/// </summary>
[JsonIgnore]
public IEnumerable<IMailItem> DownloadedMessages { get; set; } = [];
public IEnumerable<MailCopy> 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<IMailItem> downloadedMessages)
public static MailSynchronizationResult Completed(IEnumerable<MailCopy> downloadedMessages)
=> new()
{
DownloadedMessages = downloadedMessages,
+3 -3
View File
@@ -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<object, IMailItem> groupKey)
else if (groupObject is IGrouping<object, MailCopy> groupKey)
{
// From semantic group header.
dateObject = groupKey.Key;
+13 -14
View File
@@ -12,7 +12,6 @@ using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.MailItem;
using Wino.Messaging.UI;
namespace Wino.Core.WinUI.Services;
@@ -45,7 +44,7 @@ public class NotificationBuilder : INotificationBuilder
});
}
public async Task CreateNotificationsAsync(Guid inboxFolderId, IEnumerable<IMailItem> downloadedMailItems)
public async Task CreateNotificationsAsync(Guid inboxFolderId, IEnumerable<MailCopy> downloadedMailItems)
{
var mailCount = downloadedMailItems.Count();
@@ -70,7 +69,7 @@ public class NotificationBuilder : INotificationBuilder
}
else
{
var validItems = new List<IMailItem>();
var validItems = new List<MailCopy>();
// Fetch mails again to fill up assigned folder data and latest statuses.
// They've been marked as read by executing synchronizer tasks until inital sync finishes.
@@ -220,17 +219,17 @@ public class NotificationBuilder : INotificationBuilder
public async Task CreateTestNotificationAsync(string title, string message)
{
// with args test.
await CreateNotificationsAsync(Guid.Parse("28c3c39b-7147-4de3-b209-949bd19eede6"), new List<IMailItem>()
{
new MailCopy()
{
Subject = "test subject",
PreviewText = "preview html",
CreationDate = DateTime.UtcNow,
FromAddress = "bkaankose@outlook.com",
Id = "AAkALgAAAAAAHYQDEapmEc2byACqAC-EWg0AnMdP0zg8wkS_Ib2Eeh80LAAGq91I3QAA",
}
});
//await CreateNotificationsAsync(Guid.Parse("28c3c39b-7147-4de3-b209-949bd19eede6"), new List<IMailItem>()
//{
// new MailCopy()
// {
// Subject = "test subject",
// PreviewText = "preview html",
// CreationDate = DateTime.UtcNow,
// FromAddress = "bkaankose@outlook.com",
// Id = "AAkALgAAAAAAHYQDEapmEc2byACqAC-EWg0AnMdP0zg8wkS_Ib2Eeh80LAAGq91I3QAA",
// }
//});
//var builder = new ToastContentBuilder();
//builder.SetToastScenario(ToastScenario.Default);
@@ -1,60 +0,0 @@
using System.Text.Json.Serialization.Metadata;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Requests.Folder;
using Wino.Core.Requests.Mail;
namespace Wino.Core.Integration.Json;
public class ServerRequestTypeInfoResolver : DefaultJsonTypeInfoResolver
{
public ServerRequestTypeInfoResolver()
{
Modifiers.Add(new System.Action<JsonTypeInfo>(t =>
{
if (t.Type == typeof(IRequestBase))
{
t.PolymorphismOptions = new()
{
DerivedTypes =
{
new JsonDerivedType(typeof(AlwaysMoveToRequest), nameof(AlwaysMoveToRequest)),
new JsonDerivedType(typeof(ArchiveRequest), nameof(ArchiveRequest)),
new JsonDerivedType(typeof(ChangeFlagRequest), nameof(ChangeFlagRequest)),
new JsonDerivedType(typeof(CreateDraftRequest), nameof(CreateDraftRequest)),
new JsonDerivedType(typeof(DeleteRequest), nameof(DeleteRequest)),
new JsonDerivedType(typeof(EmptyFolderRequest), nameof(EmptyFolderRequest)),
new JsonDerivedType(typeof(MarkFolderAsReadRequest), nameof(MarkFolderAsReadRequest)),
new JsonDerivedType(typeof(MarkReadRequest), nameof(MarkReadRequest)),
new JsonDerivedType(typeof(MoveRequest), nameof(MoveRequest)),
new JsonDerivedType(typeof(MoveToFocusedRequest), nameof(MoveToFocusedRequest)),
new JsonDerivedType(typeof(RenameFolderRequest), nameof(RenameFolderRequest)),
new JsonDerivedType(typeof(SendDraftRequest), nameof(SendDraftRequest)),
}
};
}
else if (t.Type == typeof(IMailItem))
{
t.PolymorphismOptions = new JsonPolymorphismOptions()
{
DerivedTypes =
{
new JsonDerivedType(typeof(MailCopy), nameof(MailCopy)),
}
};
}
else if (t.Type == typeof(IMailItemFolder))
{
t.PolymorphismOptions = new JsonPolymorphismOptions()
{
DerivedTypes =
{
new JsonDerivedType(typeof(MailItemFolder), nameof(MailItemFolder)),
}
};
}
}));
}
}
@@ -1,31 +0,0 @@
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Services.Threading;
namespace Wino.Core.Services;
public class ThreadingStrategyProvider : IThreadingStrategyProvider
{
private readonly OutlookThreadingStrategy _outlookThreadingStrategy;
private readonly GmailThreadingStrategy _gmailThreadingStrategy;
private readonly ImapThreadingStrategy _imapThreadStrategy;
public ThreadingStrategyProvider(OutlookThreadingStrategy outlookThreadingStrategy,
GmailThreadingStrategy gmailThreadingStrategy,
ImapThreadingStrategy imapThreadStrategy)
{
_outlookThreadingStrategy = outlookThreadingStrategy;
_gmailThreadingStrategy = gmailThreadingStrategy;
_imapThreadStrategy = imapThreadStrategy;
}
public IThreadingStrategy GetStrategy(MailProviderType mailProviderType)
{
return mailProviderType switch
{
MailProviderType.Outlook => _outlookThreadingStrategy,
MailProviderType.Gmail => _gmailThreadingStrategy,
_ => _imapThreadStrategy,
};
}
}
+4 -4
View File
@@ -31,10 +31,10 @@ public class WinoRequestProcessor : IWinoRequestProcessor
/// </summary>
private readonly List<ToggleRequestRule> _toggleRequestRules =
[
new ToggleRequestRule(MailOperation.MarkAsRead, MailOperation.MarkAsUnread, new System.Func<IMailItem, bool>((item) => item.IsRead)),
new ToggleRequestRule(MailOperation.MarkAsUnread, MailOperation.MarkAsRead, new System.Func<IMailItem, bool>((item) => !item.IsRead)),
new ToggleRequestRule(MailOperation.SetFlag, MailOperation.ClearFlag, new System.Func<IMailItem, bool>((item) => item.IsFlagged)),
new ToggleRequestRule(MailOperation.ClearFlag, MailOperation.SetFlag, new System.Func<IMailItem, bool>((item) => !item.IsFlagged)),
new ToggleRequestRule(MailOperation.MarkAsRead, MailOperation.MarkAsUnread, new System.Func<MailCopy, bool>((item) => item.IsRead)),
new ToggleRequestRule(MailOperation.MarkAsUnread, MailOperation.MarkAsRead, new System.Func<MailCopy, bool>((item) => !item.IsRead)),
new ToggleRequestRule(MailOperation.SetFlag, MailOperation.ClearFlag, new System.Func<MailCopy, bool>((item) => item.IsFlagged)),
new ToggleRequestRule(MailOperation.ClearFlag, MailOperation.SetFlag, new System.Func<MailCopy, bool>((item) => !item.IsFlagged)),
];
public WinoRequestProcessor(IFolderService folderService,
+1 -1
View File
@@ -1032,7 +1032,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
return await _gmailChangeProcessor.GetMailCopiesAsync(messageIds);
}
public override async Task DownloadMissingMimeMessageAsync(IMailItem mailItem,
public override async Task DownloadMissingMimeMessageAsync(MailCopy mailItem,
ITransferProgress transferProgress = null,
CancellationToken cancellationToken = default)
{
+1 -1
View File
@@ -219,7 +219,7 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
}, request, request);
}
public override async Task DownloadMissingMimeMessageAsync(IMailItem mailItem,
public override async Task DownloadMissingMimeMessageAsync(MailCopy mailItem,
ITransferProgress transferProgress = null,
CancellationToken cancellationToken = default)
{
@@ -1327,7 +1327,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
return Move(batchMoveRequest);
}
public override async Task DownloadMissingMimeMessageAsync(IMailItem mailItem,
public override async Task DownloadMissingMimeMessageAsync(MailCopy mailItem,
MailKit.ITransferProgress transferProgress = null,
CancellationToken cancellationToken = default)
{
+1 -1
View File
@@ -445,7 +445,7 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
/// <param name="mailItem">Mail item that its mime file does not exist on the disk.</param>
/// <param name="transferProgress">Optional download progress for IMAP synchronizer.</param>
/// <param name="cancellationToken">Cancellation token.</param>
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()));
/// <summary>
/// Performs an online search for the given query text in the given folders.
@@ -0,0 +1,79 @@
using System;
using CommunityToolkit.Mvvm.ComponentModel;
namespace Wino.Mail.ViewModels.Collections;
/// <summary>
/// Base class for group headers in the flat collection
/// </summary>
public abstract partial class GroupHeaderBase : ObservableObject
{
[ObservableProperty]
private int itemCount;
[ObservableProperty]
private int unreadCount;
protected GroupHeaderBase(string key, string displayName)
{
Key = key;
DisplayName = displayName;
}
/// <summary>
/// The unique key for this group (used for sorting and identification)
/// </summary>
public string Key { get; }
/// <summary>
/// The display name shown in the UI
/// </summary>
public string DisplayName { get; }
}
/// <summary>
/// Group header for date-based grouping
/// </summary>
public partial class DateGroupHeader : GroupHeaderBase
{
public DateGroupHeader(DateTime date) : base(date.ToString("yyyy-MM-dd"), FormatDisplayName(date))
{
Date = date;
}
/// <summary>
/// The date this group represents
/// </summary>
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
};
}
}
/// <summary>
/// Group header for sender name-based grouping
/// </summary>
public partial class SenderGroupHeader : GroupHeaderBase
{
public SenderGroupHeader(string senderName) : base(senderName, senderName)
{
SenderName = senderName;
}
/// <summary>
/// The sender name this group represents
/// </summary>
public string SenderName { get; }
}
File diff suppressed because it is too large Load Diff
@@ -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;
}
}
@@ -1,514 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Collections;
using Serilog;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Comparers;
using Wino.Core.Domain.Models.MailItem;
using Wino.Mail.ViewModels.Data;
namespace Wino.Mail.ViewModels.Collections;
public class WinoMailCollection
{
// We cache each mail copy id for faster access on updates.
// If the item provider here for update or removal doesn't exist here
// we can ignore the operation.
public HashSet<Guid> MailCopyIdHashSet = [];
public event EventHandler<IMailItem> MailItemRemoved;
private ListItemComparer listComparer = new ListItemComparer();
private readonly ObservableGroupedCollection<object, IMailItem> _mailItemSource = new ObservableGroupedCollection<object, IMailItem>();
public ReadOnlyObservableGroupedCollection<object, IMailItem> MailItems { get; }
/// <summary>
/// Property that defines how the item sorting should be done in the collection.
/// </summary>
public SortingOptionType SortingType { get; set; }
/// <summary>
/// Threading strategy that will help thread items according to the account type.
/// </summary>
public IThreadingStrategyProvider ThreadingStrategyProvider { get; set; }
/// <summary>
/// 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.
/// </summary>
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<object, IMailItem>(_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<object, IMailItem> 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<object, IMailItem> 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<object, IMailItem> 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<object, IMailItem> 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<object, IMailItem> currentGroup, ThreadMailItemViewModel threadViewModel, object newGroupKey)
{
var mailThreadItems = threadViewModel.GetThreadMailItem();
await ExecuteUIThread(() =>
{
RemoveItemInternal(currentGroup, threadViewModel);
InsertItemInternal(newGroupKey, new ThreadMailItemViewModel(mailThreadItems));
});
}
private async Task CreateNewThreadAsync(ObservableGroup<object, IMailItem> 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<IMailItem> items, bool clearIdCache)
{
if (clearIdCache)
{
MailCopyIdHashSet.Clear();
}
var groupedByName = items
.GroupBy(a => GetGroupingKey(a))
.Select(a => new ObservableGroup<object, IMailItem>(a.Key, a));
foreach (var group in groupedByName)
{
// Store all mail copy ids for faster access.
foreach (var item in group)
{
if (item is MailItemViewModel mailCopyItem && !MailCopyIdHashSet.Contains(item.UniqueId))
{
MailCopyIdHashSet.Add(item.UniqueId);
}
else if (item is ThreadMailItemViewModel threadMailItem)
{
foreach (var mailItem in threadMailItem.ThreadItems)
{
if (!MailCopyIdHashSet.Contains(mailItem.UniqueId))
{
MailCopyIdHashSet.Add(mailItem.UniqueId);
}
}
}
}
var existingGroup = _mailItemSource.FirstGroupByKeyOrDefault(group.Key);
if (existingGroup == null)
{
_mailItemSource.AddGroup(group.Key, group);
}
else
{
foreach (var item in group)
{
existingGroup.Add(item);
}
}
}
}
public MailItemContainer GetMailItemContainer(Guid uniqueMailId)
{
var groupCount = _mailItemSource.Count;
for (int i = 0; i < groupCount; i++)
{
var group = _mailItemSource[i];
for (int k = 0; k < group.Count; k++)
{
var item = group[k];
if (item is MailItemViewModel singleMailItemViewModel && singleMailItemViewModel.UniqueId == uniqueMailId)
return new MailItemContainer(singleMailItemViewModel);
else if (item is ThreadMailItemViewModel threadMailItemViewModel && threadMailItemViewModel.HasUniqueId(uniqueMailId))
{
var singleItemViewModel = threadMailItemViewModel.GetItemById(uniqueMailId) as MailItemViewModel;
return new MailItemContainer(singleItemViewModel, threadMailItemViewModel);
}
}
}
return null;
}
public void UpdateThumbnails(string address)
{
if (CoreDispatcher == null) return;
CoreDispatcher.ExecuteOnUIThread(() =>
{
foreach (var group in _mailItemSource)
{
foreach (var item in group)
{
if (item is MailItemViewModel mailItemViewModel &&
!string.IsNullOrEmpty(mailItemViewModel.MailCopy?.FromAddress) &&
mailItemViewModel.MailCopy.FromAddress.Equals(address, StringComparison.OrdinalIgnoreCase))
{
mailItemViewModel.ThumbnailUpdatedEvent = !mailItemViewModel.ThumbnailUpdatedEvent;
}
}
}
});
}
/// <summary>
/// Fins the item container that updated mail copy belongs to and updates it.
/// </summary>
/// <param name="updatedMailCopy">Updated mail copy.</param>
/// <returns></returns>
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);
}
+5 -5
View File
@@ -211,7 +211,7 @@ public partial class ComposePageViewModel : MailBaseViewModel
isUpdatingMimeBlocked = true;
var assignedAccount = CurrentMailDraftItem.AssignedAccount;
var assignedAccount = CurrentMailDraftItem.MailCopy.AssignedAccount;
var sentFolder = await _folderService.GetSpecialFolderByAccountIdAsync(assignedAccount.Id, SpecialFolderType.Sent);
using MemoryStream memoryStream = new();
@@ -223,8 +223,8 @@ public partial class ComposePageViewModel : MailBaseViewModel
var draftSendPreparationRequest = new SendDraftPreparationRequest(CurrentMailDraftItem.MailCopy,
SelectedAlias,
sentFolder,
CurrentMailDraftItem.AssignedFolder,
CurrentMailDraftItem.AssignedAccount.Preferences,
CurrentMailDraftItem.MailCopy.AssignedFolder,
CurrentMailDraftItem.MailCopy.AssignedAccount.Preferences,
base64EncodedMessage);
await _worker.ExecuteAsync(draftSendPreparationRequest);
@@ -355,7 +355,7 @@ public partial class ComposePageViewModel : MailBaseViewModel
if (ComposingAccount != null) return true;
var composingAccount = await _accountService.GetAccountAsync(CurrentMailDraftItem.AssignedAccount.Id).ConfigureAwait(false);
var composingAccount = await _accountService.GetAccountAsync(CurrentMailDraftItem.MailCopy.AssignedAccount.Id).ConfigureAwait(false);
if (composingAccount == null) return false;
var aliases = await _accountService.GetAccountAliasesAsync(composingAccount.Id).ConfigureAwait(false);
@@ -556,7 +556,7 @@ public partial class ComposePageViewModel : MailBaseViewModel
if (CurrentMailDraftItem == null) return;
if (updatedMail.UniqueId == CurrentMailDraftItem.UniqueId)
if (updatedMail.UniqueId == CurrentMailDraftItem.MailCopy.UniqueId)
{
await ExecuteUIThread(() =>
{
+4 -25
View File
@@ -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;
/// <summary>
/// Single view model for IMailItem representation.
/// </summary>
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<Guid> GetContainingIds() => new[] { UniqueId };
}
@@ -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;
/// <summary>
/// Thread mail item (multiple IMailItem) view model representation.
/// </summary>
public partial class ThreadMailItemViewModel : ObservableObject, IMailItemThread, IComparable<string>, IComparable<DateTime>
public partial class ThreadMailItemViewModel : ObservableRecipient, IDisposable
{
public ObservableCollection<IMailItem> ThreadItems => (MailItem as IMailItemThread)?.ThreadItems ?? [];
public AccountContact SenderContact => ((IMailItemThread)MailItem).SenderContact;
private readonly string _threadId;
private readonly List<MailItemViewModel> _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)
/// <summary>
/// Gets the number of emails in this thread
/// </summary>
public int EmailCount => _threadEmails.Count;
/// <summary>
/// Gets the latest email's subject for display
/// </summary>
public string? Subject => _threadEmails
.OrderByDescending(e => e.MailCopy?.CreationDate)
.FirstOrDefault()?.MailCopy?.Subject;
/// <summary>
/// Gets the latest email's sender name for display
/// </summary>
public string? FromName => _threadEmails
.OrderByDescending(e => e.MailCopy?.CreationDate)
.FirstOrDefault()?.MailCopy?.SenderContact.Name;
/// <summary>
/// Gets the latest email's creation date for sorting
/// </summary>
public DateTime LatestEmailDate => _threadEmails
.OrderByDescending(e => e.MailCopy?.CreationDate)
.FirstOrDefault()?.MailCopy?.CreationDate ?? DateTime.MinValue;
/// <summary>
/// Gets all emails in this thread (read-only)
/// </summary>
public IReadOnlyList<MailItemViewModel> 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<MailCopy> GetMailCopies()
=> ThreadItems.OfType<MailItemViewModel>().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);
/// <summary>
/// Adds an email to this thread
/// </summary>
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()
/// <summary>
/// Removes an email from this thread
/// </summary>
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<Guid> GetContainingIds() => ((IMailItemThread)MailItem).GetContainingIds();
}
+85 -124
View File
@@ -59,8 +59,15 @@ public partial class MailListPageViewModel : MailBaseViewModel,
private IObservable<System.Reactive.EventPattern<NotifyCollectionChangedEventArgs>> selectionChangedObservable = null;
public WinoMailCollection MailCollection { get; }
public GroupedEmailCollection MailCollection { get; set; } = new GroupedEmailCollection();
//public IEnumerable<MailItemViewModel> SelectedItems
//{
// get
// {
// }
//}
public ObservableCollection<MailItemViewModel> SelectedItems { get; set; } = [];
public ObservableCollection<FolderPivotViewModel> PivotFolders { get; set; } = [];
public ObservableCollection<MailOperationMenuItem> ActionItems { get; set; } = [];
@@ -77,7 +84,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
private readonly IMailDialogService _mailDialogService;
private readonly IMailService _mailService;
private readonly IFolderService _folderService;
private readonly IThreadingStrategyProvider _threadingStrategyProvider;
private readonly IContextMenuItemService _contextMenuItemService;
private readonly IWinoRequestDelegator _winoRequestDelegator;
private readonly IKeyPressService _keyPressService;
@@ -154,7 +160,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
IMailService mailService,
IStatePersistanceService statePersistenceService,
IFolderService folderService,
IThreadingStrategyProvider threadingStrategyProvider,
IContextMenuItemService contextMenuItemService,
IWinoRequestDelegator winoRequestDelegator,
IKeyPressService keyPressService,
@@ -162,7 +167,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
INewThemeService themeService,
IWinoLogger winoLogger)
{
MailCollection = new WinoMailCollection(threadingStrategyProvider);
PreferencesService = preferencesService;
ThemeService = themeService;
_winoLogger = winoLogger;
@@ -172,7 +176,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
_mailDialogService = mailDialogService;
_mailService = mailService;
_folderService = folderService;
_threadingStrategyProvider = threadingStrategyProvider;
_contextMenuItemService = contextMenuItemService;
_winoRequestDelegator = winoRequestDelegator;
_keyPressService = keyPressService;
@@ -190,23 +193,23 @@ public partial class MailListPageViewModel : MailBaseViewModel,
await ExecuteUIThread(() => { SelectedItemCollectionUpdated(a.EventArgs); });
});
MailCollection.MailItemRemoved += (c, removedItem) =>
{
if (removedItem is ThreadMailItemViewModel removedThreadViewModelItem)
{
foreach (var viewModel in removedThreadViewModelItem.ThreadItems.Cast<MailItemViewModel>())
{
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<MailItemViewModel>())
// {
// if (SelectedItems.Contains(viewModel))
// {
// SelectedItems.Remove(viewModel);
// }
// }
// }
// else if (removedItem is MailItemViewModel removedMailItemViewModel && SelectedItems.Contains(removedMailItemViewModel))
// {
// SelectedItems.Remove(removedMailItemViewModel);
// }
//};
}
private void SetupTopBarActions()
@@ -243,10 +246,11 @@ public partial class MailListPageViewModel : MailBaseViewModel,
{
if (SetProperty(ref _selectedSortingOption, value))
{
if (value != null && MailCollection != null)
{
MailCollection.SortingType = value.Type;
}
// TODO: Update sorting in mail collection.
//if (value != null && MailCollection != null)
//{
// MailCollection.SortingType = value.Type;
//}
}
}
}
@@ -318,16 +322,16 @@ public partial class MailListPageViewModel : MailBaseViewModel,
if (markAsPreference == MailMarkAsOption.WhenSelected)
{
var operation = MailOperation.MarkAsRead;
var package = new MailOperationPreperationRequest(operation, _activeMailItem.MailCopy);
//var operation = MailOperation.MarkAsRead;
//var package = new MailOperationPreperationRequest(operation, _activeMailItem.MailCopy);
if (ActiveFolder?.SpecialFolderType == SpecialFolderType.Unread &&
!gmailUnreadFolderMarkedAsReadUniqueIds.Contains(_activeMailItem.UniqueId))
{
gmailUnreadFolderMarkedAsReadUniqueIds.Add(_activeMailItem.UniqueId);
}
//if (ActiveFolder?.SpecialFolderType == SpecialFolderType.Unread &&
// !gmailUnreadFolderMarkedAsReadUniqueIds.Contains(_activeMailItem.UniqueId))
//{
// gmailUnreadFolderMarkedAsReadUniqueIds.Add(_activeMailItem.UniqueId);
//}
await ExecuteMailOperationAsync(package);
//await ExecuteMailOperationAsync(package);
}
else if (markAsPreference == MailMarkAsOption.AfterDelay && PreferencesService.MarkAsDelay >= 0)
{
@@ -353,13 +357,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
OnPropertyChanged(nameof(IsFolderEmpty));
}
protected override void OnDispatcherAssigned()
{
base.OnDispatcherAssigned();
MailCollection.CoreDispatcher = Dispatcher;
}
private async void UpdateBarMessage(InfoBarMessageType severity, string title, string message)
{
await ExecuteUIThread(() =>
@@ -565,67 +562,35 @@ public partial class MailListPageViewModel : MailBaseViewModel,
[RelayCommand]
private async Task LoadMoreItemsAsync()
{
if (IsInitializingFolder || IsOnlineSearchEnabled) return;
//if (IsInitializingFolder || IsOnlineSearchEnabled) return;
await ExecuteUIThread(() => { IsInitializingFolder = true; });
//await ExecuteUIThread(() => { IsInitializingFolder = true; });
var initializationOptions = new MailListInitializationOptions(ActiveFolder.HandlingFolders,
SelectedFilterOption.Type,
SelectedSortingOption.Type,
PreferencesService.IsThreadingEnabled,
SelectedFolderPivot.IsFocused,
IsInSearchMode ? SearchQuery : string.Empty,
MailCollection.MailCopyIdHashSet);
//var initializationOptions = new MailListInitializationOptions(ActiveFolder.HandlingFolders,
// SelectedFilterOption.Type,
// SelectedSortingOption.Type,
// PreferencesService.IsThreadingEnabled,
// SelectedFolderPivot.IsFocused,
// IsInSearchMode ? SearchQuery : string.Empty,
// MailCollection.MailCopyIdHashSet);
var items = await _mailService.FetchMailsAsync(initializationOptions).ConfigureAwait(false);
//var items = await _mailService.FetchMailsAsync(initializationOptions).ConfigureAwait(false);
var viewModels = PrepareMailViewModels(items);
//var viewModels = PrepareMailViewModels(items);
await ExecuteUIThread(() => { MailCollection.AddRange(viewModels, clearIdCache: false); });
await ExecuteUIThread(() => { IsInitializingFolder = false; });
//await ExecuteUIThread(() => { MailCollection.AddRange(viewModels, clearIdCache: false); });
//await ExecuteUIThread(() => { IsInitializingFolder = false; });
}
#endregion
public Task ExecuteMailOperationAsync(MailOperationPreperationRequest package) => _winoRequestDelegator.ExecuteAsync(package);
public IEnumerable<MailItemViewModel> GetTargetMailItemViewModels(IMailItem clickedItem)
{
// Threat threads as a whole and include everything in the group. Except single selections outside of the thread.
IEnumerable<MailItemViewModel> contextMailItems = null;
public IEnumerable<MailOperationMenuItem> GetAvailableMailActions(IEnumerable<MailItemViewModel> 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<MailItemViewModel>();
// 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<MailOperationMenuItem> GetAvailableMailActions(IEnumerable<IMailItem> contextMailItems)
=> _contextMenuItemService.GetMailItemContextMenuActions(contextMailItems);
public void ChangeCustomFocusedState(IEnumerable<IMailItem> mailItems, bool isFocused)
=> mailItems.OfType<MailItemViewModel>().ForEach(a => a.IsCustomFocused = isFocused);
private bool ShouldPreventItemAdd(IMailItem mailItem)
private bool ShouldPreventItemAdd(MailCopy mailItem)
{
bool condition = mailItem.IsRead
&& SelectedFilterOption.Type == FilterOptionType.Unread
@@ -660,7 +625,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
await listManipulationSemepahore.WaitAsync();
await MailCollection.AddAsync(addedMail);
// await MailCollection.AddAsync(addedMail);
await ExecuteUIThread(() => { NotifyItemFoundState(); });
}
@@ -677,7 +642,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
Debug.WriteLine($"Updating {updatedMail.Id}-> {updatedMail.UniqueId}");
await MailCollection.UpdateMailCopy(updatedMail);
// await MailCollection.UpdateMailCopy(updatedMail);
await ExecuteUIThread(() => { SetupTopBarActions(); });
}
@@ -712,12 +677,12 @@ public partial class MailListPageViewModel : MailBaseViewModel,
{
await ExecuteUIThread(() =>
{
nextItem = MailCollection.GetNextItem(removedMail);
// nextItem = MailCollection.GetNextItem(removedMail);
});
}
// Remove the deleted item from the list.
await MailCollection.RemoveAsync(removedMail);
// await MailCollection.RemoveAsync(removedMail);
if (nextItem != null)
WeakReferenceMessenger.Default.Send(new SelectMailItemContainerEvent(nextItem, ScrollToItem: true));
@@ -748,11 +713,11 @@ public partial class MailListPageViewModel : MailBaseViewModel,
// Otherwise the draft mail item will be duplicated on the next add execution.
await listManipulationSemepahore.WaitAsync();
// Create the item. Draft folder navigation is already done at this point.
await MailCollection.AddAsync(draftMail);
await ExecuteUIThread(() =>
{
// Create the item. Draft folder navigation is already done at this point.
MailCollection.AddEmail(new MailItemViewModel(draftMail));
// New draft is created by user. Select the item.
Messenger.Send(new MailItemNavigationRequested(draftMail.UniqueId, ScrollToItem: true));
@@ -765,15 +730,16 @@ public partial class MailListPageViewModel : MailBaseViewModel,
}
}
private IEnumerable<IMailItem> PrepareMailViewModels(IEnumerable<IMailItem> mailItems)
private IEnumerable<MailItemViewModel> PrepareMailViewModels(IEnumerable<MailCopy> mailItems)
{
foreach (var item in mailItems)
{
if (item is MailCopy singleMailItem)
yield return new MailItemViewModel(singleMailItem);
else if (item is ThreadMailItem threadMailItem)
yield return new ThreadMailItemViewModel(threadMailItem);
}
return mailItems.Select(a => new MailItemViewModel(a));
//foreach (var item in mailItems)
//{
// if (item is MailCopy singleMailItem)
// yield return new MailItemViewModel(singleMailItem);
// else if (item is ThreadMailItem threadMailItem)
// yield return new ThreadMailItemViewModel(threadMailItem);
//}
}
[RelayCommand]
@@ -793,7 +759,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
try
{
MailCollection.Clear();
MailCollection.MailCopyIdHashSet.Clear();
SelectedItems.Clear();
@@ -816,17 +781,9 @@ public partial class MailListPageViewModel : MailBaseViewModel,
await listManipulationSemepahore.WaitAsync(cancellationToken);
// Setup MailCollection configuration.
// Don't pass any threading strategy if disabled in settings.
MailCollection.ThreadingStrategyProvider = PreferencesService.IsThreadingEnabled ? _threadingStrategyProvider : null;
// TODO: This should go inside
MailCollection.PruneSingleNonDraftItems = ActiveFolder.SpecialFolderType == SpecialFolderType.Draft;
// Here items are sorted and filtered.
List<IMailItem> items = null;
List<MailCopy> items = null;
List<MailCopy> onlineSearchItems = null;
bool isDoingSearch = !string.IsNullOrEmpty(SearchQuery);
@@ -894,7 +851,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
PreferencesService.IsThreadingEnabled,
SelectedFolderPivot.IsFocused,
SearchQuery,
MailCollection.MailCopyIdHashSet,
default,
onlineSearchItems);
items = await _mailService.FetchMailsAsync(initializationOptions, cancellationToken).ConfigureAwait(false);
@@ -909,7 +866,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
await ExecuteUIThread(() =>
{
MailCollection.AddRange(viewModels, true);
MailCollection.AddEmails(viewModels);
if (isDoingSearch && !isDoingOnlineSearch)
{
@@ -1055,15 +1012,16 @@ public partial class MailListPageViewModel : MailBaseViewModel,
for (int i = 0; i < 3; i++)
{
var mailContainer = MailCollection.GetMailItemContainer(message.UniqueMailId);
// TODO: Get container.
//var mailContainer = MailCollection.GetMailItemContainer(message.UniqueMailId);
if (mailContainer != null)
{
navigatingMailItem = mailContainer.ItemViewModel;
threadMailItemViewModel = mailContainer.ThreadViewModel;
//if (mailContainer != null)
//{
// navigatingMailItem = mailContainer.ItemViewModel;
// threadMailItemViewModel = mailContainer.ThreadViewModel;
break;
}
// break;
//}
}
if (threadMailItemViewModel != null)
@@ -1138,5 +1096,8 @@ public partial class MailListPageViewModel : MailBaseViewModel,
}
}
public void Receive(ThumbnailAdded message) => MailCollection.UpdateThumbnails(message.Email);
public void Receive(ThumbnailAdded message)
{
// MailCollection.UpdateThumbnails(message.Email);
}
}
@@ -60,7 +60,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
public bool ShouldDisplayDownloadProgress => IsIndetermineProgress || (CurrentDownloadPercentage > 0 && CurrentDownloadPercentage <= 100);
public bool CanUnsubscribe => CurrentRenderModel?.UnsubscribeInfo?.CanUnsubscribe ?? false;
public bool IsJunkMail => initializedMailItemViewModel?.AssignedFolder != null && initializedMailItemViewModel.AssignedFolder.SpecialFolderType == SpecialFolderType.Junk;
public bool IsJunkMail => initializedMailItemViewModel?.MailCopy.AssignedFolder != null && initializedMailItemViewModel.MailCopy.AssignedFolder.SpecialFolderType == SpecialFolderType.Junk;
public bool IsImageRenderingDisabled
{
@@ -283,9 +283,9 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
}
};
var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(initializedMailItemViewModel.AssignedAccount.Id, draftOptions).ConfigureAwait(false);
var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(initializedMailItemViewModel.MailCopy.AssignedAccount.Id, draftOptions).ConfigureAwait(false);
var draftPreparationRequest = new DraftPreparationRequest(initializedMailItemViewModel.AssignedAccount, draftMailCopy, draftBase64MimeMessage, draftOptions.Reason, initializedMailItemViewModel.MailCopy);
var draftPreparationRequest = new DraftPreparationRequest(initializedMailItemViewModel.MailCopy.AssignedAccount, draftMailCopy, draftBase64MimeMessage, draftOptions.Reason, initializedMailItemViewModel.MailCopy);
await _requestDelegator.ExecuteAsync(draftPreparationRequest);
@@ -375,7 +375,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
private async Task RenderAsync(MailItemViewModel mailItemViewModel, CancellationToken cancellationToken = default)
{
ResetProgress();
var isMimeExists = await _mimeFileService.IsMimeExistAsync(mailItemViewModel.AssignedAccount.Id, mailItemViewModel.MailCopy.FileId);
var isMimeExists = await _mimeFileService.IsMimeExistAsync(mailItemViewModel.MailCopy.AssignedAccount.Id, mailItemViewModel.MailCopy.FileId);
if (!isMimeExists)
{
@@ -384,7 +384,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
// Find the MIME for this item and render it.
var mimeMessageInformation = await _mimeFileService.GetMimeMessageInformationAsync(mailItemViewModel.MailCopy.FileId,
mailItemViewModel.AssignedAccount.Id,
mailItemViewModel.MailCopy.AssignedAccount.Id,
cancellationToken).ConfigureAwait(false);
if (mimeMessageInformation == null)
@@ -436,12 +436,12 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
FromAddress = message.From.Mailboxes.FirstOrDefault()?.Address ?? Translator.UnknownAddress;
FromName = message.From.Mailboxes.FirstOrDefault()?.Name ?? Translator.UnknownSender;
CreationDate = message.Date.DateTime;
ContactPicture = initializedMailItemViewModel?.SenderContact?.Base64ContactPicture;
ContactPicture = initializedMailItemViewModel?.MailCopy.SenderContact?.Base64ContactPicture;
// Automatically disable images for Junk folder to prevent pixel tracking.
// This can only work for selected mail item rendering, not for EML file rendering.
if (initializedMailItemViewModel != null &&
initializedMailItemViewModel.AssignedFolder.SpecialFolderType == SpecialFolderType.Junk)
initializedMailItemViewModel.MailCopy.AssignedFolder.SpecialFolderType == SpecialFolderType.Junk)
{
renderingOptions.LoadImages = false;
}
@@ -480,7 +480,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
var contactViewModel = new AccountContactViewModel(foundContact);
// Make sure that user account first in the list.
if (string.Equals(contactViewModel.Address, initializedMailItemViewModel?.AssignedAccount?.Address, StringComparison.OrdinalIgnoreCase))
if (string.Equals(contactViewModel.Address, initializedMailItemViewModel?.MailCopy.AssignedAccount?.Address, StringComparison.OrdinalIgnoreCase))
{
contactViewModel.IsMe = true;
accounts.Insert(0, contactViewModel);
@@ -563,7 +563,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
}
// Archive - Unarchive
if (initializedMailItemViewModel.AssignedFolder.SpecialFolderType == SpecialFolderType.Archive)
if (initializedMailItemViewModel.MailCopy.AssignedFolder.SpecialFolderType == SpecialFolderType.Archive)
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.UnArchive));
else
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.Archive));
@@ -594,7 +594,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
// Check if the updated mail is the same mail item we are rendering.
// This is done with UniqueId to include FolderId into calculations.
if (initializedMailItemViewModel.UniqueId != updatedMail.UniqueId) return;
if (initializedMailItemViewModel.MailCopy.UniqueId != updatedMail.UniqueId) return;
// Mail operation might change the mail item like mark read/unread or change flag.
// So we need to update the mail item view model when this happens.
+8 -9
View File
@@ -20,7 +20,6 @@ using Wino.Core.Domain.Models.Navigation;
using Wino.Core.WinUI;
using Wino.Core.WinUI.Controls;
using Wino.Extensions;
using Wino.Mail.ViewModels.Data;
using Wino.MenuFlyouts;
using Wino.MenuFlyouts.Context;
using Wino.Messaging.Client.Accounts;
@@ -67,14 +66,14 @@ public sealed partial class AppShell : AppShellAbstract,
foreach (var item in dragPackage.DraggingMails)
{
if (item is MailItemViewModel singleMailItemViewModel)
{
mailCopies.Add(singleMailItemViewModel.MailCopy);
}
else if (item is ThreadMailItemViewModel threadViewModel)
{
mailCopies.AddRange(threadViewModel.GetMailCopies());
}
//if (item is MailItemViewModel singleMailItemViewModel)
//{
// mailCopies.Add(singleMailItemViewModel.MailCopy);
//}
//else if (item is ThreadMailItemViewModel threadViewModel)
//{
// mailCopies.AddRange(threadViewModel.GetMailCopies());
//}
}
await ViewModel.PerformMoveOperationAsync(mailCopies, draggingFolder);
@@ -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<object>? CastedItemsSource => ItemsSource as IEnumerable<object>;
public WinoItemsView()
{
DefaultStyleKey = typeof(ItemsView);
}
}
@@ -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;
/// <summary>
/// Custom ListView control that handles multiple selection with Extended/Multiple selection mode
/// and supports threads.
/// </summary>
public partial class WinoListView : ListView, IDisposable
{
private ILogger logger = Log.ForContext<WinoListView>();
private const string PART_ScrollViewer = "ScrollViewer";
private ScrollViewer internalScrollviewer;
/// <summary>
/// Gets or sets whether this ListView belongs to thread items.
/// This is important for detecting selected items etc.
/// </summary>
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<MailItemViewModel>().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<MailItemViewModel>();
// Highlight all items
allItems.ForEach(a => a.IsCustomFocused = true);
// Set native drag arg properties.
var dragPackage = new MailDragPackage(allItems.Cast<IMailItem>());
args.Data.Properties.Add(nameof(MailDragPackage), dragPackage);
}
else
{
var dragPackage = new MailDragPackage(args.Items.Cast<IMailItem>());
args.Data.Properties.Add(nameof(MailDragPackage), dragPackage);
}
}
public void ChangeSelectionMode(ListViewSelectionMode selectionMode)
{
SelectionMode = selectionMode;
if (!IsThreadListView)
{
Items.Where(a => a is ThreadMailItemViewModel).Cast<ThreadMailItemViewModel>().ForEach(c =>
{
var threadListView = GetThreadInternalListView(c);
if (threadListView != null)
{
threadListView.SelectionMode = selectionMode;
}
});
}
}
/// <summary>
/// Finds the container for given mail item and adds it to selected items.
/// </summary>
/// <param name="mailItemViewModel">Mail to be added to selected items.</param>
/// <returns>Whether selection was successful or not.</returns>
public bool SelectMailItemContainer(MailItemViewModel mailItemViewModel)
{
var itemContainer = ContainerFromItem(mailItemViewModel);
// This item might be in thread container.
if (itemContainer == null)
{
bool found = false;
Items.OfType<ThreadMailItemViewModel>().ForEach(c =>
{
if (!found)
{
var threadListView = GetThreadInternalListView(c);
if (threadListView != null)
found = threadListView.SelectMailItemContainer(mailItemViewModel);
}
});
return found;
}
SelectedItems.Add(mailItemViewModel);
return true;
}
/// <summary>
/// Recursively clears all selections except the given mail.
/// </summary>
/// <param name="exceptViewModel">Exceptional mail item to be not unselected.</param>
/// <param name="preserveThreadExpanding">Whether expansion states of thread containers should stay as it is or not.</param>
public void ClearSelections(MailItemViewModel exceptViewModel = null, bool preserveThreadExpanding = false)
{
SelectedItems.Clear();
Items.Where(a => a is ThreadMailItemViewModel).Cast<ThreadMailItemViewModel>().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();
}
});
}
/// <summary>
/// Recursively selects all mails, including thread items.
/// </summary>
public void SelectAllWino()
{
SelectAll();
Items.Where(a => a is ThreadMailItemViewModel).Cast<ThreadMailItemViewModel>().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<ThreadMailItemViewModel>().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<WinoExpander>("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();
}
}
}
}
@@ -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<MailItemViewModel>().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<MailItemViewModel>().Select(a => a.MailCopy), toggleExecution: true);
if (package == null) return;
@@ -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;
}
}
@@ -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)
};
}
}
+216 -221
View File
@@ -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">
<Page.Resources>
<CollectionViewSource
x:Name="MailCollectionViewSource"
IsSourceGrouped="True"
Source="{x:Bind ViewModel.MailCollection.MailItems, Mode=OneWay}" />
<Thickness x:Key="ExpanderHeaderPadding">0,0,0,0</Thickness>
<Thickness x:Key="ExpanderChevronMargin">0,0,12,0</Thickness>
<Thickness x:Key="ExpanderHeaderBorderThickness">0,0,0,0</Thickness>
<SolidColorBrush x:Key="CardBackgroundFillColorDefaultBrush">Transparent</SolidColorBrush>
<SolidColorBrush x:Key="CardBackgroundFillColorSecondaryBrush">Transparent</SolidColorBrush>
<SolidColorBrush x:Key="ExpanderContentBorderBrush">Transparent</SolidColorBrush>
<StaticResource x:Key="ExpanderContentBackground" ResourceKey="CardBackgroundFillColorSecondaryBrush" />
@@ -49,155 +46,210 @@
<SolidColorBrush x:Key="SystemControlRevealFocusVisualBrush" Color="#FF4C4A48" />
<SolidColorBrush x:Key="SystemControlFocusVisualSecondaryBrush" Color="#FFFFFFFF" />
<!-- Header Templates -->
<DataTemplate x:Key="MailGroupHeaderDefaultTemplate" x:DataType="collections:IReadOnlyObservableGroup">
<Grid
Margin="4,2"
AllowFocusOnInteraction="False"
Background="{ThemeResource MailListHeaderBackgroundColor}"
CornerRadius="6">
<TextBlock
Padding="12"
VerticalAlignment="Center"
AllowFocusOnInteraction="False"
FontSize="13"
FontWeight="SemiBold"
Text="{x:Bind helpers:XamlHelpers.GetMailGroupDateString(Key)}" />
</Grid>
</DataTemplate>
<!-- Swipe Items -->
<!--
TODO: Temporarily disabled. WinUI2 - SwipeControl crash.
https://github.com/microsoft/microsoft-ui-xaml/issues/2527
-->
<!--<SwipeItems x:Key="LeftSwipeItems" Mode="Execute">
<SwipeItem BehaviorOnInvoked="Close"
Invoked="LeftSwipeItemInvoked"
Text="{x:Bind domain:Translator.MailOperation_Delete}">
<SwipeItem.IconSource>
<coreControls:WinoFontIconSource Icon="Delete" />
</SwipeItem.IconSource>
<SwipeItem.Background>
<LinearGradientBrush StartPoint="0,0.5" EndPoint="1,0.5">
<GradientStop Offset="0.0" Color="#FFF5A6A6" />
<GradientStop Offset="0.5" Color="#FFEF4444" />
<GradientStop Offset="1.0" Color="#FFF32525" />
</LinearGradientBrush>
</SwipeItem.Background>
</SwipeItem>
</SwipeItems>
-->
<!--
<SwipeItems x:Key="RightSwipeItems" Mode="Execute">
<SwipeItem BehaviorOnInvoked="Close"
Invoked="RightSwipeItemInvoked"
Text="{x:Bind domain:Translator.MarkReadUnread}">
<SwipeItem.IconSource>
<coreControls:WinoFontIconSource Icon="MarkRead" />
</SwipeItem.IconSource>
</SwipeItem>
</SwipeItems>-->
<!-- Single Mail Item Template -->
<DataTemplate x:Key="SingleMailItemTemplate" x:DataType="viewModelData:MailItemViewModel">
<controls:MailItemDisplayInformationControl
x:DefaultBindMode="OneWay"
CenterHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.CenterHoverAction, Mode=OneWay}"
ContextRequested="MailItemContextRequested"
DisplayMode="{Binding ElementName=root, Path=ViewModel.PreferencesService.MailItemDisplayMode, Mode=OneWay}"
HoverActionExecutedCommand="{Binding ElementName=root, Path=ViewModel.ExecuteHoverActionCommand}"
IsAvatarVisible="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowSenderPicturesEnabled, Mode=OneWay}"
IsCustomFocused="{x:Bind IsCustomFocused, Mode=OneWay}"
IsHoverActionsEnabled="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsHoverActionsEnabled, Mode=OneWay}"
IsThumbnailUpdated="{x:Bind ThumbnailUpdatedEvent, Mode=OneWay}"
LeftHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.LeftHoverAction, Mode=OneWay}"
MailItem="{x:Bind MailCopy, Mode=OneWay}"
Prefer24HourTimeFormat="{Binding ElementName=root, Path=ViewModel.PreferencesService.Prefer24HourTimeFormat, Mode=OneWay}"
RightHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.RightHoverAction, Mode=OneWay}"
ShowPreviewText="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowPreviewEnabled, Mode=OneWay}" />
</DataTemplate>
<!-- Single Mail Item Template for Threads -->
<DataTemplate x:Key="ThreadSingleMailItemTemplate" x:DataType="viewModelData:MailItemViewModel">
<controls:MailItemDisplayInformationControl
x:DefaultBindMode="OneWay"
CenterHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.CenterHoverAction, Mode=OneWay}"
ContextRequested="MailItemContextRequested"
DisplayMode="{Binding ElementName=root, Path=ViewModel.PreferencesService.MailItemDisplayMode, Mode=OneWay}"
FocusVisualMargin="8"
FocusVisualPrimaryBrush="{StaticResource SystemControlRevealFocusVisualBrush}"
FocusVisualPrimaryThickness="2"
FocusVisualSecondaryBrush="{StaticResource SystemControlFocusVisualSecondaryBrush}"
FocusVisualSecondaryThickness="1"
HoverActionExecutedCommand="{Binding ElementName=root, Path=ViewModel.ExecuteHoverActionCommand}"
IsAvatarVisible="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowSenderPicturesEnabled, Mode=OneWay}"
IsCustomFocused="{x:Bind IsCustomFocused, Mode=OneWay}"
IsHoverActionsEnabled="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsHoverActionsEnabled, Mode=OneWay}"
IsThumbnailUpdated="{x:Bind ThumbnailUpdatedEvent, Mode=OneWay}"
LeftHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.LeftHoverAction, Mode=OneWay}"
MailItem="{x:Bind MailCopy, Mode=OneWay}"
Prefer24HourTimeFormat="{Binding ElementName=root, Path=ViewModel.PreferencesService.Prefer24HourTimeFormat, Mode=OneWay}"
RightHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.RightHoverAction, Mode=OneWay}"
ShowPreviewText="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowPreviewEnabled, Mode=OneWay}" />
</DataTemplate>
<!-- Mail Item Content Selector -->
<selectors:MailItemDisplaySelector x:Key="MailItemDisplaySelector" SingleMailItemTemplate="{StaticResource SingleMailItemTemplate}">
<selectors:MailItemDisplaySelector.ThreadMailItemTemplate>
<DataTemplate x:DataType="viewModelData:ThreadMailItemViewModel">
<controls:WinoExpander
x:Name="ThreadExpander"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
IsExpanded="{x:Bind IsThreadExpanded, Mode=TwoWay}">
<controls:WinoExpander.Header>
<controls:MailItemDisplayInformationControl
x:DefaultBindMode="OneWay"
Background="Transparent"
BorderThickness="0"
CenterHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.CenterHoverAction, Mode=OneWay}"
ConnectedExpander="{Binding ElementName=ThreadExpander}"
ContextRequested="MailItemContextRequested"
DisplayMode="{Binding ElementName=root, Path=ViewModel.PreferencesService.MailItemDisplayMode, Mode=OneWay}"
DragStarting="ThreadHeaderDragStart"
DropCompleted="ThreadHeaderDragFinished"
HoverActionExecutedCommand="{Binding ElementName=root, Path=ViewModel.ExecuteHoverActionCommand}"
IsAvatarVisible="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowSenderPicturesEnabled, Mode=OneWay}"
IsHitTestVisible="True"
IsHoverActionsEnabled="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsHoverActionsEnabled, Mode=OneWay}"
IsThreadExpanded="{x:Bind IsThreadExpanded, Mode=TwoWay}"
IsThreadExpanderVisible="True"
LeftHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.LeftHoverAction, Mode=OneWay}"
MailItem="{x:Bind MailItem, Mode=OneWay}"
Prefer24HourTimeFormat="{Binding ElementName=root, Path=ViewModel.PreferencesService.Prefer24HourTimeFormat, Mode=OneWay}"
RightHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.RightHoverAction, Mode=OneWay}"
ShowPreviewText="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowPreviewEnabled, Mode=OneWay}" />
</controls:WinoExpander.Header>
<controls:WinoExpander.Content>
<listview:WinoListView
x:Name="ThreadItemsList"
toolkitExt:ListViewExtensions.ItemContainerStretchDirection="Horizontal"
IsThreadListView="True"
ItemTemplate="{StaticResource ThreadSingleMailItemTemplate}"
ItemsSource="{x:Bind ThreadItems}"
ScrollViewer.VerticalScrollBarVisibility="Hidden">
<ListView.ItemContainerTransitions>
<TransitionCollection>
<EdgeUIThemeTransition />
</TransitionCollection>
</ListView.ItemContainerTransitions>
</listview:WinoListView>
</controls:WinoExpander.Content>
</controls:WinoExpander>
</DataTemplate>
</selectors:MailItemDisplaySelector.ThreadMailItemTemplate>
</selectors:MailItemDisplaySelector>
<SolidColorBrush x:Key="ButtonBackgroundDisabled">Transparent</SolidColorBrush>
<StackLayout
x:Key="DefaultItemsViewLayout"
Orientation="Vertical"
Spacing="6" />
<!-- Templates -->
<DataTemplate x:Key="EmailTemplate" x:DataType="viewModelData:MailItemViewModel">
<ItemContainer
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
IsSelected="{x:Bind IsSelected, Mode=TwoWay}">
<animations:Implicit.ShowAnimations>
<animations:OpacityAnimation To="1.0" Duration="0:0:1" />
</animations:Implicit.ShowAnimations>
<animations:Implicit.HideAnimations>
<animations:OpacityAnimation To="0.0" Duration="0:0:1" />
</animations:Implicit.HideAnimations>
<Grid Height="80">
<!-- Thread background -->
<Grid
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="{ThemeResource CardStrokeColorDefaultBrush}"
Visibility="{x:Bind IsDisplayedInThread, Mode=OneWay}" />
<Grid Padding="8,4,12,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- Left: PersonPicture -->
<PersonPicture
Grid.Column="0"
Width="40"
Height="40"
Margin="0,0,12,0"
VerticalAlignment="Center"
DisplayName="{x:Bind FromName, Mode=OneWay}" />
<!-- Center: Content -->
<StackPanel
Grid.Column="1"
VerticalAlignment="Center"
Spacing="2">
<!-- Subject -->
<TextBlock
FontWeight="{x:Bind helpers:XamlHelpers.GetFontWeightByReadState(IsRead), Mode=OneWay}"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
MaxLines="1"
Text="{x:Bind Subject, Mode=OneWay}"
TextTrimming="CharacterEllipsis" />
<!-- Sender Name -->
<TextBlock
FontWeight="{x:Bind helpers:XamlHelpers.GetFontWeightByReadState(IsRead), Mode=OneWay}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
MaxLines="1"
Text="{x:Bind FromName, Mode=OneWay}"
TextTrimming="CharacterEllipsis" />
<!-- Preview Text -->
<TextBlock
Foreground="{ThemeResource TextFillColorTertiaryBrush}"
MaxLines="1"
Text="{x:Bind PreviewText, Mode=OneWay}"
TextTrimming="CharacterEllipsis" />
</StackPanel>
<!-- Right: Indicators -->
<StackPanel
Grid.Column="2"
Margin="12,0,0,0"
VerticalAlignment="Center"
Spacing="4">
<!-- Unread Indicator (show when IsRead is false) -->
<Ellipse
Width="8"
Height="8"
Fill="{ThemeResource AccentTextFillColorPrimaryBrush}"
Visibility="{x:Bind helpers:XamlHelpers.ReverseBoolToVisibilityConverter(IsRead), Mode=OneWay}" />
<!-- Flagged Indicator -->
<FontIcon
FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="12"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Glyph="&#xE129;"
Visibility="{x:Bind IsFlagged, Mode=OneWay}" />
<!-- Attachment Indicator -->
<FontIcon
FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Glyph="&#xE723;"
Visibility="{x:Bind HasAttachments, Mode=OneWay}" />
</StackPanel>
</Grid>
</Grid>
</ItemContainer>
</DataTemplate>
<DataTemplate x:Key="ThreadExpanderTemplate" x:DataType="viewModelData:ThreadMailItemViewModel">
<ItemContainer Tag="{x:Bind}" Tapped="ThreadContainerTapped">
<Grid
Padding="4,12,0,12"
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- Expansion indicator -->
<Viewbox Width="12" Height="12">
<FontIcon
x:Name="ExpanderIcon"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Glyph="&#xE76C;" />
</Viewbox>
<!-- Thread content -->
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<TextBlock
FontWeight="SemiBold"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Text="{x:Bind Subject, Mode=OneWay}"
TextTrimming="CharacterEllipsis" />
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{ThemeResource CaptionTextBlockStyle}"
Text="{x:Bind LatestEmailDate, Mode=OneWay}"
TextTrimming="CharacterEllipsis" />
</StackPanel>
<!-- Email count badge -->
<Border
Grid.Column="2"
Margin="8,0,12,0"
Padding="6,2"
VerticalAlignment="Center"
Background="{ThemeResource AccentTextFillColorPrimaryBrush}"
CornerRadius="8">
<TextBlock
Foreground="White"
Style="{ThemeResource CaptionTextBlockStyle}"
Text="{x:Bind EmailCount, Mode=OneWay}" />
</Border>
</Grid>
</ItemContainer>
</DataTemplate>
<DataTemplate x:Key="DateGroupHeaderTemplate" x:DataType="data:DateGroupHeader">
<ItemContainer IsHitTestVisible="False">
<Grid Padding="12,8" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}">
<TextBlock
FontSize="14"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Style="{ThemeResource CaptionTextBlockStyle}"
Text="{x:Bind DisplayName, Mode=OneWay}" />
</Grid>
</ItemContainer>
</DataTemplate>
<DataTemplate x:Key="SenderGroupHeaderTemplate" x:DataType="data:SenderGroupHeader">
<ItemContainer IsHitTestVisible="False">
<Grid Padding="12,8" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Style="{ThemeResource SubtitleTextBlockStyle}"
Text="{x:Bind DisplayName, Mode=OneWay}" />
<StackPanel
Grid.Column="1"
Orientation="Horizontal"
Spacing="8">
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{ThemeResource CaptionTextBlockStyle}"
Text="{x:Bind ItemCount, Mode=OneWay}" />
</StackPanel>
</Grid>
</ItemContainer>
</DataTemplate>
<selectors1:MailItemContainerSelector
x:Name="MailItemContainerSelector"
DateGroupHeaderTemplate="{StaticResource DateGroupHeaderTemplate}"
EmailTemplate="{StaticResource EmailTemplate}"
SenderGroupHeaderTemplate="{StaticResource SenderGroupHeaderTemplate}"
ThreadExpanderTemplate="{StaticResource ThreadExpanderTemplate}" />
</Page.Resources>
<Page.KeyboardAccelerators>
@@ -303,8 +355,7 @@
VerticalAlignment="Center"
Canvas.ZIndex="100"
Checked="SelectAllCheckboxChecked"
Unchecked="SelectAllCheckboxUnchecked"
Visibility="{x:Bind helpers:XamlHelpers.IsSelectionModeMultiple(MailListView.SelectionMode), Mode=OneWay}" />
Unchecked="SelectAllCheckboxUnchecked" />
<!-- Folders -->
@@ -452,70 +503,14 @@
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<RefreshContainer RefreshRequested="PullToRefreshRequested" Visibility="{x:Bind ViewModel.IsEmpty, Converter={StaticResource ReverseBooleanToVisibilityConverter}, Mode=OneWay}">
<SemanticZoom x:Name="SemanticZoomContainer" CanChangeViews="{x:Bind ViewModel.PreferencesService.IsSemanticZoomEnabled, Mode=OneWay}">
<SemanticZoom.ZoomedInView>
<listview:WinoListView
x:Name="MailListView"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
toolkitExt:ListViewExtensions.ItemContainerStretchDirection="Horizontal"
toolkitExt:ScrollViewerExtensions.VerticalScrollBarMargin="0"
ItemDeletedCommand="{x:Bind ViewModel.ExecuteMailOperationCommand}"
ItemTemplateSelector="{StaticResource MailItemDisplaySelector}"
ItemsSource="{x:Bind MailCollectionViewSource.View, Mode=OneWay}"
LoadMoreCommand="{x:Bind ViewModel.LoadMoreItemsCommand}"
ScrollViewer.VerticalScrollBarVisibility="Auto">
<ListView.ItemContainerTransitions>
<TransitionCollection>
<AddDeleteThemeTransition />
</TransitionCollection>
</ListView.ItemContainerTransitions>
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsStackPanel Margin="8,0,12,0" AreStickyGroupHeadersEnabled="True" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.Resources>
<ResourceDictionary>
<Style BasedOn="{StaticResource MailListHeaderStyle}" TargetType="ListViewHeaderItem" />
</ResourceDictionary>
</ListView.Resources>
<ListView.GroupStyle>
<GroupStyle HeaderTemplate="{StaticResource MailGroupHeaderDefaultTemplate}" HidesIfEmpty="True" />
</ListView.GroupStyle>
</listview:WinoListView>
</SemanticZoom.ZoomedInView>
<SemanticZoom.ZoomedOutView>
<ListView
x:Name="ZoomOutList"
x:Load="{x:Bind helpers:XamlHelpers.ReverseBoolConverter(SemanticZoomContainer.IsZoomedInViewActive), Mode=OneWay}"
ItemsSource="{x:Bind MailCollectionViewSource.View.CollectionGroups}">
<ListView.Resources>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Stretch" />
<Setter Property="Margin" Value="12" />
<Setter Property="Padding" Value="0" />
</Style>
</ListView.Resources>
<ListView.ItemTemplate>
<DataTemplate x:DataType="ICollectionViewGroup">
<Grid Background="{ThemeResource MailListHeaderBackgroundColor}" CornerRadius="6">
<TextBlock
Margin="12,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontWeight="SemiBold"
Text="{x:Bind helpers:XamlHelpers.GetMailGroupDateString(Group)}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</SemanticZoom.ZoomedOutView>
</SemanticZoom>
</RefreshContainer>
<advanced:WinoItemsView
x:Name="MailListView"
Margin="4"
ItemTemplate="{StaticResource MailItemContainerSelector}"
ItemsSource="{x:Bind ViewModel.MailCollection.Items, Mode=OneTime}"
Layout="{StaticResource DefaultItemsViewLayout}"
SelectionChanged="ListSelectionChanged"
SelectionMode="Extended" />
<!-- Try online search panel. -->
<Grid Grid.Row="1" Visibility="{x:Bind ViewModel.IsOnlineSearchButtonVisible, Mode=OneWay}">
+136 -83
View File
@@ -7,19 +7,19 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Hosting;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media.Animation;
using Microsoft.UI.Xaml.Navigation;
using MoreLinq;
using Windows.Foundation;
using Wino.Controls;
using Wino.Controls.Advanced;
using Wino.Core.Domain;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Menus;
using Wino.Core.Domain.Models.Navigation;
using Wino.Helpers;
using Wino.Mail.ViewModels.Data;
using Wino.Mail.ViewModels.Messages;
using Wino.Mail.WinUI;
@@ -91,7 +91,7 @@ public sealed partial class MailListPage : MailListPageAbstract,
SelectAllCheckbox.Checked -= SelectAllCheckboxChecked;
SelectAllCheckbox.Unchecked -= SelectAllCheckboxUnchecked;
SelectAllCheckbox.IsChecked = MailListView.Items.Count > 0 && MailListView.SelectedItems.Count == MailListView.Items.Count;
SelectAllCheckbox.IsChecked = MailListView.CastedItemsSource?.Count() > 0 && MailListView.SelectedItems.Count == MailListView.CastedItemsSource.Count();
SelectAllCheckbox.Checked += SelectAllCheckboxChecked;
SelectAllCheckbox.Unchecked += SelectAllCheckboxUnchecked;
@@ -120,10 +120,10 @@ public sealed partial class MailListPage : MailListPageAbstract,
}
}
SelectAllCheckbox.IsChecked = false;
// SelectAllCheckbox.IsChecked = false;
SelectionModeToggle.IsChecked = false;
MailListView.ClearSelections();
// MailListView.ClearSelections();
UpdateSelectAllButtonStatus();
ViewModel.SelectedPivotChangedCommand.Execute(null);
@@ -131,7 +131,8 @@ public sealed partial class MailListPage : MailListPageAbstract,
private void ChangeSelectionMode(ListViewSelectionMode mode)
{
MailListView.ChangeSelectionMode(mode);
// ItemsView doesn't have a ChangeSelectionMode method like ListView
// The selection mode is set in XAML and doesn't need to change dynamically for ItemsView
if (ViewModel?.PivotFolders != null)
{
@@ -146,44 +147,46 @@ public sealed partial class MailListPage : MailListPageAbstract,
private void SelectAllCheckboxChecked(object sender, RoutedEventArgs e)
{
MailListView.SelectAllWino();
// MailListView.SelectAllWino();
}
private void SelectAllCheckboxUnchecked(object sender, RoutedEventArgs e)
{
MailListView.ClearSelections();
// MailListView.ClearSelections();
}
private async void MailItemContextRequested(UIElement sender, ContextRequestedEventArgs args)
{
// TODO: New ItemsView implementation.
// Context is requested from a single mail point, but we might have multiple selected items.
// This menu should be calculated based on all selected items by providers.
if (sender is MailItemDisplayInformationControl control && args.TryGetPosition(sender, out Point p))
{
await FocusManager.TryFocusAsync(control, FocusState.Keyboard);
//if (sender is MailItemDisplayInformationControl control && args.TryGetPosition(sender, out Point p))
//{
// await FocusManager.TryFocusAsync(control, FocusState.Keyboard);
if (control.DataContext is IMailItem clickedMailItemContext)
{
var targetItems = ViewModel.GetTargetMailItemViewModels(clickedMailItemContext);
var availableActions = ViewModel.GetAvailableMailActions(targetItems);
// if (control.DataContext is IMailItem clickedMailItemContext)
// {
// var targetItems = ViewModel.GetTargetMailItemViewModels(clickedMailItemContext);
// var availableActions = ViewModel.GetAvailableMailActions(targetItems);
if (!availableActions?.Any() ?? false) return;
var t = targetItems.ElementAt(0);
// if (!availableActions?.Any() ?? false) return;
// var t = targetItems.ElementAt(0);
ViewModel.ChangeCustomFocusedState(targetItems, true);
// ViewModel.ChangeCustomFocusedState(targetItems, true);
var clickedOperation = await GetMailOperationFromFlyoutAsync(availableActions, control, p.X, p.Y);
// var clickedOperation = await GetMailOperationFromFlyoutAsync(availableActions, control, p.X, p.Y);
ViewModel.ChangeCustomFocusedState(targetItems, false);
// ViewModel.ChangeCustomFocusedState(targetItems, false);
if (clickedOperation == null) return;
// if (clickedOperation == null) return;
var prepRequest = new MailOperationPreperationRequest(clickedOperation.Operation, targetItems.Select(a => a.MailCopy));
// var prepRequest = new MailOperationPreperationRequest(clickedOperation.Operation, targetItems.Select(a => a.MailCopy));
await ViewModel.ExecuteMailOperationAsync(prepRequest);
}
}
// await ViewModel.ExecuteMailOperationAsync(prepRequest);
// }
//}
}
private async Task<MailOperationMenuItem> GetMailOperationFromFlyoutAsync(IEnumerable<MailOperationMenuItem> availableActions,
@@ -206,7 +209,7 @@ public sealed partial class MailListPage : MailListPageAbstract,
void IRecipient<ClearMailSelectionsRequested>.Receive(ClearMailSelectionsRequested message)
{
MailListView.ClearSelections(null, preserveThreadExpanding: true);
// MailListView.ClearSelections(null, preserveThreadExpanding: true);
}
void IRecipient<ActiveMailItemChangedEvent>.Receive(ActiveMailItemChangedEvent message)
@@ -309,46 +312,46 @@ public sealed partial class MailListPage : MailListPageAbstract,
{
if (message.SelectedMailViewModel == null) return;
await ViewModel.ExecuteUIThread(async () =>
{
MailListView.ClearSelections(message.SelectedMailViewModel, true);
//await ViewModel.ExecuteUIThread(async () =>
//{
// MailListView.ClearSelections(message.SelectedMailViewModel, true);
int retriedSelectionCount = 0;
trySelection:
// int retriedSelectionCount = 0;
//trySelection:
bool isSelected = MailListView.SelectMailItemContainer(message.SelectedMailViewModel);
// bool isSelected = MailListView.SelectMailItemContainer(message.SelectedMailViewModel);
if (!isSelected)
{
for (int i = retriedSelectionCount; i < 5;)
{
// Retry with delay until the container is realized. Max 1 second.
await Task.Delay(200);
// if (!isSelected)
// {
// for (int i = retriedSelectionCount; i < 5;)
// {
// // Retry with delay until the container is realized. Max 1 second.
// await Task.Delay(200);
retriedSelectionCount++;
// retriedSelectionCount++;
goto trySelection;
}
}
// goto trySelection;
// }
// }
// Automatically scroll to the selected item.
// This is useful when creating draft.
if (isSelected && message.ScrollToItem)
{
var collectionContainer = ViewModel.MailCollection.GetMailItemContainer(message.SelectedMailViewModel.UniqueId);
// // Automatically scroll to the selected item.
// // This is useful when creating draft.
// if (isSelected && message.ScrollToItem)
// {
// var collectionContainer = ViewModel.MailCollection.GetMailItemContainer(message.SelectedMailViewModel.UniqueId);
// Scroll to thread if available.
if (collectionContainer.ThreadViewModel != null)
{
MailListView.ScrollIntoView(collectionContainer.ThreadViewModel, ScrollIntoViewAlignment.Default);
}
else if (collectionContainer.ItemViewModel != null)
{
MailListView.ScrollIntoView(collectionContainer.ItemViewModel, ScrollIntoViewAlignment.Default);
}
// // Scroll to thread if available.
// if (collectionContainer.ThreadViewModel != null)
// {
// MailListView.StartBringItemIntoView(collectionContainer.ThreadViewModel, new BringIntoViewOptions());
// }
// else if (collectionContainer.ItemViewModel != null)
// {
// MailListView.StartBringItemIntoView(collectionContainer.ItemViewModel, new BringIntoViewOptions());
// }
}
});
// }
//});
}
private void SearchBoxFocused(object sender, RoutedEventArgs e)
@@ -367,32 +370,29 @@ public sealed partial class MailListPage : MailListPageAbstract,
/// </summary>
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<MailItemViewModel>().ForEach(a => a.IsCustomFocused = true);
// // Highlight all items.
// allItems.Cast<MailItemViewModel>().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<IMailItem>());
// var dragPackage = new MailDragPackage(allItems.Cast<MailCopy>());
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<MailItemViewModel>().ForEach(a => a.IsCustomFocused = false);
}
}
private async void LeftSwipeItemInvoked(Microsoft.UI.Xaml.Controls.SwipeItem sender, Microsoft.UI.Xaml.Controls.SwipeItemInvokedEventArgs args)
@@ -410,7 +410,7 @@ public sealed partial class MailListPage : MailListPageAbstract,
}
else if (swipeControl.Tag is ThreadMailItemViewModel threadMailItemViewModel)
{
var package = new MailOperationPreperationRequest(MailOperation.SoftDelete, threadMailItemViewModel.GetMailCopies());
var package = new MailOperationPreperationRequest(MailOperation.SoftDelete, threadMailItemViewModel.ThreadEmails.Select(a => a.MailCopy));
await ViewModel.ExecuteMailOperationAsync(package);
}
}
@@ -432,19 +432,15 @@ public sealed partial class MailListPage : MailListPageAbstract,
}
else if (swipeControl.Tag is ThreadMailItemViewModel threadMailItemViewModel)
{
bool isAllRead = threadMailItemViewModel.ThreadItems.All(a => a.IsRead);
bool isAllRead = threadMailItemViewModel.ThreadEmails.All(a => a.IsRead);
var operation = isAllRead ? MailOperation.MarkAsUnread : MailOperation.MarkAsRead;
var package = new MailOperationPreperationRequest(operation, threadMailItemViewModel.GetMailCopies());
var package = new MailOperationPreperationRequest(operation, threadMailItemViewModel.ThreadEmails.Select(a => a.MailCopy));
await ViewModel.ExecuteMailOperationAsync(package);
}
}
private void PullToRefreshRequested(Microsoft.UI.Xaml.Controls.RefreshContainer sender, Microsoft.UI.Xaml.Controls.RefreshRequestedEventArgs args)
{
ViewModel.SyncFolderCommand?.Execute(null);
}
private async void SearchBar_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
{
@@ -504,8 +500,65 @@ public sealed partial class MailListPage : MailListPageAbstract,
}
private void SelectAllInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
=> MailListView.SelectAllWino();
{
// MailListView.SelectAllWino();
}
private void DeleteAllInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
=> ViewModel.ExecuteMailOperationCommand.Execute(MailOperation.SoftDelete);
private void ThreadContainerTapped(object sender, TappedRoutedEventArgs e)
{
if (sender is ItemContainer container && container.Tag is ThreadMailItemViewModel expander)
{
// Toggle expansion state
expander.IsThreadExpanded = !expander.IsThreadExpanded;
// Find the expander icon and animate its rotation using Composition APIs
var expanderIcon = WinoVisualTreeHelper.GetChildObject<FontIcon>(container, "ExpanderIcon");
if (expanderIcon != null)
{
var targetAngle = expander.IsThreadExpanded ? 90f : 0f;
AnimateRotationWithComposition(expanderIcon, targetAngle);
}
}
}
/// <summary>
/// Animates the rotation using high-performance Composition APIs
/// </summary>
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();
}
}
@@ -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;
/// <param name="AutoSelectAccount">Account to extend menu item for.</param>
/// <param name="FolderId">Folder to select after expansion.</param>
/// <param name="NavigateMailItem">Mail item to select if possible in the expanded folder.</param>
public record AccountMenuItemExtended(Guid FolderId, IMailItem NavigateMailItem);
public record AccountMenuItemExtended(Guid FolderId, MailCopy NavigateMailItem);
@@ -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;
/// </summary>
/// <param name="AccountId">Account id for corresponding synchronizer.</param>
/// <param name="MailCopyId">Mail copy id to download.</param>
public record DownloadMissingMessageRequested(Guid AccountId, IMailItem MailItem) : IClientMessage;
public record DownloadMissingMessageRequested(Guid AccountId, MailCopy MailItem) : IClientMessage;
-3
View File
@@ -9,9 +9,6 @@
<ItemGroup>
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" />
</ItemGroup>
<ItemGroup>
<Folder Include="Client\Authorization\" />
</ItemGroup>
+4 -4
View File
@@ -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<MailOperationMenuItem> GetMailItemContextMenuActions(IEnumerable<IMailItem> selectedMailItems)
public virtual IEnumerable<MailOperationMenuItem> GetMailItemContextMenuActions(IEnumerable<MailCopy> 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<MailOperationMenuItem> GetMailItemRenderMenuActions(IMailItem mailItem, bool isDarkEditor)
public virtual IEnumerable<MailOperationMenuItem> GetMailItemRenderMenuActions(MailCopy mailItem, bool isDarkEditor)
{
var actionList = new List<MailOperationMenuItem>();
+6 -58
View File
@@ -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<List<IMailItem>> FetchMailsAsync(MailListInitializationOptions options, CancellationToken cancellationToken = default)
public async Task<List<MailCopy>> FetchMailsAsync(MailListInitializationOptions options, CancellationToken cancellationToken = default)
{
List<MailCopy> 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<IMailItem> 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];
}
/// <summary>
/// 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.
/// </summary>
private async Task LoadAssignedPropertiesWithCacheAsync(IMailItem mail, Dictionary<Guid, MailItemFolder> folderCache, Dictionary<Guid, MailAccount> accountCache, Dictionary<string, AccountContact> contactCache)
private async Task LoadAssignedPropertiesWithCacheAsync(MailCopy mail, Dictionary<Guid, MailItemFolder> folderCache, Dictionary<Guid, MailAccount> accountCache, Dictionary<string, AccountContact> 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);
-8
View File
@@ -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<ISignatureService, SignatureService>();
services.AddTransient<IContextMenuItemService, ContextMenuItemService>();
services.AddTransient<ISpecialImapProviderConfigResolver, SpecialImapProviderConfigResolver>();
services.AddSingleton<IThreadingStrategyProvider, ThreadingStrategyProvider>();
services.AddTransient<IOutlookThreadingStrategy, OutlookThreadingStrategy>();
services.AddTransient<IGmailThreadingStrategy, GmailThreadingStrategy>();
services.AddTransient<IImapThreadingStrategy, ImapThreadingStrategy>();
}
}
@@ -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;
}
///<inheritdoc/>
public async Task<List<IMailItem>> ThreadItemsAsync(List<MailCopy> 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<IMailItem> 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<List<MailCopy>> 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<MailCopy>(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}'))";
}
}
}
@@ -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) { }
}
@@ -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<MailCopy> GetReplyParentAsync(IMailItem replyItem, Guid accountId, Guid threadingFolderId, Guid sentFolderId, Guid draftFolderId)
{
if (string.IsNullOrEmpty(replyItem?.MessageId)) return Task.FromResult<MailCopy>(null);
var query = new Query("MailCopy")
.Distinct()
.Take(1)
.Join("MailItemFolder", "MailItemFolder.Id", "MailCopy.FolderId")
.Where("MailItemFolder.MailAccountId", accountId)
.WhereIn("MailItemFolder.Id", new List<Guid> { threadingFolderId, sentFolderId, draftFolderId })
.Where("MailCopy.MessageId", replyItem.InReplyTo)
.WhereNot("MailCopy.Id", replyItem.Id)
.Select("MailCopy.*");
return Connection.FindWithQueryAsync<MailCopy>(query.GetRawQuery());
}
private Task<MailCopy> GetInReplyToReplyAsync(IMailItem originalItem, Guid accountId, Guid threadingFolderId, Guid sentFolderId, Guid draftFolderId)
{
if (string.IsNullOrEmpty(originalItem?.MessageId)) return Task.FromResult<MailCopy>(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<Guid> { threadingFolderId, sentFolderId, draftFolderId })
.Select("MailCopy.*");
var raq = query.GetRawQuery();
return Connection.FindWithQueryAsync<MailCopy>(query.GetRawQuery());
}
public async Task<List<IMailItem>> ThreadItemsAsync(List<MailCopy> items, IMailItemFolder threadingForFolder)
{
var threads = new List<ThreadMailItem>();
var account = items.First().AssignedAccount;
var accountId = account.Id;
// Child -> Parent approach.
var mailLookupTable = new Dictionary<string, bool>();
// 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<IMailItem>(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<IMailItem>(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;
}
}
@@ -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) { }
}
@@ -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,
};
}
}