This commit is contained in:
Burak Kaan Köse
2025-10-03 15:46:38 +02:00
parent 2bec513d2c
commit 7b41f558d4
62 changed files with 1811 additions and 2514 deletions
+1 -2
View File
@@ -3,7 +3,6 @@ using System.Collections.Generic;
using SQLite; using SQLite;
using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.MailItem;
namespace Wino.Core.Domain.Entities.Mail; namespace Wino.Core.Domain.Entities.Mail;
@@ -11,7 +10,7 @@ namespace Wino.Core.Domain.Entities.Mail;
/// Summary of the parsed MIME messages. /// Summary of the parsed MIME messages.
/// Wino will do non-network operations on this table and others from the original MIME. /// Wino will do non-network operations on this table and others from the original MIME.
/// </summary> /// </summary>
public class MailCopy : IMailItem public class MailCopy
{ {
/// <summary> /// <summary>
/// Unique Id of the mail. /// Unique Id of the mail.
@@ -1,6 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Models.Folders; using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Menus; using Wino.Core.Domain.Models.Menus;
namespace Wino.Core.Domain.Interfaces; namespace Wino.Core.Domain.Interfaces;
@@ -8,6 +8,6 @@ namespace Wino.Core.Domain.Interfaces;
public interface IContextMenuItemService public interface IContextMenuItemService
{ {
IEnumerable<FolderOperationMenuItem> GetFolderContextMenuActions(IBaseFolderMenuItem folderInformation); IEnumerable<FolderOperationMenuItem> GetFolderContextMenuActions(IBaseFolderMenuItem folderInformation);
IEnumerable<MailOperationMenuItem> GetMailItemContextMenuActions(IEnumerable<IMailItem> selectedMailItems); IEnumerable<MailOperationMenuItem> GetMailItemContextMenuActions(IEnumerable<MailCopy> selectedMailItems);
IEnumerable<MailOperationMenuItem> GetMailItemRenderMenuActions(IMailItem mailItem, bool isDarkEditor); IEnumerable<MailOperationMenuItem> GetMailItemRenderMenuActions(MailCopy mailItem, bool isDarkEditor);
} }
@@ -1,6 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Models.Folders; using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Menus; using Wino.Core.Domain.Models.Menus;
namespace Wino.Core.Domain.Interfaces; namespace Wino.Core.Domain.Interfaces;
@@ -18,12 +18,12 @@ public interface IContextMenuProvider
/// </summary> /// </summary>
/// <param name="folderInformation">Current folder that asks for the menu items.</param> /// <param name="folderInformation">Current folder that asks for the menu items.</param>
/// <param name="selectedMailItems">Selected menu items in the given folder.</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> /// <summary>
/// Calculates and returns available mail operations for mail rendering CommandBar. /// Calculates and returns available mail operations for mail rendering CommandBar.
/// </summary> /// </summary>
/// <param name="mailItem">Rendered mail item.</param> /// <param name="mailItem">Rendered mail item.</param>
/// <param name="activeFolder">Folder that mail item belongs to.</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. /// Caution: This method is not safe. Use other overrides.
/// </summary> /// </summary>
Task<List<MailCopy>> GetMailItemsAsync(IEnumerable<string> mailCopyIds); 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> /// <summary>
/// Deletes all mail copies for all folders. /// Deletes all mail copies for all folders.
@@ -1,7 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Entities.Mail;
namespace Wino.Core.Domain.Interfaces; namespace Wino.Core.Domain.Interfaces;
@@ -10,7 +10,7 @@ public interface INotificationBuilder
/// <summary> /// <summary>
/// Creates toast notifications for new mails. /// Creates toast notifications for new mails.
/// </summary> /// </summary>
Task CreateNotificationsAsync(Guid inboxFolderId, IEnumerable<IMailItem> newMailItems); Task CreateNotificationsAsync(Guid inboxFolderId, IEnumerable<MailCopy> newMailItems);
/// <summary> /// <summary>
/// Gets the unread Inbox messages for each account and updates the taskbar icon. /// Gets the unread Inbox messages for each account and updates the taskbar icon.
@@ -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 MailKit;
using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Models.Folders; using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Synchronization; using Wino.Core.Domain.Models.Synchronization;
namespace Wino.Core.Domain.Interfaces; 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="mailItem">Mail item to download from server.</param>
/// <param name="transferProgress">Optional progress reporting for download operation.</param> /// <param name="transferProgress">Optional progress reporting for download operation.</param>
/// <param name="cancellationToken">Cancellation token.</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> /// <summary>
/// 1. Cancel active synchronization. /// 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 System.Collections.Generic;
using Wino.Core.Domain.Entities.Mail;
namespace Wino.Core.Domain.Models.MailItem; namespace Wino.Core.Domain.Models.MailItem;
@@ -7,12 +8,12 @@ namespace Wino.Core.Domain.Models.MailItem;
/// </summary> /// </summary>
public class MailDragPackage public class MailDragPackage
{ {
public MailDragPackage(IEnumerable<IMailItem> draggingMails) public MailDragPackage(IEnumerable<MailCopy> draggingMails)
{ {
DraggingMails = draggingMails; DraggingMails = draggingMails;
} }
public MailDragPackage(IMailItem draggingMail) public MailDragPackage(MailCopy draggingMail)
{ {
DraggingMails = 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 System;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Models.MailItem; namespace Wino.Core.Domain.Models.MailItem;
@@ -10,4 +11,4 @@ namespace Wino.Core.Domain.Models.MailItem;
/// <param name="SourceAction"></param> /// <param name="SourceAction"></param>
/// <param name="TargetAction"></param> /// <param name="TargetAction"></param>
/// <param name="Condition"></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.Enums;
using Wino.Core.Domain.Models.Comparers;
using Wino.Core.Domain.Models.MailItem;
namespace Wino.Core.Domain.Models.Reader; namespace Wino.Core.Domain.Models.Reader;
@@ -9,16 +6,6 @@ public class SortingOption
{ {
public SortingOptionType Type { get; set; } public SortingOptionType Type { get; set; }
public string Title { 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) public SortingOption(string title, SortingOptionType type)
{ {
@@ -1,8 +1,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Accounts; using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.MailItem;
namespace Wino.Core.Domain.Models.Synchronization; namespace Wino.Core.Domain.Models.Synchronization;
@@ -16,7 +16,7 @@ public class MailSynchronizationResult
/// It's ignored in serialization. Client should not react to this. /// It's ignored in serialization. Client should not react to this.
/// </summary> /// </summary>
[JsonIgnore] [JsonIgnore]
public IEnumerable<IMailItem> DownloadedMessages { get; set; } = []; public IEnumerable<MailCopy> DownloadedMessages { get; set; } = [];
public ProfileInformation ProfileInformation { get; set; } public ProfileInformation ProfileInformation { get; set; }
@@ -25,7 +25,7 @@ public class MailSynchronizationResult
public static MailSynchronizationResult Empty => new() { CompletedState = SynchronizationCompletedState.Success }; public static MailSynchronizationResult Empty => new() { CompletedState = SynchronizationCompletedState.Success };
// Mail synchronization // Mail synchronization
public static MailSynchronizationResult Completed(IEnumerable<IMailItem> downloadedMessages) public static MailSynchronizationResult Completed(IEnumerable<MailCopy> downloadedMessages)
=> new() => new()
{ {
DownloadedMessages = downloadedMessages, DownloadedMessages = downloadedMessages,
+3 -3
View File
@@ -12,9 +12,9 @@ using Microsoft.UI.Xaml.Media;
using Windows.UI; using Windows.UI;
using Windows.UI.Text; using Windows.UI.Text;
using Wino.Core.Domain; using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.WinUI.Controls; using Wino.Core.WinUI.Controls;
namespace Wino.Helpers; namespace Wino.Helpers;
@@ -104,9 +104,9 @@ public static class XamlHelpers
public static Color GetWindowsColorFromHex(string hex) => hex.ToColor(); 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 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 GetFontWeightBySyncState(bool isSyncing) => isSyncing ? FontWeights.SemiBold : FontWeights.Normal;
public static FontWeight GetFontWeightByChildSelectedState(bool isChildSelected) => isChildSelected ? 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 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 Visibility StringToVisibilityReversedConverter(string value) => string.IsNullOrWhiteSpace(value) ? Visibility.Visible : Visibility.Collapsed;
public static string GetMailItemDisplaySummaryForListing(bool isDraft, DateTime receivedDate, bool prefer24HourTime) public static string GetMailItemDisplaySummaryForListing(bool isDraft, DateTime receivedDate, bool prefer24HourTime)
@@ -135,7 +135,7 @@ public static class XamlHelpers
// From regular mail header template // From regular mail header template
if (groupObject is DateTime groupedDate) if (groupObject is DateTime groupedDate)
dateObject = groupedDate; dateObject = groupedDate;
else if (groupObject is IGrouping<object, IMailItem> groupKey) else if (groupObject is IGrouping<object, MailCopy> groupKey)
{ {
// From semantic group header. // From semantic group header.
dateObject = groupKey.Key; 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.Entities.Mail;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.MailItem;
using Wino.Messaging.UI; using Wino.Messaging.UI;
namespace Wino.Core.WinUI.Services; 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(); var mailCount = downloadedMailItems.Count();
@@ -70,7 +69,7 @@ public class NotificationBuilder : INotificationBuilder
} }
else else
{ {
var validItems = new List<IMailItem>(); var validItems = new List<MailCopy>();
// Fetch mails again to fill up assigned folder data and latest statuses. // 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. // 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) public async Task CreateTestNotificationAsync(string title, string message)
{ {
// with args test. // with args test.
await CreateNotificationsAsync(Guid.Parse("28c3c39b-7147-4de3-b209-949bd19eede6"), new List<IMailItem>() //await CreateNotificationsAsync(Guid.Parse("28c3c39b-7147-4de3-b209-949bd19eede6"), new List<IMailItem>()
{ //{
new MailCopy() // new MailCopy()
{ // {
Subject = "test subject", // Subject = "test subject",
PreviewText = "preview html", // PreviewText = "preview html",
CreationDate = DateTime.UtcNow, // CreationDate = DateTime.UtcNow,
FromAddress = "bkaankose@outlook.com", // FromAddress = "bkaankose@outlook.com",
Id = "AAkALgAAAAAAHYQDEapmEc2byACqAC-EWg0AnMdP0zg8wkS_Ib2Eeh80LAAGq91I3QAA", // Id = "AAkALgAAAAAAHYQDEapmEc2byACqAC-EWg0AnMdP0zg8wkS_Ib2Eeh80LAAGq91I3QAA",
} // }
}); //});
//var builder = new ToastContentBuilder(); //var builder = new ToastContentBuilder();
//builder.SetToastScenario(ToastScenario.Default); //builder.SetToastScenario(ToastScenario.Default);
+1 -1
View File
@@ -197,7 +197,7 @@ public class ThemeService : IThemeService
// UpdateSystemCaptionButtonColors(); // UpdateSystemCaptionButtonColors();
//}); //});
NotifyThemeUpdate(); // NotifyThemeUpdate();
} }
public void UpdateSystemCaptionButtonColors() public void UpdateSystemCaptionButtonColors()
@@ -15,7 +15,6 @@ using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Requests; using Wino.Core.Domain.Models.Requests;
using Wino.Core.Domain.Models.Server; using Wino.Core.Domain.Models.Server;
using Wino.Core.Integration.Json;
using Wino.Messaging; using Wino.Messaging;
using Wino.Messaging.Client.Connection; using Wino.Messaging.Client.Connection;
using Wino.Messaging.Enums; using Wino.Messaging.Enums;
@@ -77,11 +76,6 @@ public class WinoServerConnectionManager :
} }
} }
private readonly JsonSerializerOptions _jsonSerializerOptions = new()
{
TypeInfoResolver = new ServerRequestTypeInfoResolver()
};
public WinoServerConnectionManager() public WinoServerConnectionManager()
{ {
WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this);
@@ -304,7 +298,7 @@ public class WinoServerConnectionManager :
try try
{ {
serializedMessage = JsonSerializer.Serialize(message, _jsonSerializerOptions); serializedMessage = JsonSerializer.Serialize(message);
} }
catch (Exception serializationException) catch (Exception serializationException)
{ {
@@ -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> /// </summary>
private readonly List<ToggleRequestRule> _toggleRequestRules = private readonly List<ToggleRequestRule> _toggleRequestRules =
[ [
new ToggleRequestRule(MailOperation.MarkAsRead, MailOperation.MarkAsUnread, new System.Func<IMailItem, bool>((item) => item.IsRead)), new ToggleRequestRule(MailOperation.MarkAsRead, MailOperation.MarkAsUnread, new System.Func<MailCopy, bool>((item) => item.IsRead)),
new ToggleRequestRule(MailOperation.MarkAsUnread, MailOperation.MarkAsRead, new System.Func<IMailItem, 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<IMailItem, bool>((item) => item.IsFlagged)), new ToggleRequestRule(MailOperation.SetFlag, MailOperation.ClearFlag, new System.Func<MailCopy, bool>((item) => item.IsFlagged)),
new ToggleRequestRule(MailOperation.ClearFlag, MailOperation.SetFlag, new System.Func<IMailItem, bool>((item) => !item.IsFlagged)), new ToggleRequestRule(MailOperation.ClearFlag, MailOperation.SetFlag, new System.Func<MailCopy, bool>((item) => !item.IsFlagged)),
]; ];
public WinoRequestProcessor(IFolderService folderService, public WinoRequestProcessor(IFolderService folderService,
+1 -1
View File
@@ -1032,7 +1032,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
return await _gmailChangeProcessor.GetMailCopiesAsync(messageIds); return await _gmailChangeProcessor.GetMailCopiesAsync(messageIds);
} }
public override async Task DownloadMissingMimeMessageAsync(IMailItem mailItem, public override async Task DownloadMissingMimeMessageAsync(MailCopy mailItem,
ITransferProgress transferProgress = null, ITransferProgress transferProgress = null,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
+1 -1
View File
@@ -219,7 +219,7 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
}, request, request); }, request, request);
} }
public override async Task DownloadMissingMimeMessageAsync(IMailItem mailItem, public override async Task DownloadMissingMimeMessageAsync(MailCopy mailItem,
ITransferProgress transferProgress = null, ITransferProgress transferProgress = null,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
@@ -871,7 +871,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
return Move(batchMoveRequest); return Move(batchMoveRequest);
} }
public override async Task DownloadMissingMimeMessageAsync(IMailItem mailItem, public override async Task DownloadMissingMimeMessageAsync(MailCopy mailItem,
MailKit.ITransferProgress transferProgress = null, MailKit.ITransferProgress transferProgress = null,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
+1 -1
View File
@@ -415,7 +415,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="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="transferProgress">Optional download progress for IMAP synchronizer.</param>
/// <param name="cancellationToken">Cancellation token.</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> /// <summary>
/// Performs an online search for the given query text in the given folders. /// 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,512 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Collections;
using Serilog;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Comparers;
using Wino.Core.Domain.Models.MailItem;
using Wino.Mail.ViewModels.Data;
namespace Wino.Mail.ViewModels.Collections;
public class WinoMailCollection
{
// We cache each mail copy id for faster access on updates.
// If the item provider here for update or removal doesn't exist here
// we can ignore the operation.
public HashSet<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 && 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);
}
+6 -7
View File
@@ -17,7 +17,6 @@ using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Extensions; using Wino.Core.Extensions;
using Wino.Core.Services;
using Wino.Mail.ViewModels.Data; using Wino.Mail.ViewModels.Data;
using Wino.Messaging.Client.Mails; using Wino.Messaging.Client.Mails;
using Wino.Messaging.Server; using Wino.Messaging.Server;
@@ -214,7 +213,7 @@ public partial class ComposePageViewModel : MailBaseViewModel
isUpdatingMimeBlocked = true; isUpdatingMimeBlocked = true;
var assignedAccount = CurrentMailDraftItem.AssignedAccount; var assignedAccount = CurrentMailDraftItem.MailCopy.AssignedAccount;
var sentFolder = await _folderService.GetSpecialFolderByAccountIdAsync(assignedAccount.Id, SpecialFolderType.Sent); var sentFolder = await _folderService.GetSpecialFolderByAccountIdAsync(assignedAccount.Id, SpecialFolderType.Sent);
using MemoryStream memoryStream = new(); using MemoryStream memoryStream = new();
@@ -226,8 +225,8 @@ public partial class ComposePageViewModel : MailBaseViewModel
var draftSendPreparationRequest = new SendDraftPreparationRequest(CurrentMailDraftItem.MailCopy, var draftSendPreparationRequest = new SendDraftPreparationRequest(CurrentMailDraftItem.MailCopy,
SelectedAlias, SelectedAlias,
sentFolder, sentFolder,
CurrentMailDraftItem.AssignedFolder, CurrentMailDraftItem.MailCopy.AssignedFolder,
CurrentMailDraftItem.AssignedAccount.Preferences, CurrentMailDraftItem.MailCopy.AssignedAccount.Preferences,
base64EncodedMessage); base64EncodedMessage);
await _worker.ExecuteAsync(draftSendPreparationRequest); await _worker.ExecuteAsync(draftSendPreparationRequest);
@@ -358,7 +357,7 @@ public partial class ComposePageViewModel : MailBaseViewModel
if (ComposingAccount != null) return true; 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; if (composingAccount == null) return false;
var aliases = await _accountService.GetAccountAliasesAsync(composingAccount.Id).ConfigureAwait(false); var aliases = await _accountService.GetAccountAliasesAsync(composingAccount.Id).ConfigureAwait(false);
@@ -412,7 +411,7 @@ public partial class ComposePageViewModel : MailBaseViewModel
{ {
downloadIfNeeded = false; downloadIfNeeded = false;
var package = new DownloadMissingMessageRequested(CurrentMailDraftItem.AssignedAccount.Id, CurrentMailDraftItem.MailCopy); var package = new DownloadMissingMessageRequested(CurrentMailDraftItem.MailCopy.AssignedAccount.Id, CurrentMailDraftItem.MailCopy);
var downloadResponse = await _winoServerConnectionManager.GetResponseAsync<bool, DownloadMissingMessageRequested>(package); var downloadResponse = await _winoServerConnectionManager.GetResponseAsync<bool, DownloadMissingMessageRequested>(package);
if (downloadResponse.IsSuccess) if (downloadResponse.IsSuccess)
@@ -560,7 +559,7 @@ public partial class ComposePageViewModel : MailBaseViewModel
if (CurrentMailDraftItem == null) return; if (CurrentMailDraftItem == null) return;
if (updatedMail.UniqueId == CurrentMailDraftItem.UniqueId) if (updatedMail.UniqueId == CurrentMailDraftItem.MailCopy.UniqueId)
{ {
await ExecuteUIThread(() => await ExecuteUIThread(() =>
{ {
+4 -25
View File
@@ -1,35 +1,24 @@
using System; using CommunityToolkit.Mvvm.ComponentModel;
using System.Collections.Generic;
using CommunityToolkit.Mvvm.ComponentModel;
using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Models.MailItem;
namespace Wino.Mail.ViewModels.Data; namespace Wino.Mail.ViewModels.Data;
/// <summary> /// <summary>
/// Single view model for IMailItem representation. /// Single view model for IMailItem representation.
/// </summary> /// </summary>
public partial class MailItemViewModel(MailCopy mailCopy) : ObservableObject, IMailItem public partial class MailItemViewModel(MailCopy mailCopy) : ObservableObject
{ {
[ObservableProperty] [ObservableProperty]
public partial MailCopy MailCopy { get; set; } = mailCopy; 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] [ObservableProperty]
public partial bool ThumbnailUpdatedEvent { get; set; } = false; public partial bool ThumbnailUpdatedEvent { get; set; } = false;
[ObservableProperty] [ObservableProperty]
public partial bool IsCustomFocused { get; set; } public partial bool IsSelected { get; set; }
[ObservableProperty] [ObservableProperty]
public partial bool IsSelected { get; set; } public partial bool IsDisplayedInThread { get; set; }
public bool IsFlagged public bool IsFlagged
{ {
@@ -96,14 +85,4 @@ public partial class MailItemViewModel(MailCopy mailCopy) : ObservableObject, IM
get => MailCopy.HasAttachments; get => MailCopy.HasAttachments;
set => SetProperty(MailCopy.HasAttachments, value, MailCopy, (u, n) => u.HasAttachments = n); 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq; using System.Linq;
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; namespace Wino.Mail.ViewModels.Data;
/// <summary> /// <summary>
/// Thread mail item (multiple IMailItem) view model representation. /// Thread mail item (multiple IMailItem) view model representation.
/// </summary> /// </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 ?? []; private readonly string _threadId;
public AccountContact SenderContact => ((IMailItemThread)MailItem).SenderContact;
private readonly List<MailItemViewModel> _threadEmails = [];
private bool _disposed;
[ObservableProperty] [ObservableProperty]
private ThreadMailItem mailItem; [NotifyPropertyChangedRecipients]
public partial bool IsThreadExpanded { get; set; }
[ObservableProperty] [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 partial void OnIsSelectedChanged(bool value)
foreach (var item in threadMailItem.ThreadItems) {
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{ {
AddMailItemViewModel(item); _threadEmails.Clear();
} }
_disposed = true;
} }
public ThreadMailItem GetThreadMailItem() => MailItem; public void Dispose()
public IEnumerable<MailCopy> GetMailCopies()
=> ThreadItems.OfType<MailItemViewModel>().Select(a => a.MailCopy);
public void AddMailItemViewModel(IMailItem mailItem)
{ {
if (mailItem == null) return; Dispose(true);
GC.SuppressFinalize(this);
if (mailItem is MailCopy mailCopy)
MailItem.AddThreadItem(new MailItemViewModel(mailCopy));
else if (mailItem is MailItemViewModel mailItemViewModel)
MailItem.AddThreadItem(mailItemViewModel);
else
Debugger.Break();
} }
public bool HasUniqueId(Guid uniqueMailId) private void NotifyPropertyChanges()
=> ThreadItems.Any(a => a.UniqueId == uniqueMailId);
public IMailItem GetItemById(Guid uniqueMailId)
=> ThreadItems.FirstOrDefault(a => a.UniqueId == uniqueMailId);
public void RemoveCopyItem(IMailItem item)
{ {
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); /// <summary>
/// Adds an email to this thread
if (existedItem == null) return; /// </summary>
public void AddEmail(MailItemViewModel email)
ThreadItems.Remove(existedItem); {
if (email.MailCopy.ThreadId != _threadId)
throw new ArgumentException($"Email ThreadId '{email.MailCopy.ThreadId}' does not match expander ThreadId '{_threadId}'");
_threadEmails.Add(email);
NotifyPropertyChanges(); NotifyPropertyChanges();
} }
public void NotifyPropertyChanges() /// <summary>
/// Removes an email from this thread
/// </summary>
public void RemoveEmail(MailItemViewModel email)
{ {
// TODO if (_threadEmails.Remove(email))
// Stupid temporary fix for not updating UI. {
// This view model must be reworked with ThreadMailItem together. NotifyPropertyChanges();
}
var current = MailItem;
MailItem = null;
MailItem = current;
} }
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; 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<MailItemViewModel> SelectedItems { get; set; } = [];
public ObservableCollection<FolderPivotViewModel> PivotFolders { get; set; } = []; public ObservableCollection<FolderPivotViewModel> PivotFolders { get; set; } = [];
public ObservableCollection<MailOperationMenuItem> ActionItems { get; set; } = []; public ObservableCollection<MailOperationMenuItem> ActionItems { get; set; } = [];
@@ -77,7 +84,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
private readonly IMailDialogService _mailDialogService; private readonly IMailDialogService _mailDialogService;
private readonly IMailService _mailService; private readonly IMailService _mailService;
private readonly IFolderService _folderService; private readonly IFolderService _folderService;
private readonly IThreadingStrategyProvider _threadingStrategyProvider;
private readonly IContextMenuItemService _contextMenuItemService; private readonly IContextMenuItemService _contextMenuItemService;
private readonly IWinoRequestDelegator _winoRequestDelegator; private readonly IWinoRequestDelegator _winoRequestDelegator;
private readonly IKeyPressService _keyPressService; private readonly IKeyPressService _keyPressService;
@@ -155,7 +161,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
IMailService mailService, IMailService mailService,
IStatePersistanceService statePersistenceService, IStatePersistanceService statePersistenceService,
IFolderService folderService, IFolderService folderService,
IThreadingStrategyProvider threadingStrategyProvider,
IContextMenuItemService contextMenuItemService, IContextMenuItemService contextMenuItemService,
IWinoRequestDelegator winoRequestDelegator, IWinoRequestDelegator winoRequestDelegator,
IKeyPressService keyPressService, IKeyPressService keyPressService,
@@ -164,7 +169,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
IWinoLogger winoLogger, IWinoLogger winoLogger,
IWinoServerConnectionManager winoServerConnectionManager) IWinoServerConnectionManager winoServerConnectionManager)
{ {
MailCollection = new WinoMailCollection(threadingStrategyProvider);
PreferencesService = preferencesService; PreferencesService = preferencesService;
ThemeService = themeService; ThemeService = themeService;
_winoLogger = winoLogger; _winoLogger = winoLogger;
@@ -175,7 +179,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
_mailDialogService = mailDialogService; _mailDialogService = mailDialogService;
_mailService = mailService; _mailService = mailService;
_folderService = folderService; _folderService = folderService;
_threadingStrategyProvider = threadingStrategyProvider;
_contextMenuItemService = contextMenuItemService; _contextMenuItemService = contextMenuItemService;
_winoRequestDelegator = winoRequestDelegator; _winoRequestDelegator = winoRequestDelegator;
_keyPressService = keyPressService; _keyPressService = keyPressService;
@@ -193,23 +196,23 @@ public partial class MailListPageViewModel : MailBaseViewModel,
await ExecuteUIThread(() => { SelectedItemCollectionUpdated(a.EventArgs); }); await ExecuteUIThread(() => { SelectedItemCollectionUpdated(a.EventArgs); });
}); });
MailCollection.MailItemRemoved += (c, removedItem) => //MailCollection.MailItemRemoved += (c, removedItem) =>
{ //{
if (removedItem is ThreadMailItemViewModel removedThreadViewModelItem) // if (removedItem is ThreadMailItemViewModel removedThreadViewModelItem)
{ // {
foreach (var viewModel in removedThreadViewModelItem.ThreadItems.Cast<MailItemViewModel>()) // foreach (var viewModel in removedThreadViewModelItem.ThreadItems.Cast<MailItemViewModel>())
{ // {
if (SelectedItems.Contains(viewModel)) // if (SelectedItems.Contains(viewModel))
{ // {
SelectedItems.Remove(viewModel); // SelectedItems.Remove(viewModel);
} // }
} // }
} // }
else if (removedItem is MailItemViewModel removedMailItemViewModel && SelectedItems.Contains(removedMailItemViewModel)) // else if (removedItem is MailItemViewModel removedMailItemViewModel && SelectedItems.Contains(removedMailItemViewModel))
{ // {
SelectedItems.Remove(removedMailItemViewModel); // SelectedItems.Remove(removedMailItemViewModel);
} // }
}; //};
} }
private void SetupTopBarActions() private void SetupTopBarActions()
@@ -246,10 +249,11 @@ public partial class MailListPageViewModel : MailBaseViewModel,
{ {
if (SetProperty(ref _selectedSortingOption, value)) if (SetProperty(ref _selectedSortingOption, value))
{ {
if (value != null && MailCollection != null) // TODO: Update sorting in mail collection.
{ //if (value != null && MailCollection != null)
MailCollection.SortingType = value.Type; //{
} // MailCollection.SortingType = value.Type;
//}
} }
} }
} }
@@ -321,16 +325,16 @@ public partial class MailListPageViewModel : MailBaseViewModel,
if (markAsPreference == MailMarkAsOption.WhenSelected) if (markAsPreference == MailMarkAsOption.WhenSelected)
{ {
var operation = MailOperation.MarkAsRead; //var operation = MailOperation.MarkAsRead;
var package = new MailOperationPreperationRequest(operation, _activeMailItem.MailCopy); //var package = new MailOperationPreperationRequest(operation, _activeMailItem.MailCopy);
if (ActiveFolder?.SpecialFolderType == SpecialFolderType.Unread && //if (ActiveFolder?.SpecialFolderType == SpecialFolderType.Unread &&
!gmailUnreadFolderMarkedAsReadUniqueIds.Contains(_activeMailItem.UniqueId)) // !gmailUnreadFolderMarkedAsReadUniqueIds.Contains(_activeMailItem.UniqueId))
{ //{
gmailUnreadFolderMarkedAsReadUniqueIds.Add(_activeMailItem.UniqueId); // gmailUnreadFolderMarkedAsReadUniqueIds.Add(_activeMailItem.UniqueId);
} //}
await ExecuteMailOperationAsync(package); //await ExecuteMailOperationAsync(package);
} }
else if (markAsPreference == MailMarkAsOption.AfterDelay && PreferencesService.MarkAsDelay >= 0) else if (markAsPreference == MailMarkAsOption.AfterDelay && PreferencesService.MarkAsDelay >= 0)
{ {
@@ -356,13 +360,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
OnPropertyChanged(nameof(IsFolderEmpty)); OnPropertyChanged(nameof(IsFolderEmpty));
} }
protected override void OnDispatcherAssigned()
{
base.OnDispatcherAssigned();
MailCollection.CoreDispatcher = Dispatcher;
}
private async void UpdateBarMessage(InfoBarMessageType severity, string title, string message) private async void UpdateBarMessage(InfoBarMessageType severity, string title, string message)
{ {
await ExecuteUIThread(() => await ExecuteUIThread(() =>
@@ -568,67 +565,35 @@ public partial class MailListPageViewModel : MailBaseViewModel,
[RelayCommand] [RelayCommand]
private async Task LoadMoreItemsAsync() 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, //var initializationOptions = new MailListInitializationOptions(ActiveFolder.HandlingFolders,
SelectedFilterOption.Type, // SelectedFilterOption.Type,
SelectedSortingOption.Type, // SelectedSortingOption.Type,
PreferencesService.IsThreadingEnabled, // PreferencesService.IsThreadingEnabled,
SelectedFolderPivot.IsFocused, // SelectedFolderPivot.IsFocused,
IsInSearchMode ? SearchQuery : string.Empty, // IsInSearchMode ? SearchQuery : string.Empty,
MailCollection.MailCopyIdHashSet); // 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(() => { MailCollection.AddRange(viewModels, clearIdCache: false); });
await ExecuteUIThread(() => { IsInitializingFolder = false; }); //await ExecuteUIThread(() => { IsInitializingFolder = false; });
} }
#endregion #endregion
public Task ExecuteMailOperationAsync(MailOperationPreperationRequest package) => _winoRequestDelegator.ExecuteAsync(package); public Task ExecuteMailOperationAsync(MailOperationPreperationRequest package) => _winoRequestDelegator.ExecuteAsync(package);
public IEnumerable<MailItemViewModel> GetTargetMailItemViewModels(IMailItem clickedItem) public IEnumerable<MailOperationMenuItem> GetAvailableMailActions(IEnumerable<MailItemViewModel> contextMailItems)
{ => _contextMenuItemService.GetMailItemContextMenuActions(contextMailItems.Select(a => a.MailCopy));
// Threat threads as a whole and include everything in the group. Except single selections outside of the thread.
IEnumerable<MailItemViewModel> contextMailItems = null;
if (clickedItem is ThreadMailItemViewModel clickedThreadItem)
{
// Clicked item is a thread.
clickedThreadItem.IsThreadExpanded = true; private bool ShouldPreventItemAdd(MailCopy mailItem)
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)
{ {
bool condition = mailItem.IsRead bool condition = mailItem.IsRead
&& SelectedFilterOption.Type == FilterOptionType.Unread && SelectedFilterOption.Type == FilterOptionType.Unread
@@ -663,7 +628,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
await listManipulationSemepahore.WaitAsync(); await listManipulationSemepahore.WaitAsync();
await MailCollection.AddAsync(addedMail); // await MailCollection.AddAsync(addedMail);
await ExecuteUIThread(() => { NotifyItemFoundState(); }); await ExecuteUIThread(() => { NotifyItemFoundState(); });
} }
@@ -680,7 +645,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
Debug.WriteLine($"Updating {updatedMail.Id}-> {updatedMail.UniqueId}"); Debug.WriteLine($"Updating {updatedMail.Id}-> {updatedMail.UniqueId}");
await MailCollection.UpdateMailCopy(updatedMail); // await MailCollection.UpdateMailCopy(updatedMail);
await ExecuteUIThread(() => { SetupTopBarActions(); }); await ExecuteUIThread(() => { SetupTopBarActions(); });
} }
@@ -715,12 +680,12 @@ public partial class MailListPageViewModel : MailBaseViewModel,
{ {
await ExecuteUIThread(() => await ExecuteUIThread(() =>
{ {
nextItem = MailCollection.GetNextItem(removedMail); // nextItem = MailCollection.GetNextItem(removedMail);
}); });
} }
// Remove the deleted item from the list. // Remove the deleted item from the list.
await MailCollection.RemoveAsync(removedMail); // await MailCollection.RemoveAsync(removedMail);
if (nextItem != null) if (nextItem != null)
WeakReferenceMessenger.Default.Send(new SelectMailItemContainerEvent(nextItem, ScrollToItem: true)); WeakReferenceMessenger.Default.Send(new SelectMailItemContainerEvent(nextItem, ScrollToItem: true));
@@ -751,11 +716,11 @@ public partial class MailListPageViewModel : MailBaseViewModel,
// Otherwise the draft mail item will be duplicated on the next add execution. // Otherwise the draft mail item will be duplicated on the next add execution.
await listManipulationSemepahore.WaitAsync(); await listManipulationSemepahore.WaitAsync();
// Create the item. Draft folder navigation is already done at this point.
await MailCollection.AddAsync(draftMail);
await ExecuteUIThread(() => 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. // New draft is created by user. Select the item.
Messenger.Send(new MailItemNavigationRequested(draftMail.UniqueId, ScrollToItem: true)); Messenger.Send(new MailItemNavigationRequested(draftMail.UniqueId, ScrollToItem: true));
@@ -768,15 +733,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) return mailItems.Select(a => new MailItemViewModel(a));
{ //foreach (var item in mailItems)
if (item is MailCopy singleMailItem) //{
yield return new MailItemViewModel(singleMailItem); // if (item is MailCopy singleMailItem)
else if (item is ThreadMailItem threadMailItem) // yield return new MailItemViewModel(singleMailItem);
yield return new ThreadMailItemViewModel(threadMailItem); // else if (item is ThreadMailItem threadMailItem)
} // yield return new ThreadMailItemViewModel(threadMailItem);
//}
} }
[RelayCommand] [RelayCommand]
@@ -796,7 +762,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
try try
{ {
MailCollection.Clear(); MailCollection.Clear();
MailCollection.MailCopyIdHashSet.Clear();
SelectedItems.Clear(); SelectedItems.Clear();
@@ -819,17 +784,9 @@ public partial class MailListPageViewModel : MailBaseViewModel,
await listManipulationSemepahore.WaitAsync(cancellationToken); 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. // Here items are sorted and filtered.
List<IMailItem> items = null; List<MailCopy> items = null;
List<MailCopy> onlineSearchItems = null; List<MailCopy> onlineSearchItems = null;
bool isDoingSearch = !string.IsNullOrEmpty(SearchQuery); bool isDoingSearch = !string.IsNullOrEmpty(SearchQuery);
@@ -896,7 +853,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
PreferencesService.IsThreadingEnabled, PreferencesService.IsThreadingEnabled,
SelectedFolderPivot.IsFocused, SelectedFolderPivot.IsFocused,
SearchQuery, SearchQuery,
MailCollection.MailCopyIdHashSet, default,
onlineSearchItems); onlineSearchItems);
items = await _mailService.FetchMailsAsync(initializationOptions, cancellationToken).ConfigureAwait(false); items = await _mailService.FetchMailsAsync(initializationOptions, cancellationToken).ConfigureAwait(false);
@@ -911,7 +868,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
await ExecuteUIThread(() => await ExecuteUIThread(() =>
{ {
MailCollection.AddRange(viewModels, true); MailCollection.AddEmails(viewModels);
if (isDoingSearch && !isDoingOnlineSearch) if (isDoingSearch && !isDoingOnlineSearch)
{ {
@@ -1057,15 +1014,16 @@ public partial class MailListPageViewModel : MailBaseViewModel,
for (int i = 0; i < 3; i++) 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) //if (mailContainer != null)
{ //{
navigatingMailItem = mailContainer.ItemViewModel; // navigatingMailItem = mailContainer.ItemViewModel;
threadMailItemViewModel = mailContainer.ThreadViewModel; // threadMailItemViewModel = mailContainer.ThreadViewModel;
break; // break;
} //}
} }
if (threadMailItemViewModel != null) if (threadMailItemViewModel != null)
@@ -1142,5 +1100,8 @@ public partial class MailListPageViewModel : MailBaseViewModel,
} }
} }
public void Receive(ThumbnailAdded message) => MailCollection.UpdateThumbnails(message.Email); public void Receive(ThumbnailAdded message)
{
// MailCollection.UpdateThumbnails(message.Email);
}
} }
@@ -60,7 +60,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
public bool ShouldDisplayDownloadProgress => IsIndetermineProgress || (CurrentDownloadPercentage > 0 && CurrentDownloadPercentage <= 100); public bool ShouldDisplayDownloadProgress => IsIndetermineProgress || (CurrentDownloadPercentage > 0 && CurrentDownloadPercentage <= 100);
public bool CanUnsubscribe => CurrentRenderModel?.UnsubscribeInfo?.CanUnsubscribe ?? false; 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 public bool IsImageRenderingDisabled
{ {
@@ -285,9 +285,9 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
} }
}; };
var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(initializedMailItemViewModel.AssignedAccount.Id, draftOptions).ConfigureAwait(false); var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(initializedMailItemViewModel.MailCopy.AssignedAccount.Id, draftOptions).ConfigureAwait(false);
var draftPreparationRequest = new DraftPreparationRequest(initializedMailItemViewModel.AssignedAccount, draftMailCopy, draftBase64MimeMessage, draftOptions.Reason, initializedMailItemViewModel.MailCopy); var draftPreparationRequest = new DraftPreparationRequest(initializedMailItemViewModel.MailCopy.AssignedAccount, draftMailCopy, draftBase64MimeMessage, draftOptions.Reason, initializedMailItemViewModel.MailCopy);
await _requestDelegator.ExecuteAsync(draftPreparationRequest); await _requestDelegator.ExecuteAsync(draftPreparationRequest);
@@ -355,7 +355,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
// To show the progress on the UI. // To show the progress on the UI.
CurrentDownloadPercentage = 1; CurrentDownloadPercentage = 1;
var package = new DownloadMissingMessageRequested(mailItemViewModel.AssignedAccount.Id, mailItemViewModel.MailCopy); var package = new DownloadMissingMessageRequested(mailItemViewModel.MailCopy.AssignedAccount.Id, mailItemViewModel.MailCopy);
await _winoServerConnectionManager.GetResponseAsync<bool, DownloadMissingMessageRequested>(package); await _winoServerConnectionManager.GetResponseAsync<bool, DownloadMissingMessageRequested>(package);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
@@ -375,7 +375,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
private async Task RenderAsync(MailItemViewModel mailItemViewModel, CancellationToken cancellationToken = default) private async Task RenderAsync(MailItemViewModel mailItemViewModel, CancellationToken cancellationToken = default)
{ {
ResetProgress(); 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) if (!isMimeExists)
{ {
@@ -384,7 +384,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
// Find the MIME for this item and render it. // Find the MIME for this item and render it.
var mimeMessageInformation = await _mimeFileService.GetMimeMessageInformationAsync(mailItemViewModel.MailCopy.FileId, var mimeMessageInformation = await _mimeFileService.GetMimeMessageInformationAsync(mailItemViewModel.MailCopy.FileId,
mailItemViewModel.AssignedAccount.Id, mailItemViewModel.MailCopy.AssignedAccount.Id,
cancellationToken).ConfigureAwait(false); cancellationToken).ConfigureAwait(false);
if (mimeMessageInformation == null) if (mimeMessageInformation == null)
@@ -436,12 +436,12 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
FromAddress = message.From.Mailboxes.FirstOrDefault()?.Address ?? Translator.UnknownAddress; FromAddress = message.From.Mailboxes.FirstOrDefault()?.Address ?? Translator.UnknownAddress;
FromName = message.From.Mailboxes.FirstOrDefault()?.Name ?? Translator.UnknownSender; FromName = message.From.Mailboxes.FirstOrDefault()?.Name ?? Translator.UnknownSender;
CreationDate = message.Date.DateTime; CreationDate = message.Date.DateTime;
ContactPicture = initializedMailItemViewModel?.SenderContact?.Base64ContactPicture; ContactPicture = initializedMailItemViewModel?.MailCopy.SenderContact?.Base64ContactPicture;
// Automatically disable images for Junk folder to prevent pixel tracking. // Automatically disable images for Junk folder to prevent pixel tracking.
// This can only work for selected mail item rendering, not for EML file rendering. // This can only work for selected mail item rendering, not for EML file rendering.
if (initializedMailItemViewModel != null && if (initializedMailItemViewModel != null &&
initializedMailItemViewModel.AssignedFolder.SpecialFolderType == SpecialFolderType.Junk) initializedMailItemViewModel.MailCopy.AssignedFolder.SpecialFolderType == SpecialFolderType.Junk)
{ {
renderingOptions.LoadImages = false; renderingOptions.LoadImages = false;
} }
@@ -480,7 +480,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
var contactViewModel = new AccountContactViewModel(foundContact); var contactViewModel = new AccountContactViewModel(foundContact);
// Make sure that user account first in the list. // 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; contactViewModel.IsMe = true;
accounts.Insert(0, contactViewModel); accounts.Insert(0, contactViewModel);
@@ -563,7 +563,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
} }
// Archive - Unarchive // Archive - Unarchive
if (initializedMailItemViewModel.AssignedFolder.SpecialFolderType == SpecialFolderType.Archive) if (initializedMailItemViewModel.MailCopy.AssignedFolder.SpecialFolderType == SpecialFolderType.Archive)
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.UnArchive)); MenuItems.Add(MailOperationMenuItem.Create(MailOperation.UnArchive));
else else
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.Archive)); 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. // Check if the updated mail is the same mail item we are rendering.
// This is done with UniqueId to include FolderId into calculations. // 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. // 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. // So we need to update the mail item view model when this happens.
+8 -9
View File
@@ -19,7 +19,6 @@ using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Navigation;
using Wino.Core.WinUI.Controls; using Wino.Core.WinUI.Controls;
using Wino.Extensions; using Wino.Extensions;
using Wino.Mail.ViewModels.Data;
using Wino.MenuFlyouts; using Wino.MenuFlyouts;
using Wino.MenuFlyouts.Context; using Wino.MenuFlyouts.Context;
using Wino.Messaging.Client.Accounts; using Wino.Messaging.Client.Accounts;
@@ -61,14 +60,14 @@ public sealed partial class AppShell : AppShellAbstract,
foreach (var item in dragPackage.DraggingMails) foreach (var item in dragPackage.DraggingMails)
{ {
if (item is MailItemViewModel singleMailItemViewModel) //if (item is MailItemViewModel singleMailItemViewModel)
{ //{
mailCopies.Add(singleMailItemViewModel.MailCopy); // mailCopies.Add(singleMailItemViewModel.MailCopy);
} //}
else if (item is ThreadMailItemViewModel threadViewModel) //else if (item is ThreadMailItemViewModel threadViewModel)
{ //{
mailCopies.AddRange(threadViewModel.GetMailCopies()); // mailCopies.AddRange(threadViewModel.GetMailCopies());
} //}
} }
await ViewModel.PerformMoveOperationAsync(mailCopies, draggingFolder); 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 System.Windows.Input;
using CommunityToolkit.WinUI; using CommunityToolkit.WinUI;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
@@ -9,7 +8,6 @@ using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.MailItem;
using Wino.Extensions; using Wino.Extensions;
using Wino.Mail.ViewModels.Data;
namespace Wino.Controls; 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 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 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 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 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)); 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 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 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 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 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 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)); 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); } set { SetValue(IsHoverActionsEnabledProperty, value); }
} }
public IMailItem MailItem public MailCopy MailItem
{ {
get { return (IMailItem)GetValue(MailItemProperty); } get { return (MailCopy)GetValue(MailItemProperty); }
set { SetValue(MailItemProperty, value); } set { SetValue(MailItemProperty, value); }
} }
@@ -114,11 +111,6 @@ public sealed partial class MailItemDisplayInformationControl : UserControl
set { SetValue(IsAvatarVisibleProperty, value); } set { SetValue(IsAvatarVisibleProperty, value); }
} }
public bool IsCustomFocused
{
get { return (bool)GetValue(IsCustomFocusedProperty); }
set { SetValue(IsCustomFocusedProperty, value); }
}
public bool ShowPreviewText public bool ShowPreviewText
{ {
@@ -189,12 +181,12 @@ public sealed partial class MailItemDisplayInformationControl : UserControl
MailOperationPreperationRequest package = null; MailOperationPreperationRequest package = null;
if (MailItem is MailCopy mailCopy) //if (MailItem is MailCopy mailCopy)
package = new MailOperationPreperationRequest(operation, mailCopy, toggleExecution: true); // package = new MailOperationPreperationRequest(operation, mailCopy, toggleExecution: true);
else if (MailItem is ThreadMailItemViewModel threadMailItemViewModel) //else if (MailItem is ThreadMailItemViewModel threadMailItemViewModel)
package = new MailOperationPreperationRequest(operation, threadMailItemViewModel.GetMailCopies(), toggleExecution: true); // package = new MailOperationPreperationRequest(operation, threadMailItemViewModel.GetMailCopies(), toggleExecution: true);
else if (MailItem is ThreadMailItem threadMailItem) //else if (MailItem is ThreadMailItem threadMailItem)
package = new MailOperationPreperationRequest(operation, threadMailItem.ThreadItems.Cast<MailItemViewModel>().Select(a => a.MailCopy), toggleExecution: true); // package = new MailOperationPreperationRequest(operation, threadMailItem.ThreadItems.Cast<MailItemViewModel>().Select(a => a.MailCopy), toggleExecution: true);
if (package == null) return; 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="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:abstract="using:Wino.Views.Abstract" xmlns:abstract="using:Wino.Views.Abstract"
xmlns:advanced="using:Wino.Mail.WinUI.Controls.Advanced"
xmlns:animatedvisuals="using:Microsoft.UI.Xaml.Controls.AnimatedVisuals" xmlns:animatedvisuals="using:Microsoft.UI.Xaml.Controls.AnimatedVisuals"
xmlns:animations="using:CommunityToolkit.WinUI.Animations"
xmlns:collections="using:CommunityToolkit.Mvvm.Collections" xmlns:collections="using:CommunityToolkit.Mvvm.Collections"
xmlns:controls="using:Wino.Controls" xmlns:controls="using:Wino.Controls"
xmlns:converters="using:Wino.Converters" xmlns:converters="using:Wino.Converters"
xmlns:coreControls="using:Wino.Core.WinUI.Controls" xmlns:coreControls="using:Wino.Core.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:data="using:Wino.Mail.ViewModels.Collections"
xmlns:domain="using:Wino.Core.Domain" xmlns:domain="using:Wino.Core.Domain"
xmlns:enums="using:Wino.Core.Domain.Enums" xmlns:enums="using:Wino.Core.Domain.Enums"
xmlns:helpers="using:Wino.Helpers" xmlns:helpers="using:Wino.Helpers"
xmlns:i="using:Microsoft.Xaml.Interactivity" xmlns:i="using:Microsoft.Xaml.Interactivity"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity" xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:listview="using:Wino.Controls.Advanced"
xmlns:local="using:Wino.Behaviors" xmlns:local="using:Wino.Behaviors"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:menuflyouts="using:Wino.MenuFlyouts" xmlns:menuflyouts="using:Wino.MenuFlyouts"
xmlns:muxc="using:Microsoft.UI.Xaml.Controls" xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
xmlns:selectors="using:Wino.Selectors" xmlns:selectors="using:Wino.Selectors"
xmlns:selectors1="using:Wino.Mail.WinUI.Selectors"
xmlns:toolkit="using:CommunityToolkit.WinUI.Controls" xmlns:toolkit="using:CommunityToolkit.WinUI.Controls"
xmlns:toolkitExt="using:CommunityToolkit.WinUI" xmlns:toolkitExt="using:CommunityToolkit.WinUI"
xmlns:viewModelData="using:Wino.Mail.ViewModels.Data" xmlns:viewModelData="using:Wino.Mail.ViewModels.Data"
@@ -30,17 +33,11 @@
mc:Ignorable="d"> mc:Ignorable="d">
<Page.Resources> <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="ExpanderHeaderPadding">0,0,0,0</Thickness>
<Thickness x:Key="ExpanderChevronMargin">0,0,12,0</Thickness> <Thickness x:Key="ExpanderChevronMargin">0,0,12,0</Thickness>
<Thickness x:Key="ExpanderHeaderBorderThickness">0,0,0,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> <SolidColorBrush x:Key="ExpanderContentBorderBrush">Transparent</SolidColorBrush>
<StaticResource x:Key="ExpanderContentBackground" ResourceKey="CardBackgroundFillColorSecondaryBrush" /> <StaticResource x:Key="ExpanderContentBackground" ResourceKey="CardBackgroundFillColorSecondaryBrush" />
@@ -49,155 +46,210 @@
<SolidColorBrush x:Key="SystemControlRevealFocusVisualBrush" Color="#FF4C4A48" /> <SolidColorBrush x:Key="SystemControlRevealFocusVisualBrush" Color="#FF4C4A48" />
<SolidColorBrush x:Key="SystemControlFocusVisualSecondaryBrush" Color="#FFFFFFFF" /> <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> <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.Resources>
<Page.KeyboardAccelerators> <Page.KeyboardAccelerators>
@@ -299,8 +351,7 @@
VerticalAlignment="Center" VerticalAlignment="Center"
Canvas.ZIndex="100" Canvas.ZIndex="100"
Checked="SelectAllCheckboxChecked" Checked="SelectAllCheckboxChecked"
Unchecked="SelectAllCheckboxUnchecked" Unchecked="SelectAllCheckboxUnchecked" />
Visibility="{x:Bind helpers:XamlHelpers.IsSelectionModeMultiple(MailListView.SelectionMode), Mode=OneWay}" />
<!-- Folders --> <!-- Folders -->
@@ -448,70 +499,14 @@
<RowDefinition Height="*" /> <RowDefinition Height="*" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<RefreshContainer RefreshRequested="PullToRefreshRequested" Visibility="{x:Bind ViewModel.IsEmpty, Converter={StaticResource ReverseBooleanToVisibilityConverter}, Mode=OneWay}"> <advanced:WinoItemsView
<SemanticZoom x:Name="SemanticZoomContainer" CanChangeViews="{x:Bind ViewModel.PreferencesService.IsSemanticZoomEnabled, Mode=OneWay}"> x:Name="MailListView"
<SemanticZoom.ZoomedInView> Margin="4"
<listview:WinoListView ItemTemplate="{StaticResource MailItemContainerSelector}"
x:Name="MailListView" ItemsSource="{x:Bind ViewModel.MailCollection.Items, Mode=OneTime}"
HorizontalAlignment="Stretch" Layout="{StaticResource DefaultItemsViewLayout}"
HorizontalContentAlignment="Stretch" SelectionChanged="ListSelectionChanged"
toolkitExt:ListViewExtensions.ItemContainerStretchDirection="Horizontal" SelectionMode="Extended" />
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>
<!-- Try online search panel. --> <!-- Try online search panel. -->
<Grid Grid.Row="1" Visibility="{x:Bind ViewModel.IsOnlineSearchButtonVisible, Mode=OneWay}"> <Grid Grid.Row="1" Visibility="{x:Bind ViewModel.IsOnlineSearchButtonVisible, Mode=OneWay}">
+136 -87
View File
@@ -7,19 +7,19 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives; using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Hosting;
using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media.Animation; using Microsoft.UI.Xaml.Media.Animation;
using Microsoft.UI.Xaml.Navigation; using Microsoft.UI.Xaml.Navigation;
using MoreLinq; using MoreLinq;
using Windows.Foundation; using Windows.Foundation;
using Wino.Controls;
using Wino.Controls.Advanced;
using Wino.Core.Domain; using Wino.Core.Domain;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Menus; using Wino.Core.Domain.Models.Menus;
using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Navigation;
using Wino.Helpers;
using Wino.Mail.ViewModels.Data; using Wino.Mail.ViewModels.Data;
using Wino.Mail.ViewModels.Messages; using Wino.Mail.ViewModels.Messages;
using Wino.Mail.WinUI; using Wino.Mail.WinUI;
@@ -62,10 +62,6 @@ public sealed partial class MailListPage : MailListPageAbstract,
{ {
base.OnNavigatedFrom(e); base.OnNavigatedFrom(e);
// Dispose all WinoListView items.
MailListView.Dispose();
this.Bindings.StopTracking(); this.Bindings.StopTracking();
RenderingFrame.Navigate(typeof(IdlePage)); RenderingFrame.Navigate(typeof(IdlePage));
@@ -81,7 +77,7 @@ public sealed partial class MailListPage : MailListPageAbstract,
SelectAllCheckbox.Checked -= SelectAllCheckboxChecked; SelectAllCheckbox.Checked -= SelectAllCheckboxChecked;
SelectAllCheckbox.Unchecked -= SelectAllCheckboxUnchecked; 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.Checked += SelectAllCheckboxChecked;
SelectAllCheckbox.Unchecked += SelectAllCheckboxUnchecked; SelectAllCheckbox.Unchecked += SelectAllCheckboxUnchecked;
@@ -110,10 +106,10 @@ public sealed partial class MailListPage : MailListPageAbstract,
} }
} }
SelectAllCheckbox.IsChecked = false; // SelectAllCheckbox.IsChecked = false;
SelectionModeToggle.IsChecked = false; SelectionModeToggle.IsChecked = false;
MailListView.ClearSelections(); // MailListView.ClearSelections();
UpdateSelectAllButtonStatus(); UpdateSelectAllButtonStatus();
ViewModel.SelectedPivotChangedCommand.Execute(null); ViewModel.SelectedPivotChangedCommand.Execute(null);
@@ -121,7 +117,8 @@ public sealed partial class MailListPage : MailListPageAbstract,
private void ChangeSelectionMode(ListViewSelectionMode mode) 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) if (ViewModel?.PivotFolders != null)
{ {
@@ -136,44 +133,46 @@ public sealed partial class MailListPage : MailListPageAbstract,
private void SelectAllCheckboxChecked(object sender, RoutedEventArgs e) private void SelectAllCheckboxChecked(object sender, RoutedEventArgs e)
{ {
MailListView.SelectAllWino(); // MailListView.SelectAllWino();
} }
private void SelectAllCheckboxUnchecked(object sender, RoutedEventArgs e) private void SelectAllCheckboxUnchecked(object sender, RoutedEventArgs e)
{ {
MailListView.ClearSelections(); // MailListView.ClearSelections();
} }
private async void MailItemContextRequested(UIElement sender, ContextRequestedEventArgs args) 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. // 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. // This menu should be calculated based on all selected items by providers.
if (sender is MailItemDisplayInformationControl control && args.TryGetPosition(sender, out Point p)) //if (sender is MailItemDisplayInformationControl control && args.TryGetPosition(sender, out Point p))
{ //{
await FocusManager.TryFocusAsync(control, FocusState.Keyboard); // await FocusManager.TryFocusAsync(control, FocusState.Keyboard);
if (control.DataContext is IMailItem clickedMailItemContext) // if (control.DataContext is IMailItem clickedMailItemContext)
{ // {
var targetItems = ViewModel.GetTargetMailItemViewModels(clickedMailItemContext); // var targetItems = ViewModel.GetTargetMailItemViewModels(clickedMailItemContext);
var availableActions = ViewModel.GetAvailableMailActions(targetItems); // var availableActions = ViewModel.GetAvailableMailActions(targetItems);
if (!availableActions?.Any() ?? false) return; // if (!availableActions?.Any() ?? false) return;
var t = targetItems.ElementAt(0); // 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, private async Task<MailOperationMenuItem> GetMailOperationFromFlyoutAsync(IEnumerable<MailOperationMenuItem> availableActions,
@@ -196,7 +195,7 @@ public sealed partial class MailListPage : MailListPageAbstract,
void IRecipient<ClearMailSelectionsRequested>.Receive(ClearMailSelectionsRequested message) void IRecipient<ClearMailSelectionsRequested>.Receive(ClearMailSelectionsRequested message)
{ {
MailListView.ClearSelections(null, preserveThreadExpanding: true); // MailListView.ClearSelections(null, preserveThreadExpanding: true);
} }
void IRecipient<ActiveMailItemChangedEvent>.Receive(ActiveMailItemChangedEvent message) void IRecipient<ActiveMailItemChangedEvent>.Receive(ActiveMailItemChangedEvent message)
@@ -299,46 +298,46 @@ public sealed partial class MailListPage : MailListPageAbstract,
{ {
if (message.SelectedMailViewModel == null) return; if (message.SelectedMailViewModel == null) return;
await ViewModel.ExecuteUIThread(async () => //await ViewModel.ExecuteUIThread(async () =>
{ //{
MailListView.ClearSelections(message.SelectedMailViewModel, true); // MailListView.ClearSelections(message.SelectedMailViewModel, true);
int retriedSelectionCount = 0; // int retriedSelectionCount = 0;
trySelection: //trySelection:
bool isSelected = MailListView.SelectMailItemContainer(message.SelectedMailViewModel); // bool isSelected = MailListView.SelectMailItemContainer(message.SelectedMailViewModel);
if (!isSelected) // if (!isSelected)
{ // {
for (int i = retriedSelectionCount; i < 5;) // for (int i = retriedSelectionCount; i < 5;)
{ // {
// Retry with delay until the container is realized. Max 1 second. // // Retry with delay until the container is realized. Max 1 second.
await Task.Delay(200); // await Task.Delay(200);
retriedSelectionCount++; // retriedSelectionCount++;
goto trySelection; // goto trySelection;
} // }
} // }
// Automatically scroll to the selected item. // // Automatically scroll to the selected item.
// This is useful when creating draft. // // This is useful when creating draft.
if (isSelected && message.ScrollToItem) // if (isSelected && message.ScrollToItem)
{ // {
var collectionContainer = ViewModel.MailCollection.GetMailItemContainer(message.SelectedMailViewModel.UniqueId); // var collectionContainer = ViewModel.MailCollection.GetMailItemContainer(message.SelectedMailViewModel.UniqueId);
// Scroll to thread if available. // // Scroll to thread if available.
if (collectionContainer.ThreadViewModel != null) // if (collectionContainer.ThreadViewModel != null)
{ // {
MailListView.ScrollIntoView(collectionContainer.ThreadViewModel, ScrollIntoViewAlignment.Default); // MailListView.StartBringItemIntoView(collectionContainer.ThreadViewModel, new BringIntoViewOptions());
} // }
else if (collectionContainer.ItemViewModel != null) // else if (collectionContainer.ItemViewModel != null)
{ // {
MailListView.ScrollIntoView(collectionContainer.ItemViewModel, ScrollIntoViewAlignment.Default); // MailListView.StartBringItemIntoView(collectionContainer.ItemViewModel, new BringIntoViewOptions());
} // }
} // }
}); //});
} }
private void SearchBoxFocused(object sender, RoutedEventArgs e) private void SearchBoxFocused(object sender, RoutedEventArgs e)
@@ -357,32 +356,29 @@ public sealed partial class MailListPage : MailListPageAbstract,
/// </summary> /// </summary>
private void ThreadHeaderDragStart(UIElement sender, DragStartingEventArgs args) private void ThreadHeaderDragStart(UIElement sender, DragStartingEventArgs args)
{ {
if (sender is MailItemDisplayInformationControl control //if (sender is MailItemDisplayInformationControl control
&& control.ConnectedExpander?.Content is WinoListView contentListView) // && control.ConnectedExpander?.Content is WinoListView contentListView)
{ //{
var allItems = contentListView.Items.Where(a => a is IMailItem); // var allItems = contentListView.Items.Where(a => a is MailCopy);
// Highlight all items. // // Highlight all items.
allItems.Cast<MailItemViewModel>().ForEach(a => a.IsCustomFocused = true); // allItems.Cast<MailItemViewModel>().ForEach(a => a.IsCustomFocused = true);
// Set native drag arg properties. // // Set native drag arg properties.
args.AllowedOperations = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move; // 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.Data.Properties.Add(nameof(MailDragPackage), dragPackage);
args.DragUI.SetContentFromDataPackage(); // args.DragUI.SetContentFromDataPackage();
control.ConnectedExpander.IsExpanded = true; // control.ConnectedExpander.IsExpanded = true;
} //}
} }
private void ThreadHeaderDragFinished(UIElement sender, DropCompletedEventArgs args) 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) private async void LeftSwipeItemInvoked(Microsoft.UI.Xaml.Controls.SwipeItem sender, Microsoft.UI.Xaml.Controls.SwipeItemInvokedEventArgs args)
@@ -400,7 +396,7 @@ public sealed partial class MailListPage : MailListPageAbstract,
} }
else if (swipeControl.Tag is ThreadMailItemViewModel threadMailItemViewModel) 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); await ViewModel.ExecuteMailOperationAsync(package);
} }
} }
@@ -422,19 +418,15 @@ public sealed partial class MailListPage : MailListPageAbstract,
} }
else if (swipeControl.Tag is ThreadMailItemViewModel threadMailItemViewModel) 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 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); 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) private async void SearchBar_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
{ {
@@ -494,8 +486,65 @@ public sealed partial class MailListPage : MailListPageAbstract,
} }
private void SelectAllInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args) private void SelectAllInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
=> MailListView.SelectAllWino(); {
// MailListView.SelectAllWino();
}
private void DeleteAllInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args) private void DeleteAllInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
=> ViewModel.ExecuteMailOperationCommand.Execute(MailOperation.SoftDelete); => 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 System;
using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Entities.Mail;
namespace Wino.Messaging.Client.Accounts; 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="AutoSelectAccount">Account to extend menu item for.</param>
/// <param name="FolderId">Folder to select after expansion.</param> /// <param name="FolderId">Folder to select after expansion.</param>
/// <param name="NavigateMailItem">Mail item to select if possible in the expanded folder.</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 System;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.MailItem;
namespace Wino.Messaging.Server; namespace Wino.Messaging.Server;
@@ -10,4 +10,4 @@ namespace Wino.Messaging.Server;
/// </summary> /// </summary>
/// <param name="AccountId">Account id for corresponding synchronizer.</param> /// <param name="AccountId">Account id for corresponding synchronizer.</param>
/// <param name="MailCopyId">Mail copy id to download.</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> <ItemGroup>
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" /> <ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Client\Authorization\" /> <Folder Include="Client\Authorization\" />
</ItemGroup> </ItemGroup>
+4 -4
View File
@@ -1,9 +1,9 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Folders; using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Menus; using Wino.Core.Domain.Models.Menus;
namespace Wino.Services; namespace Wino.Services;
@@ -35,7 +35,7 @@ public class ContextMenuItemService : IContextMenuItemService
return list; return list;
} }
public virtual IEnumerable<MailOperationMenuItem> GetMailItemContextMenuActions(IEnumerable<IMailItem> selectedMailItems) public virtual IEnumerable<MailOperationMenuItem> GetMailItemContextMenuActions(IEnumerable<MailCopy> selectedMailItems)
{ {
if (selectedMailItems == null) if (selectedMailItems == null)
return default; return default;
@@ -50,7 +50,7 @@ public class ContextMenuItemService : IContextMenuItemService
bool isSingleItem = selectedMailItems.Count() == 1; bool isSingleItem = selectedMailItems.Count() == 1;
IMailItem singleItem = selectedMailItems.FirstOrDefault(); MailCopy singleItem = selectedMailItems.FirstOrDefault();
// Archive button. // Archive button.
@@ -125,7 +125,7 @@ public class ContextMenuItemService : IContextMenuItemService
return operationList; 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>(); 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.Exceptions;
using Wino.Core.Domain.Extensions; using Wino.Core.Domain.Extensions;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Comparers;
using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.MailItem;
using Wino.Messaging.UI; using Wino.Messaging.UI;
using Wino.Services.Extensions; using Wino.Services.Extensions;
@@ -29,7 +28,6 @@ public class MailService : BaseDatabaseService, IMailService
private readonly IContactService _contactService; private readonly IContactService _contactService;
private readonly IAccountService _accountService; private readonly IAccountService _accountService;
private readonly ISignatureService _signatureService; private readonly ISignatureService _signatureService;
private readonly IThreadingStrategyProvider _threadingStrategyProvider;
private readonly IMimeFileService _mimeFileService; private readonly IMimeFileService _mimeFileService;
private readonly IPreferencesService _preferencesService; private readonly IPreferencesService _preferencesService;
@@ -40,7 +38,6 @@ public class MailService : BaseDatabaseService, IMailService
IContactService contactService, IContactService contactService,
IAccountService accountService, IAccountService accountService,
ISignatureService signatureService, ISignatureService signatureService,
IThreadingStrategyProvider threadingStrategyProvider,
IMimeFileService mimeFileService, IMimeFileService mimeFileService,
IPreferencesService preferencesService) : base(databaseService) IPreferencesService preferencesService) : base(databaseService)
{ {
@@ -48,7 +45,6 @@ public class MailService : BaseDatabaseService, IMailService
_contactService = contactService; _contactService = contactService;
_accountService = accountService; _accountService = accountService;
_signatureService = signatureService; _signatureService = signatureService;
_threadingStrategyProvider = threadingStrategyProvider;
_mimeFileService = mimeFileService; _mimeFileService = mimeFileService;
_preferencesService = preferencesService; _preferencesService = preferencesService;
} }
@@ -209,7 +205,7 @@ public class MailService : BaseDatabaseService, IMailService
return query.GetRawQuery(); 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; List<MailCopy> mails = null;
@@ -241,68 +237,20 @@ public class MailService : BaseDatabaseService, IMailService
// Remove items that has no assigned account or folder. // Remove items that has no assigned account or folder.
mails.RemoveAll(a => a.AssignedAccount == null || a.AssignedFolder == null); 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(); 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> /// <summary>
/// This method should used for operations with multiple mailItems. Don't use this for single mail items. /// 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. /// Called method should provide own instances for caches.
/// </summary> /// </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) if (mail is MailCopy mailCopy)
{ {
var isFolderCached = folderCache.TryGetValue(mailCopy.FolderId, out MailItemFolder folderAssignment); var isFolderCached = folderCache.TryGetValue(mailCopy.FolderId, out MailItemFolder folderAssignment);
-8
View File
@@ -1,6 +1,5 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Services.Threading;
namespace Wino.Services; namespace Wino.Services;
@@ -24,12 +23,5 @@ public static class ServicesContainerSetup
services.AddTransient<ISignatureService, SignatureService>(); services.AddTransient<ISignatureService, SignatureService>();
services.AddTransient<IContextMenuItemService, ContextMenuItemService>(); services.AddTransient<IContextMenuItemService, ContextMenuItemService>();
services.AddTransient<ISpecialImapProviderConfigResolver, SpecialImapProviderConfigResolver>(); 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,
};
}
}
+5 -3
View File
@@ -5,6 +5,11 @@
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly> <ProduceReferenceAssembly>true</ProduceReferenceAssembly>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Compile Remove="Misc\**" />
<EmbeddedResource Remove="Misc\**" />
<None Remove="Misc\**" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="HtmlAgilityPack" /> <PackageReference Include="HtmlAgilityPack" />
<PackageReference Include="Ical.Net" /> <PackageReference Include="Ical.Net" />
@@ -20,7 +25,4 @@
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" /> <ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" />
<ProjectReference Include="..\Wino.Messages\Wino.Messaging.csproj" /> <ProjectReference Include="..\Wino.Messages\Wino.Messaging.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Misc\" />
</ItemGroup>
</Project> </Project>