ItemsView thing.
This commit is contained in:
@@ -3,7 +3,6 @@ using System.Collections.Generic;
|
||||
using SQLite;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
|
||||
namespace Wino.Core.Domain.Entities.Mail;
|
||||
|
||||
@@ -11,7 +10,7 @@ namespace Wino.Core.Domain.Entities.Mail;
|
||||
/// Summary of the parsed MIME messages.
|
||||
/// Wino will do non-network operations on this table and others from the original MIME.
|
||||
/// </summary>
|
||||
public class MailCopy : IMailItem
|
||||
public class MailCopy
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique Id of the mail.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models.Menus;
|
||||
|
||||
namespace Wino.Core.Domain.Interfaces;
|
||||
@@ -8,6 +8,6 @@ namespace Wino.Core.Domain.Interfaces;
|
||||
public interface IContextMenuItemService
|
||||
{
|
||||
IEnumerable<FolderOperationMenuItem> GetFolderContextMenuActions(IBaseFolderMenuItem folderInformation);
|
||||
IEnumerable<MailOperationMenuItem> GetMailItemContextMenuActions(IEnumerable<IMailItem> selectedMailItems);
|
||||
IEnumerable<MailOperationMenuItem> GetMailItemRenderMenuActions(IMailItem mailItem, bool isDarkEditor);
|
||||
IEnumerable<MailOperationMenuItem> GetMailItemContextMenuActions(IEnumerable<MailCopy> selectedMailItems);
|
||||
IEnumerable<MailOperationMenuItem> GetMailItemRenderMenuActions(MailCopy mailItem, bool isDarkEditor);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models.Menus;
|
||||
|
||||
namespace Wino.Core.Domain.Interfaces;
|
||||
@@ -18,12 +18,12 @@ public interface IContextMenuProvider
|
||||
/// </summary>
|
||||
/// <param name="folderInformation">Current folder that asks for the menu items.</param>
|
||||
/// <param name="selectedMailItems">Selected menu items in the given folder.</param>
|
||||
IEnumerable<MailOperationMenuItem> GetMailItemContextMenuActions(IMailItemFolder folderInformation, IEnumerable<IMailItem> selectedMailItems);
|
||||
IEnumerable<MailOperationMenuItem> GetMailItemContextMenuActions(IMailItemFolder folderInformation, IEnumerable<MailCopy> selectedMailItems);
|
||||
|
||||
/// <summary>
|
||||
/// Calculates and returns available mail operations for mail rendering CommandBar.
|
||||
/// </summary>
|
||||
/// <param name="mailItem">Rendered mail item.</param>
|
||||
/// <param name="activeFolder">Folder that mail item belongs to.</param>
|
||||
IEnumerable<MailOperationMenuItem> GetMailItemRenderMenuActions(IMailItem mailItem, IMailItemFolder activeFolder, bool isDarkEditor);
|
||||
IEnumerable<MailOperationMenuItem> GetMailItemRenderMenuActions(MailCopy mailItem, IMailItemFolder activeFolder, bool isDarkEditor);
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
namespace Wino.Core.Domain.Interfaces;
|
||||
|
||||
public interface IGmailThreadingStrategy : IThreadingStrategy { }
|
||||
@@ -1,3 +0,0 @@
|
||||
namespace Wino.Core.Domain.Interfaces;
|
||||
|
||||
public interface IImapThreadingStrategy : IThreadingStrategy { }
|
||||
@@ -25,7 +25,7 @@ public interface IMailService
|
||||
/// Caution: This method is not safe. Use other overrides.
|
||||
/// </summary>
|
||||
Task<List<MailCopy>> GetMailItemsAsync(IEnumerable<string> mailCopyIds);
|
||||
Task<List<IMailItem>> FetchMailsAsync(MailListInitializationOptions options, CancellationToken cancellationToken = default);
|
||||
Task<List<MailCopy>> FetchMailsAsync(MailListInitializationOptions options, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all mail copies for all folders.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
|
||||
namespace Wino.Core.Domain.Interfaces;
|
||||
|
||||
@@ -10,7 +10,7 @@ public interface INotificationBuilder
|
||||
/// <summary>
|
||||
/// Creates toast notifications for new mails.
|
||||
/// </summary>
|
||||
Task CreateNotificationsAsync(Guid inboxFolderId, IEnumerable<IMailItem> newMailItems);
|
||||
Task CreateNotificationsAsync(Guid inboxFolderId, IEnumerable<MailCopy> newMailItems);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the unread Inbox messages for each account and updates the taskbar icon.
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
namespace Wino.Core.Domain.Interfaces;
|
||||
|
||||
public interface IOutlookThreadingStrategy : IThreadingStrategy { }
|
||||
@@ -1,18 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
|
||||
namespace Wino.Core.Domain.Interfaces;
|
||||
|
||||
public interface IThreadingStrategy
|
||||
{
|
||||
/// <summary>
|
||||
/// Attach thread mails to the list.
|
||||
/// </summary>
|
||||
/// <param name="items">Original mails.</param>
|
||||
/// <returns>Original mails with thread mails.</returns>
|
||||
Task<List<IMailItem>> ThreadItemsAsync(List<MailCopy> items, IMailItemFolder threadingForFolder);
|
||||
bool ShouldThreadWithItem(IMailItem originalItem, IMailItem targetItem);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Core.Domain.Interfaces;
|
||||
|
||||
public interface IThreadingStrategyProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns corresponding threading strategy that applies to given provider type.
|
||||
/// </summary>
|
||||
/// <param name="mailProviderType">Provider type.</param>
|
||||
IThreadingStrategy GetStrategy(MailProviderType mailProviderType);
|
||||
}
|
||||
@@ -4,7 +4,6 @@ using System.Threading.Tasks;
|
||||
using MailKit;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
|
||||
namespace Wino.Core.Domain.Interfaces;
|
||||
@@ -30,7 +29,7 @@ public interface IWinoSynchronizerBase : IBaseSynchronizer
|
||||
/// <param name="mailItem">Mail item to download from server.</param>
|
||||
/// <param name="transferProgress">Optional progress reporting for download operation.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
Task DownloadMissingMimeMessageAsync(IMailItem mailItem, ITransferProgress transferProgress, CancellationToken cancellationToken = default);
|
||||
Task DownloadMissingMimeMessageAsync(MailCopy mailItem, ITransferProgress transferProgress, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 1. Cancel active synchronization.
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
|
||||
namespace Wino.Core.Domain.Models.Comparers;
|
||||
|
||||
public class DateComparer : IComparer<IMailItem>, IEqualityComparer
|
||||
{
|
||||
public int Compare(IMailItem x, IMailItem y)
|
||||
{
|
||||
return DateTime.Compare(y.CreationDate, x.CreationDate);
|
||||
}
|
||||
|
||||
public new bool Equals(object x, object y)
|
||||
{
|
||||
if (x is IMailItem firstItem && y is IMailItem secondItem)
|
||||
{
|
||||
return firstItem.Equals(secondItem);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public int GetHashCode(object obj) => (obj as IMailItem).GetHashCode();
|
||||
|
||||
public DateComparer()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
|
||||
namespace Wino.Core.Domain.Models.Comparers;
|
||||
|
||||
public class ListItemComparer : IComparer<object>
|
||||
{
|
||||
public bool SortByName { get; set; }
|
||||
|
||||
public DateComparer DateComparer = new DateComparer();
|
||||
public readonly NameComparer NameComparer = new NameComparer();
|
||||
|
||||
public int Compare(object x, object y)
|
||||
{
|
||||
if (x is IMailItem xMail && y is IMailItem yMail)
|
||||
{
|
||||
var itemComparer = GetItemComparer();
|
||||
|
||||
return itemComparer.Compare(xMail, yMail);
|
||||
}
|
||||
else if (x is DateTime dateX && y is DateTime dateY)
|
||||
return DateTime.Compare(dateY, dateX);
|
||||
else if (x is string stringX && y is string stringY)
|
||||
return stringY.CompareTo(stringX);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public IComparer<IMailItem> GetItemComparer()
|
||||
{
|
||||
if (SortByName)
|
||||
return NameComparer;
|
||||
else
|
||||
return DateComparer;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
|
||||
namespace Wino.Core.Domain.Models.Comparers;
|
||||
|
||||
public class NameComparer : IComparer<IMailItem>
|
||||
{
|
||||
public int Compare(IMailItem x, IMailItem y)
|
||||
{
|
||||
return string.Compare(x.FromName, y.FromName);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Wino.Core.Domain.Models.MailItem;
|
||||
|
||||
/// <summary>
|
||||
/// An interface that returns the UniqueId store for IMailItem.
|
||||
/// For threads, it may be multiple items.
|
||||
/// For single mails, it'll always be one item.
|
||||
/// </summary>
|
||||
public interface IMailHashContainer
|
||||
{
|
||||
IEnumerable<Guid> GetContainingIds();
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
using System;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
|
||||
namespace Wino.Core.Domain.Models.MailItem;
|
||||
|
||||
/// <summary>
|
||||
/// Interface of simplest representation of a MailCopy.
|
||||
/// </summary>
|
||||
public interface IMailItem : IMailHashContainer
|
||||
{
|
||||
Guid UniqueId { get; }
|
||||
string Id { get; }
|
||||
string Subject { get; }
|
||||
string ThreadId { get; }
|
||||
string MessageId { get; }
|
||||
string References { get; }
|
||||
string InReplyTo { get; }
|
||||
string PreviewText { get; }
|
||||
string FromName { get; }
|
||||
DateTime CreationDate { get; }
|
||||
string FromAddress { get; }
|
||||
bool HasAttachments { get; }
|
||||
bool IsFlagged { get; }
|
||||
bool IsFocused { get; }
|
||||
bool IsRead { get; }
|
||||
string DraftId { get; }
|
||||
bool IsDraft { get; }
|
||||
Guid FileId { get; }
|
||||
|
||||
MailItemFolder AssignedFolder { get; }
|
||||
MailAccount AssignedAccount { get; }
|
||||
AccountContact SenderContact { get; }
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Wino.Core.Domain.Models.MailItem;
|
||||
|
||||
/// <summary>
|
||||
/// Interface that represents conversation threads.
|
||||
/// Even though this type has 1 single UI representation most of the time,
|
||||
/// it can contain multiple IMailItem.
|
||||
/// </summary>
|
||||
public interface IMailItemThread : IMailItem
|
||||
{
|
||||
ObservableCollection<IMailItem> ThreadItems { get; }
|
||||
IMailItem LatestMailItem { get; }
|
||||
IMailItem FirstMailItem { get; }
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
|
||||
namespace Wino.Core.Domain.Models.MailItem;
|
||||
|
||||
@@ -7,12 +8,12 @@ namespace Wino.Core.Domain.Models.MailItem;
|
||||
/// </summary>
|
||||
public class MailDragPackage
|
||||
{
|
||||
public MailDragPackage(IEnumerable<IMailItem> draggingMails)
|
||||
public MailDragPackage(IEnumerable<MailCopy> draggingMails)
|
||||
{
|
||||
DraggingMails = draggingMails;
|
||||
}
|
||||
|
||||
public MailDragPackage(IMailItem draggingMail)
|
||||
public MailDragPackage(MailCopy draggingMail)
|
||||
{
|
||||
DraggingMails =
|
||||
[
|
||||
@@ -20,5 +21,5 @@ public class MailDragPackage
|
||||
];
|
||||
}
|
||||
|
||||
public IEnumerable<IMailItem> DraggingMails { get; set; }
|
||||
public IEnumerable<MailCopy> DraggingMails { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
|
||||
namespace Wino.Core.Domain.Models.MailItem;
|
||||
|
||||
public class ThreadMailItem : IMailItemThread
|
||||
{
|
||||
// TODO: Ideally this should be SortedList.
|
||||
public ObservableCollection<IMailItem> ThreadItems { get; } = new ObservableCollection<IMailItem>();
|
||||
|
||||
public IMailItem LatestMailItem => ThreadItems.LastOrDefault();
|
||||
public IMailItem FirstMailItem => ThreadItems.FirstOrDefault();
|
||||
|
||||
public bool AddThreadItem(IMailItem item)
|
||||
{
|
||||
if (item == null) return false;
|
||||
|
||||
if (ThreadItems.Any(a => a.Id == item.Id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (item != null && item.IsDraft)
|
||||
{
|
||||
ThreadItems.Insert(0, item);
|
||||
return true;
|
||||
}
|
||||
|
||||
var insertItem = ThreadItems.FirstOrDefault(a => !a.IsDraft && a.CreationDate < item.CreationDate);
|
||||
|
||||
if (insertItem == null)
|
||||
ThreadItems.Insert(ThreadItems.Count, item);
|
||||
else
|
||||
{
|
||||
var index = ThreadItems.IndexOf(insertItem);
|
||||
|
||||
ThreadItems.Insert(index, item);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public IEnumerable<Guid> GetContainingIds() => ThreadItems?.Select(a => a.UniqueId) ?? default;
|
||||
|
||||
#region IMailItem
|
||||
|
||||
public Guid UniqueId => LatestMailItem?.UniqueId ?? Guid.Empty;
|
||||
public string Id => LatestMailItem?.Id ?? string.Empty;
|
||||
|
||||
// Show subject from last item.
|
||||
public string Subject => LatestMailItem?.Subject ?? string.Empty;
|
||||
|
||||
public string ThreadId => LatestMailItem?.ThreadId ?? string.Empty;
|
||||
|
||||
public string PreviewText => FirstMailItem?.PreviewText ?? string.Empty;
|
||||
|
||||
public string FromName => LatestMailItem?.FromName ?? string.Empty;
|
||||
|
||||
public string FromAddress => LatestMailItem?.FromAddress ?? string.Empty;
|
||||
|
||||
public bool HasAttachments => ThreadItems.Any(a => a.HasAttachments);
|
||||
|
||||
public bool IsFlagged => ThreadItems.Any(a => a.IsFlagged);
|
||||
|
||||
public bool IsFocused => LatestMailItem?.IsFocused ?? false;
|
||||
|
||||
public bool IsRead => ThreadItems.All(a => a.IsRead);
|
||||
|
||||
public DateTime CreationDate => FirstMailItem?.CreationDate ?? DateTime.MinValue;
|
||||
|
||||
public bool IsDraft => ThreadItems.Any(a => a.IsDraft);
|
||||
|
||||
public string DraftId => string.Empty;
|
||||
|
||||
public string MessageId => LatestMailItem?.MessageId;
|
||||
|
||||
public string References => LatestMailItem?.References ?? string.Empty;
|
||||
|
||||
public string InReplyTo => LatestMailItem?.InReplyTo ?? string.Empty;
|
||||
|
||||
public MailItemFolder AssignedFolder => LatestMailItem?.AssignedFolder;
|
||||
|
||||
public MailAccount AssignedAccount => LatestMailItem?.AssignedAccount;
|
||||
|
||||
public Guid FileId => LatestMailItem?.FileId ?? Guid.Empty;
|
||||
|
||||
public AccountContact SenderContact => LatestMailItem?.SenderContact;
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Core.Domain.Models.MailItem;
|
||||
@@ -10,4 +11,4 @@ namespace Wino.Core.Domain.Models.MailItem;
|
||||
/// <param name="SourceAction"></param>
|
||||
/// <param name="TargetAction"></param>
|
||||
/// <param name="Condition"></param>
|
||||
public record ToggleRequestRule(MailOperation SourceAction, MailOperation TargetAction, Func<IMailItem, bool> Condition);
|
||||
public record ToggleRequestRule(MailOperation SourceAction, MailOperation TargetAction, Func<MailCopy, bool> Condition);
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Models.Comparers;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Core.Domain.Models.Reader;
|
||||
|
||||
@@ -9,16 +6,6 @@ public class SortingOption
|
||||
{
|
||||
public SortingOptionType Type { get; set; }
|
||||
public string Title { get; set; }
|
||||
public IComparer<IMailItem> Comparer
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Type == SortingOptionType.ReceiveDate)
|
||||
return new DateComparer();
|
||||
else
|
||||
return new NameComparer();
|
||||
}
|
||||
}
|
||||
|
||||
public SortingOption(string title, SortingOptionType type)
|
||||
{
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Models.Accounts;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
|
||||
namespace Wino.Core.Domain.Models.Synchronization;
|
||||
|
||||
@@ -16,7 +16,7 @@ public class MailSynchronizationResult
|
||||
/// It's ignored in serialization. Client should not react to this.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public IEnumerable<IMailItem> DownloadedMessages { get; set; } = [];
|
||||
public IEnumerable<MailCopy> DownloadedMessages { get; set; } = [];
|
||||
|
||||
public ProfileInformation ProfileInformation { get; set; }
|
||||
|
||||
@@ -25,7 +25,7 @@ public class MailSynchronizationResult
|
||||
public static MailSynchronizationResult Empty => new() { CompletedState = SynchronizationCompletedState.Success };
|
||||
|
||||
// Mail synchronization
|
||||
public static MailSynchronizationResult Completed(IEnumerable<IMailItem> downloadedMessages)
|
||||
public static MailSynchronizationResult Completed(IEnumerable<MailCopy> downloadedMessages)
|
||||
=> new()
|
||||
{
|
||||
DownloadedMessages = downloadedMessages,
|
||||
|
||||
@@ -12,9 +12,9 @@ using Microsoft.UI.Xaml.Media;
|
||||
using Windows.UI;
|
||||
using Windows.UI.Text;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.WinUI.Controls;
|
||||
|
||||
namespace Wino.Helpers;
|
||||
@@ -104,9 +104,9 @@ public static class XamlHelpers
|
||||
public static Color GetWindowsColorFromHex(string hex) => hex.ToColor();
|
||||
|
||||
public static SolidColorBrush GetSolidColorBrushFromHex(string colorHex) => string.IsNullOrEmpty(colorHex) ? new SolidColorBrush(Colors.Transparent) : new SolidColorBrush(colorHex.ToColor());
|
||||
public static Visibility IsSelectionModeMultiple(ListViewSelectionMode mode) => mode == ListViewSelectionMode.Multiple ? Visibility.Visible : Visibility.Collapsed;
|
||||
public static FontWeight GetFontWeightBySyncState(bool isSyncing) => isSyncing ? FontWeights.SemiBold : FontWeights.Normal;
|
||||
public static FontWeight GetFontWeightByChildSelectedState(bool isChildSelected) => isChildSelected ? FontWeights.SemiBold : FontWeights.Normal;
|
||||
public static FontWeight GetFontWeightByReadState(bool isChildSelected) => isChildSelected ? FontWeights.Normal : FontWeights.SemiBold;
|
||||
public static Visibility StringToVisibilityConverter(string value) => string.IsNullOrWhiteSpace(value) ? Visibility.Collapsed : Visibility.Visible;
|
||||
public static Visibility StringToVisibilityReversedConverter(string value) => string.IsNullOrWhiteSpace(value) ? Visibility.Visible : Visibility.Collapsed;
|
||||
public static string GetMailItemDisplaySummaryForListing(bool isDraft, DateTime receivedDate, bool prefer24HourTime)
|
||||
@@ -135,7 +135,7 @@ public static class XamlHelpers
|
||||
// From regular mail header template
|
||||
if (groupObject is DateTime groupedDate)
|
||||
dateObject = groupedDate;
|
||||
else if (groupObject is IGrouping<object, IMailItem> groupKey)
|
||||
else if (groupObject is IGrouping<object, MailCopy> groupKey)
|
||||
{
|
||||
// From semantic group header.
|
||||
dateObject = groupKey.Key;
|
||||
|
||||
@@ -12,7 +12,6 @@ using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Messaging.UI;
|
||||
|
||||
namespace Wino.Core.WinUI.Services;
|
||||
@@ -45,7 +44,7 @@ public class NotificationBuilder : INotificationBuilder
|
||||
});
|
||||
}
|
||||
|
||||
public async Task CreateNotificationsAsync(Guid inboxFolderId, IEnumerable<IMailItem> downloadedMailItems)
|
||||
public async Task CreateNotificationsAsync(Guid inboxFolderId, IEnumerable<MailCopy> downloadedMailItems)
|
||||
{
|
||||
var mailCount = downloadedMailItems.Count();
|
||||
|
||||
@@ -70,7 +69,7 @@ public class NotificationBuilder : INotificationBuilder
|
||||
}
|
||||
else
|
||||
{
|
||||
var validItems = new List<IMailItem>();
|
||||
var validItems = new List<MailCopy>();
|
||||
|
||||
// Fetch mails again to fill up assigned folder data and latest statuses.
|
||||
// They've been marked as read by executing synchronizer tasks until inital sync finishes.
|
||||
@@ -220,17 +219,17 @@ public class NotificationBuilder : INotificationBuilder
|
||||
public async Task CreateTestNotificationAsync(string title, string message)
|
||||
{
|
||||
// with args test.
|
||||
await CreateNotificationsAsync(Guid.Parse("28c3c39b-7147-4de3-b209-949bd19eede6"), new List<IMailItem>()
|
||||
{
|
||||
new MailCopy()
|
||||
{
|
||||
Subject = "test subject",
|
||||
PreviewText = "preview html",
|
||||
CreationDate = DateTime.UtcNow,
|
||||
FromAddress = "bkaankose@outlook.com",
|
||||
Id = "AAkALgAAAAAAHYQDEapmEc2byACqAC-EWg0AnMdP0zg8wkS_Ib2Eeh80LAAGq91I3QAA",
|
||||
}
|
||||
});
|
||||
//await CreateNotificationsAsync(Guid.Parse("28c3c39b-7147-4de3-b209-949bd19eede6"), new List<IMailItem>()
|
||||
//{
|
||||
// new MailCopy()
|
||||
// {
|
||||
// Subject = "test subject",
|
||||
// PreviewText = "preview html",
|
||||
// CreationDate = DateTime.UtcNow,
|
||||
// FromAddress = "bkaankose@outlook.com",
|
||||
// Id = "AAkALgAAAAAAHYQDEapmEc2byACqAC-EWg0AnMdP0zg8wkS_Ib2Eeh80LAAGq91I3QAA",
|
||||
// }
|
||||
//});
|
||||
|
||||
//var builder = new ToastContentBuilder();
|
||||
//builder.SetToastScenario(ToastScenario.Default);
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Requests.Folder;
|
||||
using Wino.Core.Requests.Mail;
|
||||
|
||||
namespace Wino.Core.Integration.Json;
|
||||
|
||||
public class ServerRequestTypeInfoResolver : DefaultJsonTypeInfoResolver
|
||||
{
|
||||
public ServerRequestTypeInfoResolver()
|
||||
{
|
||||
Modifiers.Add(new System.Action<JsonTypeInfo>(t =>
|
||||
{
|
||||
if (t.Type == typeof(IRequestBase))
|
||||
{
|
||||
t.PolymorphismOptions = new()
|
||||
{
|
||||
DerivedTypes =
|
||||
{
|
||||
new JsonDerivedType(typeof(AlwaysMoveToRequest), nameof(AlwaysMoveToRequest)),
|
||||
new JsonDerivedType(typeof(ArchiveRequest), nameof(ArchiveRequest)),
|
||||
new JsonDerivedType(typeof(ChangeFlagRequest), nameof(ChangeFlagRequest)),
|
||||
new JsonDerivedType(typeof(CreateDraftRequest), nameof(CreateDraftRequest)),
|
||||
new JsonDerivedType(typeof(DeleteRequest), nameof(DeleteRequest)),
|
||||
new JsonDerivedType(typeof(EmptyFolderRequest), nameof(EmptyFolderRequest)),
|
||||
new JsonDerivedType(typeof(MarkFolderAsReadRequest), nameof(MarkFolderAsReadRequest)),
|
||||
new JsonDerivedType(typeof(MarkReadRequest), nameof(MarkReadRequest)),
|
||||
new JsonDerivedType(typeof(MoveRequest), nameof(MoveRequest)),
|
||||
new JsonDerivedType(typeof(MoveToFocusedRequest), nameof(MoveToFocusedRequest)),
|
||||
new JsonDerivedType(typeof(RenameFolderRequest), nameof(RenameFolderRequest)),
|
||||
new JsonDerivedType(typeof(SendDraftRequest), nameof(SendDraftRequest)),
|
||||
}
|
||||
};
|
||||
}
|
||||
else if (t.Type == typeof(IMailItem))
|
||||
{
|
||||
t.PolymorphismOptions = new JsonPolymorphismOptions()
|
||||
{
|
||||
DerivedTypes =
|
||||
{
|
||||
new JsonDerivedType(typeof(MailCopy), nameof(MailCopy)),
|
||||
}
|
||||
};
|
||||
}
|
||||
else if (t.Type == typeof(IMailItemFolder))
|
||||
{
|
||||
t.PolymorphismOptions = new JsonPolymorphismOptions()
|
||||
{
|
||||
DerivedTypes =
|
||||
{
|
||||
new JsonDerivedType(typeof(MailItemFolder), nameof(MailItemFolder)),
|
||||
}
|
||||
};
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Services.Threading;
|
||||
|
||||
namespace Wino.Core.Services;
|
||||
|
||||
public class ThreadingStrategyProvider : IThreadingStrategyProvider
|
||||
{
|
||||
private readonly OutlookThreadingStrategy _outlookThreadingStrategy;
|
||||
private readonly GmailThreadingStrategy _gmailThreadingStrategy;
|
||||
private readonly ImapThreadingStrategy _imapThreadStrategy;
|
||||
|
||||
public ThreadingStrategyProvider(OutlookThreadingStrategy outlookThreadingStrategy,
|
||||
GmailThreadingStrategy gmailThreadingStrategy,
|
||||
ImapThreadingStrategy imapThreadStrategy)
|
||||
{
|
||||
_outlookThreadingStrategy = outlookThreadingStrategy;
|
||||
_gmailThreadingStrategy = gmailThreadingStrategy;
|
||||
_imapThreadStrategy = imapThreadStrategy;
|
||||
}
|
||||
|
||||
public IThreadingStrategy GetStrategy(MailProviderType mailProviderType)
|
||||
{
|
||||
return mailProviderType switch
|
||||
{
|
||||
MailProviderType.Outlook => _outlookThreadingStrategy,
|
||||
MailProviderType.Gmail => _gmailThreadingStrategy,
|
||||
_ => _imapThreadStrategy,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -31,10 +31,10 @@ public class WinoRequestProcessor : IWinoRequestProcessor
|
||||
/// </summary>
|
||||
private readonly List<ToggleRequestRule> _toggleRequestRules =
|
||||
[
|
||||
new ToggleRequestRule(MailOperation.MarkAsRead, MailOperation.MarkAsUnread, new System.Func<IMailItem, bool>((item) => item.IsRead)),
|
||||
new ToggleRequestRule(MailOperation.MarkAsUnread, MailOperation.MarkAsRead, new System.Func<IMailItem, bool>((item) => !item.IsRead)),
|
||||
new ToggleRequestRule(MailOperation.SetFlag, MailOperation.ClearFlag, new System.Func<IMailItem, bool>((item) => item.IsFlagged)),
|
||||
new ToggleRequestRule(MailOperation.ClearFlag, MailOperation.SetFlag, new System.Func<IMailItem, bool>((item) => !item.IsFlagged)),
|
||||
new ToggleRequestRule(MailOperation.MarkAsRead, MailOperation.MarkAsUnread, new System.Func<MailCopy, bool>((item) => item.IsRead)),
|
||||
new ToggleRequestRule(MailOperation.MarkAsUnread, MailOperation.MarkAsRead, new System.Func<MailCopy, bool>((item) => !item.IsRead)),
|
||||
new ToggleRequestRule(MailOperation.SetFlag, MailOperation.ClearFlag, new System.Func<MailCopy, bool>((item) => item.IsFlagged)),
|
||||
new ToggleRequestRule(MailOperation.ClearFlag, MailOperation.SetFlag, new System.Func<MailCopy, bool>((item) => !item.IsFlagged)),
|
||||
];
|
||||
|
||||
public WinoRequestProcessor(IFolderService folderService,
|
||||
|
||||
@@ -1032,7 +1032,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
return await _gmailChangeProcessor.GetMailCopiesAsync(messageIds);
|
||||
}
|
||||
|
||||
public override async Task DownloadMissingMimeMessageAsync(IMailItem mailItem,
|
||||
public override async Task DownloadMissingMimeMessageAsync(MailCopy mailItem,
|
||||
ITransferProgress transferProgress = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
||||
@@ -219,7 +219,7 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
|
||||
}, request, request);
|
||||
}
|
||||
|
||||
public override async Task DownloadMissingMimeMessageAsync(IMailItem mailItem,
|
||||
public override async Task DownloadMissingMimeMessageAsync(MailCopy mailItem,
|
||||
ITransferProgress transferProgress = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
||||
@@ -1327,7 +1327,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
return Move(batchMoveRequest);
|
||||
}
|
||||
|
||||
public override async Task DownloadMissingMimeMessageAsync(IMailItem mailItem,
|
||||
public override async Task DownloadMissingMimeMessageAsync(MailCopy mailItem,
|
||||
MailKit.ITransferProgress transferProgress = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
||||
@@ -445,7 +445,7 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
|
||||
/// <param name="mailItem">Mail item that its mime file does not exist on the disk.</param>
|
||||
/// <param name="transferProgress">Optional download progress for IMAP synchronizer.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public virtual Task DownloadMissingMimeMessageAsync(IMailItem mailItem, ITransferProgress transferProgress = null, CancellationToken cancellationToken = default) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual Task DownloadMissingMimeMessageAsync(MailCopy mailItem, ITransferProgress transferProgress = null, CancellationToken cancellationToken = default) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
|
||||
/// <summary>
|
||||
/// Performs an online search for the given query text in the given folders.
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for group headers in the flat collection
|
||||
/// </summary>
|
||||
public abstract partial class GroupHeaderBase : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
private int itemCount;
|
||||
|
||||
[ObservableProperty]
|
||||
private int unreadCount;
|
||||
|
||||
protected GroupHeaderBase(string key, string displayName)
|
||||
{
|
||||
Key = key;
|
||||
DisplayName = displayName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The unique key for this group (used for sorting and identification)
|
||||
/// </summary>
|
||||
public string Key { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The display name shown in the UI
|
||||
/// </summary>
|
||||
public string DisplayName { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Group header for date-based grouping
|
||||
/// </summary>
|
||||
public partial class DateGroupHeader : GroupHeaderBase
|
||||
{
|
||||
public DateGroupHeader(DateTime date) : base(date.ToString("yyyy-MM-dd"), FormatDisplayName(date))
|
||||
{
|
||||
Date = date;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The date this group represents
|
||||
/// </summary>
|
||||
public DateTime Date { get; }
|
||||
|
||||
private static string FormatDisplayName(DateTime date)
|
||||
{
|
||||
var today = DateTime.Today;
|
||||
var yesterday = today.AddDays(-1);
|
||||
|
||||
return date.Date switch
|
||||
{
|
||||
var d when d == today => "Today",
|
||||
var d when d == yesterday => "Yesterday",
|
||||
var d when d >= today.AddDays(-7) => date.ToString("dddd"), // This week
|
||||
var d when d.Year == today.Year => date.ToString("MMMM dd"), // This year
|
||||
_ => date.ToString("MMMM dd, yyyy") // Other years
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Group header for sender name-based grouping
|
||||
/// </summary>
|
||||
public partial class SenderGroupHeader : GroupHeaderBase
|
||||
{
|
||||
public SenderGroupHeader(string senderName) : base(senderName, senderName)
|
||||
{
|
||||
SenderName = senderName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The sender name this group represents
|
||||
/// </summary>
|
||||
public string SenderName { get; }
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,31 +0,0 @@
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Collections;
|
||||
|
||||
internal class ThreadingManager
|
||||
{
|
||||
private readonly IThreadingStrategyProvider _threadingStrategyProvider;
|
||||
|
||||
public ThreadingManager(IThreadingStrategyProvider threadingStrategyProvider)
|
||||
{
|
||||
_threadingStrategyProvider = threadingStrategyProvider;
|
||||
}
|
||||
|
||||
public bool ShouldThread(MailCopy newItem, IMailItem existingItem)
|
||||
{
|
||||
if (_threadingStrategyProvider == null) return false;
|
||||
|
||||
var strategy = _threadingStrategyProvider.GetStrategy(newItem.AssignedAccount.ProviderType);
|
||||
return strategy?.ShouldThreadWithItem(newItem, existingItem) ?? false;
|
||||
}
|
||||
|
||||
public ThreadMailItem CreateNewThread(IMailItem existingItem, MailCopy newItem)
|
||||
{
|
||||
var thread = new ThreadMailItem();
|
||||
thread.AddThreadItem(existingItem);
|
||||
thread.AddThreadItem(newItem);
|
||||
return thread;
|
||||
}
|
||||
}
|
||||
@@ -1,514 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Collections;
|
||||
using Serilog;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Comparers;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Collections;
|
||||
|
||||
public class WinoMailCollection
|
||||
{
|
||||
// We cache each mail copy id for faster access on updates.
|
||||
// If the item provider here for update or removal doesn't exist here
|
||||
// we can ignore the operation.
|
||||
|
||||
public HashSet<Guid> MailCopyIdHashSet = [];
|
||||
|
||||
public event EventHandler<IMailItem> MailItemRemoved;
|
||||
|
||||
private ListItemComparer listComparer = new ListItemComparer();
|
||||
|
||||
private readonly ObservableGroupedCollection<object, IMailItem> _mailItemSource = new ObservableGroupedCollection<object, IMailItem>();
|
||||
|
||||
public ReadOnlyObservableGroupedCollection<object, IMailItem> MailItems { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Property that defines how the item sorting should be done in the collection.
|
||||
/// </summary>
|
||||
public SortingOptionType SortingType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Threading strategy that will help thread items according to the account type.
|
||||
/// </summary>
|
||||
public IThreadingStrategyProvider ThreadingStrategyProvider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Automatically deletes single mail items after the delete operation or thread->single transition.
|
||||
/// This is useful when reply draft is discarded in the thread. Only enabled for Draft folder for now.
|
||||
/// </summary>
|
||||
public bool PruneSingleNonDraftItems { get; set; }
|
||||
|
||||
public int Count => _mailItemSource.Count;
|
||||
|
||||
public IDispatcher CoreDispatcher { get; set; }
|
||||
|
||||
private readonly ThreadingManager _threadingManager;
|
||||
|
||||
public WinoMailCollection(IThreadingStrategyProvider threadingStrategyProvider)
|
||||
{
|
||||
_threadingManager = new ThreadingManager(threadingStrategyProvider);
|
||||
MailItems = new ReadOnlyObservableGroupedCollection<object, IMailItem>(_mailItemSource);
|
||||
}
|
||||
|
||||
public void Clear() => _mailItemSource.Clear();
|
||||
|
||||
private object GetGroupingKey(IMailItem mailItem)
|
||||
{
|
||||
if (SortingType == SortingOptionType.ReceiveDate)
|
||||
return mailItem.CreationDate.ToLocalTime().Date;
|
||||
else
|
||||
return mailItem.FromName;
|
||||
}
|
||||
|
||||
private void UpdateUniqueIdHashes(IMailHashContainer itemContainer, bool isAdd)
|
||||
{
|
||||
foreach (var item in itemContainer.GetContainingIds())
|
||||
{
|
||||
if (isAdd)
|
||||
{
|
||||
MailCopyIdHashSet.Add(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
MailCopyIdHashSet.Remove(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void InsertItemInternal(object groupKey, IMailItem mailItem)
|
||||
{
|
||||
UpdateUniqueIdHashes(mailItem, true);
|
||||
|
||||
if (mailItem is MailCopy mailCopy)
|
||||
{
|
||||
_mailItemSource.InsertItem(groupKey, listComparer, new MailItemViewModel(mailCopy), listComparer.GetItemComparer());
|
||||
}
|
||||
else if (mailItem is ThreadMailItem threadMailItem)
|
||||
{
|
||||
_mailItemSource.InsertItem(groupKey, listComparer, new ThreadMailItemViewModel(threadMailItem), listComparer.GetItemComparer());
|
||||
}
|
||||
else
|
||||
{
|
||||
_mailItemSource.InsertItem(groupKey, listComparer, mailItem, listComparer.GetItemComparer());
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveItemInternal(ObservableGroup<object, IMailItem> group, IMailItem mailItem)
|
||||
{
|
||||
UpdateUniqueIdHashes(mailItem, false);
|
||||
|
||||
MailItemRemoved?.Invoke(this, mailItem);
|
||||
|
||||
group.Remove(mailItem);
|
||||
|
||||
if (group.Count == 0)
|
||||
{
|
||||
_mailItemSource.RemoveGroup(group.Key);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleThreadingAsync(ObservableGroup<object, IMailItem> group, IMailItem item, MailCopy addedItem)
|
||||
{
|
||||
if (item is ThreadMailItemViewModel threadViewModel)
|
||||
{
|
||||
await HandleExistingThreadAsync(group, threadViewModel, addedItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
await HandleNewThreadAsync(group, item, addedItem);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleExistingThreadAsync(ObservableGroup<object, IMailItem> group, ThreadMailItemViewModel threadViewModel, MailCopy addedItem)
|
||||
{
|
||||
var existingGroupKey = GetGroupingKey(threadViewModel);
|
||||
|
||||
await ExecuteUIThread(() => { threadViewModel.AddMailItemViewModel(addedItem); });
|
||||
|
||||
var newGroupKey = GetGroupingKey(threadViewModel);
|
||||
|
||||
if (!existingGroupKey.Equals(newGroupKey))
|
||||
{
|
||||
await MoveThreadToNewGroupAsync(group, threadViewModel, newGroupKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ExecuteUIThread(() => { threadViewModel.NotifyPropertyChanges(); });
|
||||
}
|
||||
|
||||
UpdateUniqueIdHashes(addedItem, true);
|
||||
}
|
||||
|
||||
private async Task HandleNewThreadAsync(ObservableGroup<object, IMailItem> group, IMailItem item, MailCopy addedItem)
|
||||
{
|
||||
if (item.Id == addedItem.Id)
|
||||
{
|
||||
await UpdateExistingItemAsync(item, addedItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
await CreateNewThreadAsync(group, item, addedItem);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task MoveThreadToNewGroupAsync(ObservableGroup<object, IMailItem> currentGroup, ThreadMailItemViewModel threadViewModel, object newGroupKey)
|
||||
{
|
||||
var mailThreadItems = threadViewModel.GetThreadMailItem();
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
RemoveItemInternal(currentGroup, threadViewModel);
|
||||
InsertItemInternal(newGroupKey, new ThreadMailItemViewModel(mailThreadItems));
|
||||
});
|
||||
}
|
||||
|
||||
private async Task CreateNewThreadAsync(ObservableGroup<object, IMailItem> group, IMailItem item, MailCopy addedItem)
|
||||
{
|
||||
var threadMailItem = _threadingManager.CreateNewThread(item, addedItem);
|
||||
var newGroupKey = GetGroupingKey(threadMailItem);
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
RemoveItemInternal(group, item);
|
||||
InsertItemInternal(newGroupKey, threadMailItem);
|
||||
});
|
||||
}
|
||||
|
||||
public async Task AddAsync(MailCopy addedItem)
|
||||
{
|
||||
foreach (var group in _mailItemSource)
|
||||
{
|
||||
foreach (var item in group)
|
||||
{
|
||||
if (_threadingManager.ShouldThread(addedItem, item))
|
||||
{
|
||||
await HandleThreadingAsync(group, item, addedItem);
|
||||
return;
|
||||
}
|
||||
else if (item.Id == addedItem.Id && item is MailItemViewModel itemViewModel)
|
||||
{
|
||||
await UpdateExistingItemAsync(itemViewModel, addedItem);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await AddNewItemAsync(addedItem);
|
||||
}
|
||||
|
||||
private async Task AddNewItemAsync(MailCopy addedItem)
|
||||
{
|
||||
var groupKey = GetGroupingKey(addedItem);
|
||||
await ExecuteUIThread(() => { InsertItemInternal(groupKey, addedItem); });
|
||||
}
|
||||
|
||||
private async Task UpdateExistingItemAsync(IMailItem existingItem, MailCopy updatedItem)
|
||||
{
|
||||
if (existingItem is MailItemViewModel itemViewModel)
|
||||
{
|
||||
UpdateUniqueIdHashes(itemViewModel, false);
|
||||
UpdateUniqueIdHashes(updatedItem, true);
|
||||
|
||||
await ExecuteUIThread(() => { itemViewModel.MailCopy = updatedItem; });
|
||||
}
|
||||
}
|
||||
|
||||
public void AddRange(IEnumerable<IMailItem> items, bool clearIdCache)
|
||||
{
|
||||
if (clearIdCache)
|
||||
{
|
||||
MailCopyIdHashSet.Clear();
|
||||
}
|
||||
|
||||
var groupedByName = items
|
||||
.GroupBy(a => GetGroupingKey(a))
|
||||
.Select(a => new ObservableGroup<object, IMailItem>(a.Key, a));
|
||||
|
||||
foreach (var group in groupedByName)
|
||||
{
|
||||
// Store all mail copy ids for faster access.
|
||||
foreach (var item in group)
|
||||
{
|
||||
if (item is MailItemViewModel mailCopyItem && !MailCopyIdHashSet.Contains(item.UniqueId))
|
||||
{
|
||||
MailCopyIdHashSet.Add(item.UniqueId);
|
||||
}
|
||||
else if (item is ThreadMailItemViewModel threadMailItem)
|
||||
{
|
||||
foreach (var mailItem in threadMailItem.ThreadItems)
|
||||
{
|
||||
if (!MailCopyIdHashSet.Contains(mailItem.UniqueId))
|
||||
{
|
||||
MailCopyIdHashSet.Add(mailItem.UniqueId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var existingGroup = _mailItemSource.FirstGroupByKeyOrDefault(group.Key);
|
||||
|
||||
if (existingGroup == null)
|
||||
{
|
||||
_mailItemSource.AddGroup(group.Key, group);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var item in group)
|
||||
{
|
||||
existingGroup.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public MailItemContainer GetMailItemContainer(Guid uniqueMailId)
|
||||
{
|
||||
var groupCount = _mailItemSource.Count;
|
||||
|
||||
for (int i = 0; i < groupCount; i++)
|
||||
{
|
||||
var group = _mailItemSource[i];
|
||||
|
||||
for (int k = 0; k < group.Count; k++)
|
||||
{
|
||||
var item = group[k];
|
||||
|
||||
if (item is MailItemViewModel singleMailItemViewModel && singleMailItemViewModel.UniqueId == uniqueMailId)
|
||||
return new MailItemContainer(singleMailItemViewModel);
|
||||
else if (item is ThreadMailItemViewModel threadMailItemViewModel && threadMailItemViewModel.HasUniqueId(uniqueMailId))
|
||||
{
|
||||
var singleItemViewModel = threadMailItemViewModel.GetItemById(uniqueMailId) as MailItemViewModel;
|
||||
|
||||
return new MailItemContainer(singleItemViewModel, threadMailItemViewModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void UpdateThumbnails(string address)
|
||||
{
|
||||
if (CoreDispatcher == null) return;
|
||||
|
||||
CoreDispatcher.ExecuteOnUIThread(() =>
|
||||
{
|
||||
foreach (var group in _mailItemSource)
|
||||
{
|
||||
foreach (var item in group)
|
||||
{
|
||||
if (item is MailItemViewModel mailItemViewModel &&
|
||||
!string.IsNullOrEmpty(mailItemViewModel.MailCopy?.FromAddress) &&
|
||||
mailItemViewModel.MailCopy.FromAddress.Equals(address, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
mailItemViewModel.ThumbnailUpdatedEvent = !mailItemViewModel.ThumbnailUpdatedEvent;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fins the item container that updated mail copy belongs to and updates it.
|
||||
/// </summary>
|
||||
/// <param name="updatedMailCopy">Updated mail copy.</param>
|
||||
/// <returns></returns>
|
||||
public async Task UpdateMailCopy(MailCopy updatedMailCopy)
|
||||
{
|
||||
// This item doesn't exist in the list.
|
||||
if (!MailCopyIdHashSet.Contains(updatedMailCopy.UniqueId))
|
||||
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
var itemContainer = GetMailItemContainer(updatedMailCopy.UniqueId);
|
||||
|
||||
if (itemContainer == null) return;
|
||||
|
||||
if (itemContainer.ItemViewModel != null)
|
||||
{
|
||||
UpdateUniqueIdHashes(itemContainer.ItemViewModel, false);
|
||||
}
|
||||
|
||||
if (itemContainer.ItemViewModel != null)
|
||||
{
|
||||
itemContainer.ItemViewModel.MailCopy = updatedMailCopy;
|
||||
}
|
||||
|
||||
UpdateUniqueIdHashes(updatedMailCopy, true);
|
||||
|
||||
// Call thread notifications if possible.
|
||||
itemContainer.ThreadViewModel?.NotifyPropertyChanges();
|
||||
});
|
||||
}
|
||||
|
||||
public MailItemViewModel GetNextItem(MailCopy mailCopy)
|
||||
{
|
||||
try
|
||||
{
|
||||
var groupCount = _mailItemSource.Count;
|
||||
|
||||
for (int i = 0; i < groupCount; i++)
|
||||
{
|
||||
var group = _mailItemSource[i];
|
||||
|
||||
for (int k = 0; k < group.Count; k++)
|
||||
{
|
||||
var item = group[k];
|
||||
|
||||
if (item is MailItemViewModel singleMailItemViewModel && singleMailItemViewModel.UniqueId == mailCopy.UniqueId)
|
||||
{
|
||||
if (k + 1 < group.Count)
|
||||
{
|
||||
return group[k + 1] as MailItemViewModel;
|
||||
}
|
||||
else if (i + 1 < groupCount)
|
||||
{
|
||||
return _mailItemSource[i + 1][0] as MailItemViewModel;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else if (item is ThreadMailItemViewModel threadMailItemViewModel && threadMailItemViewModel.HasUniqueId(mailCopy.UniqueId))
|
||||
{
|
||||
var singleItemViewModel = threadMailItemViewModel.GetItemById(mailCopy.UniqueId) as MailItemViewModel;
|
||||
|
||||
if (singleItemViewModel == null) return null;
|
||||
|
||||
var singleItemIndex = threadMailItemViewModel.ThreadItems.IndexOf(singleItemViewModel);
|
||||
|
||||
if (singleItemIndex + 1 < threadMailItemViewModel.ThreadItems.Count)
|
||||
{
|
||||
return threadMailItemViewModel.ThreadItems[singleItemIndex + 1] as MailItemViewModel;
|
||||
}
|
||||
else if (i + 1 < groupCount)
|
||||
{
|
||||
return _mailItemSource[i + 1][0] as MailItemViewModel;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Failed to find the next item to select.");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task RemoveAsync(MailCopy removeItem)
|
||||
{
|
||||
// This item doesn't exist in the list.
|
||||
if (!MailCopyIdHashSet.Contains(removeItem.UniqueId)) return;
|
||||
|
||||
// Check all items for whether this item should be threaded with them.
|
||||
bool shouldExit = false;
|
||||
|
||||
var groupCount = _mailItemSource.Count;
|
||||
|
||||
for (int i = 0; i < groupCount; i++)
|
||||
{
|
||||
if (shouldExit) break;
|
||||
|
||||
var group = _mailItemSource[i];
|
||||
|
||||
for (int k = 0; k < group.Count; k++)
|
||||
{
|
||||
var item = group[k];
|
||||
|
||||
if (item is ThreadMailItemViewModel threadMailItemViewModel && threadMailItemViewModel.HasUniqueId(removeItem.UniqueId))
|
||||
{
|
||||
var removalItem = threadMailItemViewModel.GetItemById(removeItem.UniqueId);
|
||||
|
||||
if (removalItem == null) return;
|
||||
|
||||
// Threads' Id is equal to the last item they hold.
|
||||
// We can't do Id check here because that'd remove the whole thread.
|
||||
|
||||
/* Remove item from the thread.
|
||||
* If thread had 1 item inside:
|
||||
* -> Remove the thread and insert item as single item.
|
||||
* If thread had 0 item inside:
|
||||
* -> Remove the thread.
|
||||
*/
|
||||
|
||||
var oldGroupKey = GetGroupingKey(threadMailItemViewModel);
|
||||
|
||||
await ExecuteUIThread(() => { threadMailItemViewModel.RemoveCopyItem(removalItem); });
|
||||
|
||||
if (threadMailItemViewModel.ThreadItems.Count == 1)
|
||||
{
|
||||
// Convert to single item.
|
||||
|
||||
var singleViewModel = threadMailItemViewModel.GetSingleItemViewModel();
|
||||
var groupKey = GetGroupingKey(singleViewModel);
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
RemoveItemInternal(group, threadMailItemViewModel);
|
||||
InsertItemInternal(groupKey, singleViewModel);
|
||||
});
|
||||
|
||||
// If thread->single conversion is being done, we should ignore it for non-draft items.
|
||||
// eg. Deleting a reply message from draft folder. Single non-draft item should not be re-added.
|
||||
|
||||
if (PruneSingleNonDraftItems && !singleViewModel.IsDraft)
|
||||
{
|
||||
// This item should not be here anymore.
|
||||
// It's basically a reply mail in Draft folder.
|
||||
var newGroup = _mailItemSource.FirstGroupByKeyOrDefault(groupKey);
|
||||
|
||||
if (newGroup != null)
|
||||
{
|
||||
await ExecuteUIThread(() => { RemoveItemInternal(newGroup, singleViewModel); });
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (threadMailItemViewModel.ThreadItems.Count == 0)
|
||||
{
|
||||
await ExecuteUIThread(() => { RemoveItemInternal(group, threadMailItemViewModel); });
|
||||
}
|
||||
else
|
||||
{
|
||||
// Item inside the thread is removed.
|
||||
await ExecuteUIThread(() => { threadMailItemViewModel.ThreadItems.Remove(removalItem); });
|
||||
|
||||
UpdateUniqueIdHashes(removalItem, false);
|
||||
}
|
||||
|
||||
shouldExit = true;
|
||||
break;
|
||||
}
|
||||
else if (item.UniqueId == removeItem.UniqueId)
|
||||
{
|
||||
await ExecuteUIThread(() => { RemoveItemInternal(group, item); });
|
||||
|
||||
shouldExit = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExecuteUIThread(Action action) => await CoreDispatcher?.ExecuteOnUIThread(action);
|
||||
}
|
||||
@@ -211,7 +211,7 @@ public partial class ComposePageViewModel : MailBaseViewModel
|
||||
|
||||
isUpdatingMimeBlocked = true;
|
||||
|
||||
var assignedAccount = CurrentMailDraftItem.AssignedAccount;
|
||||
var assignedAccount = CurrentMailDraftItem.MailCopy.AssignedAccount;
|
||||
var sentFolder = await _folderService.GetSpecialFolderByAccountIdAsync(assignedAccount.Id, SpecialFolderType.Sent);
|
||||
|
||||
using MemoryStream memoryStream = new();
|
||||
@@ -223,8 +223,8 @@ public partial class ComposePageViewModel : MailBaseViewModel
|
||||
var draftSendPreparationRequest = new SendDraftPreparationRequest(CurrentMailDraftItem.MailCopy,
|
||||
SelectedAlias,
|
||||
sentFolder,
|
||||
CurrentMailDraftItem.AssignedFolder,
|
||||
CurrentMailDraftItem.AssignedAccount.Preferences,
|
||||
CurrentMailDraftItem.MailCopy.AssignedFolder,
|
||||
CurrentMailDraftItem.MailCopy.AssignedAccount.Preferences,
|
||||
base64EncodedMessage);
|
||||
|
||||
await _worker.ExecuteAsync(draftSendPreparationRequest);
|
||||
@@ -355,7 +355,7 @@ public partial class ComposePageViewModel : MailBaseViewModel
|
||||
|
||||
if (ComposingAccount != null) return true;
|
||||
|
||||
var composingAccount = await _accountService.GetAccountAsync(CurrentMailDraftItem.AssignedAccount.Id).ConfigureAwait(false);
|
||||
var composingAccount = await _accountService.GetAccountAsync(CurrentMailDraftItem.MailCopy.AssignedAccount.Id).ConfigureAwait(false);
|
||||
if (composingAccount == null) return false;
|
||||
|
||||
var aliases = await _accountService.GetAccountAliasesAsync(composingAccount.Id).ConfigureAwait(false);
|
||||
@@ -556,7 +556,7 @@ public partial class ComposePageViewModel : MailBaseViewModel
|
||||
|
||||
if (CurrentMailDraftItem == null) return;
|
||||
|
||||
if (updatedMail.UniqueId == CurrentMailDraftItem.UniqueId)
|
||||
if (updatedMail.UniqueId == CurrentMailDraftItem.MailCopy.UniqueId)
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
|
||||
@@ -1,35 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Single view model for IMailItem representation.
|
||||
/// </summary>
|
||||
public partial class MailItemViewModel(MailCopy mailCopy) : ObservableObject, IMailItem
|
||||
public partial class MailItemViewModel(MailCopy mailCopy) : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
public partial MailCopy MailCopy { get; set; } = mailCopy;
|
||||
|
||||
public Guid UniqueId => ((IMailItem)MailCopy).UniqueId;
|
||||
public string ThreadId => ((IMailItem)MailCopy).ThreadId;
|
||||
public string MessageId => ((IMailItem)MailCopy).MessageId;
|
||||
public DateTime CreationDate => ((IMailItem)MailCopy).CreationDate;
|
||||
public string References => ((IMailItem)MailCopy).References;
|
||||
public string InReplyTo => ((IMailItem)MailCopy).InReplyTo;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool ThumbnailUpdatedEvent { get; set; } = false;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsCustomFocused { get; set; }
|
||||
public partial bool IsSelected { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsSelected { get; set; }
|
||||
public partial bool IsDisplayedInThread { get; set; }
|
||||
|
||||
public bool IsFlagged
|
||||
{
|
||||
@@ -96,14 +85,4 @@ public partial class MailItemViewModel(MailCopy mailCopy) : ObservableObject, IM
|
||||
get => MailCopy.HasAttachments;
|
||||
set => SetProperty(MailCopy.HasAttachments, value, MailCopy, (u, n) => u.HasAttachments = n);
|
||||
}
|
||||
|
||||
public MailItemFolder AssignedFolder => ((IMailItem)MailCopy).AssignedFolder;
|
||||
|
||||
public MailAccount AssignedAccount => ((IMailItem)MailCopy).AssignedAccount;
|
||||
|
||||
public Guid FileId => ((IMailItem)MailCopy).FileId;
|
||||
|
||||
public AccountContact SenderContact => ((IMailItem)MailCopy).SenderContact;
|
||||
|
||||
public IEnumerable<Guid> GetContainingIds() => new[] { UniqueId };
|
||||
}
|
||||
|
||||
@@ -1,126 +1,114 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Thread mail item (multiple IMailItem) view model representation.
|
||||
/// </summary>
|
||||
public partial class ThreadMailItemViewModel : ObservableObject, IMailItemThread, IComparable<string>, IComparable<DateTime>
|
||||
public partial class ThreadMailItemViewModel : ObservableRecipient, IDisposable
|
||||
{
|
||||
public ObservableCollection<IMailItem> ThreadItems => (MailItem as IMailItemThread)?.ThreadItems ?? [];
|
||||
public AccountContact SenderContact => ((IMailItemThread)MailItem).SenderContact;
|
||||
private readonly string _threadId;
|
||||
|
||||
private readonly List<MailItemViewModel> _threadEmails = [];
|
||||
private bool _disposed;
|
||||
|
||||
[ObservableProperty]
|
||||
private ThreadMailItem mailItem;
|
||||
[NotifyPropertyChangedRecipients]
|
||||
public partial bool IsThreadExpanded { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isThreadExpanded;
|
||||
public partial bool IsSelected { get; set; }
|
||||
|
||||
public ThreadMailItemViewModel(ThreadMailItem threadMailItem)
|
||||
/// <summary>
|
||||
/// Gets the number of emails in this thread
|
||||
/// </summary>
|
||||
public int EmailCount => _threadEmails.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the latest email's subject for display
|
||||
/// </summary>
|
||||
public string? Subject => _threadEmails
|
||||
.OrderByDescending(e => e.MailCopy?.CreationDate)
|
||||
.FirstOrDefault()?.MailCopy?.Subject;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the latest email's sender name for display
|
||||
/// </summary>
|
||||
public string? FromName => _threadEmails
|
||||
.OrderByDescending(e => e.MailCopy?.CreationDate)
|
||||
.FirstOrDefault()?.MailCopy?.SenderContact.Name;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the latest email's creation date for sorting
|
||||
/// </summary>
|
||||
public DateTime LatestEmailDate => _threadEmails
|
||||
.OrderByDescending(e => e.MailCopy?.CreationDate)
|
||||
.FirstOrDefault()?.MailCopy?.CreationDate ?? DateTime.MinValue;
|
||||
|
||||
/// <summary>
|
||||
/// Gets all emails in this thread (read-only)
|
||||
/// </summary>
|
||||
public IReadOnlyList<MailItemViewModel> ThreadEmails => _threadEmails.AsReadOnly();
|
||||
|
||||
public ThreadMailItemViewModel(string threadId)
|
||||
{
|
||||
MailItem = new ThreadMailItem();
|
||||
_threadId = threadId;
|
||||
}
|
||||
|
||||
// Local copies
|
||||
foreach (var item in threadMailItem.ThreadItems)
|
||||
partial void OnIsSelectedChanged(bool value)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
AddMailItemViewModel(item);
|
||||
_threadEmails.Clear();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
public ThreadMailItem GetThreadMailItem() => MailItem;
|
||||
|
||||
public IEnumerable<MailCopy> GetMailCopies()
|
||||
=> ThreadItems.OfType<MailItemViewModel>().Select(a => a.MailCopy);
|
||||
|
||||
public void AddMailItemViewModel(IMailItem mailItem)
|
||||
public void Dispose()
|
||||
{
|
||||
if (mailItem == null) return;
|
||||
|
||||
if (mailItem is MailCopy mailCopy)
|
||||
MailItem.AddThreadItem(new MailItemViewModel(mailCopy));
|
||||
else if (mailItem is MailItemViewModel mailItemViewModel)
|
||||
MailItem.AddThreadItem(mailItemViewModel);
|
||||
else
|
||||
Debugger.Break();
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public bool HasUniqueId(Guid uniqueMailId)
|
||||
=> ThreadItems.Any(a => a.UniqueId == uniqueMailId);
|
||||
|
||||
public IMailItem GetItemById(Guid uniqueMailId)
|
||||
=> ThreadItems.FirstOrDefault(a => a.UniqueId == uniqueMailId);
|
||||
|
||||
public void RemoveCopyItem(IMailItem item)
|
||||
private void NotifyPropertyChanges()
|
||||
{
|
||||
MailCopy copyToRemove = null;
|
||||
OnPropertyChanged(nameof(Subject));
|
||||
OnPropertyChanged(nameof(FromName));
|
||||
}
|
||||
|
||||
if (item is MailItemViewModel mailItemViewModel)
|
||||
copyToRemove = mailItemViewModel.MailCopy;
|
||||
else if (item is MailCopy copyItem)
|
||||
copyToRemove = copyItem;
|
||||
|
||||
var existedItem = ThreadItems.FirstOrDefault(a => a.Id == copyToRemove.Id);
|
||||
|
||||
if (existedItem == null) return;
|
||||
|
||||
ThreadItems.Remove(existedItem);
|
||||
/// <summary>
|
||||
/// Adds an email to this thread
|
||||
/// </summary>
|
||||
public void AddEmail(MailItemViewModel email)
|
||||
{
|
||||
if (email.MailCopy.ThreadId != _threadId)
|
||||
throw new ArgumentException($"Email ThreadId '{email.MailCopy.ThreadId}' does not match expander ThreadId '{_threadId}'");
|
||||
|
||||
_threadEmails.Add(email);
|
||||
NotifyPropertyChanges();
|
||||
}
|
||||
|
||||
public void NotifyPropertyChanges()
|
||||
/// <summary>
|
||||
/// Removes an email from this thread
|
||||
/// </summary>
|
||||
public void RemoveEmail(MailItemViewModel email)
|
||||
{
|
||||
// TODO
|
||||
// Stupid temporary fix for not updating UI.
|
||||
// This view model must be reworked with ThreadMailItem together.
|
||||
|
||||
var current = MailItem;
|
||||
|
||||
MailItem = null;
|
||||
MailItem = current;
|
||||
if (_threadEmails.Remove(email))
|
||||
{
|
||||
NotifyPropertyChanges();
|
||||
}
|
||||
}
|
||||
|
||||
public IMailItem LatestMailItem => ((IMailItemThread)MailItem).LatestMailItem;
|
||||
public IMailItem FirstMailItem => ((IMailItemThread)MailItem).FirstMailItem;
|
||||
|
||||
public string Id => ((IMailItem)MailItem).Id;
|
||||
public string Subject => ((IMailItem)MailItem).Subject;
|
||||
public string ThreadId => ((IMailItem)MailItem).ThreadId;
|
||||
public string MessageId => ((IMailItem)MailItem).MessageId;
|
||||
public string References => ((IMailItem)MailItem).References;
|
||||
public string PreviewText => ((IMailItem)MailItem).PreviewText;
|
||||
public string FromName => ((IMailItem)MailItem).FromName;
|
||||
public DateTime CreationDate => ((IMailItem)MailItem).CreationDate;
|
||||
public string FromAddress => ((IMailItem)MailItem).FromAddress;
|
||||
public bool HasAttachments => ((IMailItem)MailItem).HasAttachments;
|
||||
public bool IsFlagged => ((IMailItem)MailItem).IsFlagged;
|
||||
public bool IsFocused => ((IMailItem)MailItem).IsFocused;
|
||||
public bool IsRead => ((IMailItem)MailItem).IsRead;
|
||||
public bool IsDraft => ((IMailItem)MailItem).IsDraft;
|
||||
public string DraftId => string.Empty;
|
||||
public string InReplyTo => ((IMailItem)MailItem).InReplyTo;
|
||||
|
||||
public MailItemFolder AssignedFolder => ((IMailItem)MailItem).AssignedFolder;
|
||||
|
||||
public MailAccount AssignedAccount => ((IMailItem)MailItem).AssignedAccount;
|
||||
|
||||
public Guid UniqueId => ((IMailItem)MailItem).UniqueId;
|
||||
|
||||
public Guid FileId => ((IMailItem)MailItem).FileId;
|
||||
|
||||
public int CompareTo(DateTime other) => CreationDate.CompareTo(other);
|
||||
public int CompareTo(string other) => FromName.CompareTo(other);
|
||||
|
||||
// Get single mail item view model out of the only item in thread items.
|
||||
public MailItemViewModel GetSingleItemViewModel() => ThreadItems.First() as MailItemViewModel;
|
||||
|
||||
public IEnumerable<Guid> GetContainingIds() => ((IMailItemThread)MailItem).GetContainingIds();
|
||||
}
|
||||
|
||||
@@ -59,8 +59,15 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
|
||||
private IObservable<System.Reactive.EventPattern<NotifyCollectionChangedEventArgs>> selectionChangedObservable = null;
|
||||
|
||||
public WinoMailCollection MailCollection { get; }
|
||||
public GroupedEmailCollection MailCollection { get; set; } = new GroupedEmailCollection();
|
||||
|
||||
//public IEnumerable<MailItemViewModel> SelectedItems
|
||||
//{
|
||||
// get
|
||||
// {
|
||||
|
||||
// }
|
||||
//}
|
||||
public ObservableCollection<MailItemViewModel> SelectedItems { get; set; } = [];
|
||||
public ObservableCollection<FolderPivotViewModel> PivotFolders { get; set; } = [];
|
||||
public ObservableCollection<MailOperationMenuItem> ActionItems { get; set; } = [];
|
||||
@@ -77,7 +84,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
private readonly IMailDialogService _mailDialogService;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly IFolderService _folderService;
|
||||
private readonly IThreadingStrategyProvider _threadingStrategyProvider;
|
||||
private readonly IContextMenuItemService _contextMenuItemService;
|
||||
private readonly IWinoRequestDelegator _winoRequestDelegator;
|
||||
private readonly IKeyPressService _keyPressService;
|
||||
@@ -154,7 +160,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
IMailService mailService,
|
||||
IStatePersistanceService statePersistenceService,
|
||||
IFolderService folderService,
|
||||
IThreadingStrategyProvider threadingStrategyProvider,
|
||||
IContextMenuItemService contextMenuItemService,
|
||||
IWinoRequestDelegator winoRequestDelegator,
|
||||
IKeyPressService keyPressService,
|
||||
@@ -162,7 +167,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
INewThemeService themeService,
|
||||
IWinoLogger winoLogger)
|
||||
{
|
||||
MailCollection = new WinoMailCollection(threadingStrategyProvider);
|
||||
PreferencesService = preferencesService;
|
||||
ThemeService = themeService;
|
||||
_winoLogger = winoLogger;
|
||||
@@ -172,7 +176,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
_mailDialogService = mailDialogService;
|
||||
_mailService = mailService;
|
||||
_folderService = folderService;
|
||||
_threadingStrategyProvider = threadingStrategyProvider;
|
||||
_contextMenuItemService = contextMenuItemService;
|
||||
_winoRequestDelegator = winoRequestDelegator;
|
||||
_keyPressService = keyPressService;
|
||||
@@ -190,23 +193,23 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
await ExecuteUIThread(() => { SelectedItemCollectionUpdated(a.EventArgs); });
|
||||
});
|
||||
|
||||
MailCollection.MailItemRemoved += (c, removedItem) =>
|
||||
{
|
||||
if (removedItem is ThreadMailItemViewModel removedThreadViewModelItem)
|
||||
{
|
||||
foreach (var viewModel in removedThreadViewModelItem.ThreadItems.Cast<MailItemViewModel>())
|
||||
{
|
||||
if (SelectedItems.Contains(viewModel))
|
||||
{
|
||||
SelectedItems.Remove(viewModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (removedItem is MailItemViewModel removedMailItemViewModel && SelectedItems.Contains(removedMailItemViewModel))
|
||||
{
|
||||
SelectedItems.Remove(removedMailItemViewModel);
|
||||
}
|
||||
};
|
||||
//MailCollection.MailItemRemoved += (c, removedItem) =>
|
||||
//{
|
||||
// if (removedItem is ThreadMailItemViewModel removedThreadViewModelItem)
|
||||
// {
|
||||
// foreach (var viewModel in removedThreadViewModelItem.ThreadItems.Cast<MailItemViewModel>())
|
||||
// {
|
||||
// if (SelectedItems.Contains(viewModel))
|
||||
// {
|
||||
// SelectedItems.Remove(viewModel);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// else if (removedItem is MailItemViewModel removedMailItemViewModel && SelectedItems.Contains(removedMailItemViewModel))
|
||||
// {
|
||||
// SelectedItems.Remove(removedMailItemViewModel);
|
||||
// }
|
||||
//};
|
||||
}
|
||||
|
||||
private void SetupTopBarActions()
|
||||
@@ -243,10 +246,11 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
{
|
||||
if (SetProperty(ref _selectedSortingOption, value))
|
||||
{
|
||||
if (value != null && MailCollection != null)
|
||||
{
|
||||
MailCollection.SortingType = value.Type;
|
||||
}
|
||||
// TODO: Update sorting in mail collection.
|
||||
//if (value != null && MailCollection != null)
|
||||
//{
|
||||
// MailCollection.SortingType = value.Type;
|
||||
//}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -318,16 +322,16 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
|
||||
if (markAsPreference == MailMarkAsOption.WhenSelected)
|
||||
{
|
||||
var operation = MailOperation.MarkAsRead;
|
||||
var package = new MailOperationPreperationRequest(operation, _activeMailItem.MailCopy);
|
||||
//var operation = MailOperation.MarkAsRead;
|
||||
//var package = new MailOperationPreperationRequest(operation, _activeMailItem.MailCopy);
|
||||
|
||||
if (ActiveFolder?.SpecialFolderType == SpecialFolderType.Unread &&
|
||||
!gmailUnreadFolderMarkedAsReadUniqueIds.Contains(_activeMailItem.UniqueId))
|
||||
{
|
||||
gmailUnreadFolderMarkedAsReadUniqueIds.Add(_activeMailItem.UniqueId);
|
||||
}
|
||||
//if (ActiveFolder?.SpecialFolderType == SpecialFolderType.Unread &&
|
||||
// !gmailUnreadFolderMarkedAsReadUniqueIds.Contains(_activeMailItem.UniqueId))
|
||||
//{
|
||||
// gmailUnreadFolderMarkedAsReadUniqueIds.Add(_activeMailItem.UniqueId);
|
||||
//}
|
||||
|
||||
await ExecuteMailOperationAsync(package);
|
||||
//await ExecuteMailOperationAsync(package);
|
||||
}
|
||||
else if (markAsPreference == MailMarkAsOption.AfterDelay && PreferencesService.MarkAsDelay >= 0)
|
||||
{
|
||||
@@ -353,13 +357,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
OnPropertyChanged(nameof(IsFolderEmpty));
|
||||
}
|
||||
|
||||
protected override void OnDispatcherAssigned()
|
||||
{
|
||||
base.OnDispatcherAssigned();
|
||||
|
||||
MailCollection.CoreDispatcher = Dispatcher;
|
||||
}
|
||||
|
||||
private async void UpdateBarMessage(InfoBarMessageType severity, string title, string message)
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
@@ -565,67 +562,35 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
[RelayCommand]
|
||||
private async Task LoadMoreItemsAsync()
|
||||
{
|
||||
if (IsInitializingFolder || IsOnlineSearchEnabled) return;
|
||||
//if (IsInitializingFolder || IsOnlineSearchEnabled) return;
|
||||
|
||||
await ExecuteUIThread(() => { IsInitializingFolder = true; });
|
||||
//await ExecuteUIThread(() => { IsInitializingFolder = true; });
|
||||
|
||||
var initializationOptions = new MailListInitializationOptions(ActiveFolder.HandlingFolders,
|
||||
SelectedFilterOption.Type,
|
||||
SelectedSortingOption.Type,
|
||||
PreferencesService.IsThreadingEnabled,
|
||||
SelectedFolderPivot.IsFocused,
|
||||
IsInSearchMode ? SearchQuery : string.Empty,
|
||||
MailCollection.MailCopyIdHashSet);
|
||||
//var initializationOptions = new MailListInitializationOptions(ActiveFolder.HandlingFolders,
|
||||
// SelectedFilterOption.Type,
|
||||
// SelectedSortingOption.Type,
|
||||
// PreferencesService.IsThreadingEnabled,
|
||||
// SelectedFolderPivot.IsFocused,
|
||||
// IsInSearchMode ? SearchQuery : string.Empty,
|
||||
// MailCollection.MailCopyIdHashSet);
|
||||
|
||||
var items = await _mailService.FetchMailsAsync(initializationOptions).ConfigureAwait(false);
|
||||
//var items = await _mailService.FetchMailsAsync(initializationOptions).ConfigureAwait(false);
|
||||
|
||||
var viewModels = PrepareMailViewModels(items);
|
||||
//var viewModels = PrepareMailViewModels(items);
|
||||
|
||||
await ExecuteUIThread(() => { MailCollection.AddRange(viewModels, clearIdCache: false); });
|
||||
await ExecuteUIThread(() => { IsInitializingFolder = false; });
|
||||
//await ExecuteUIThread(() => { MailCollection.AddRange(viewModels, clearIdCache: false); });
|
||||
//await ExecuteUIThread(() => { IsInitializingFolder = false; });
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public Task ExecuteMailOperationAsync(MailOperationPreperationRequest package) => _winoRequestDelegator.ExecuteAsync(package);
|
||||
|
||||
public IEnumerable<MailItemViewModel> GetTargetMailItemViewModels(IMailItem clickedItem)
|
||||
{
|
||||
// Threat threads as a whole and include everything in the group. Except single selections outside of the thread.
|
||||
IEnumerable<MailItemViewModel> contextMailItems = null;
|
||||
public IEnumerable<MailOperationMenuItem> GetAvailableMailActions(IEnumerable<MailItemViewModel> contextMailItems)
|
||||
=> _contextMenuItemService.GetMailItemContextMenuActions(contextMailItems.Select(a => a.MailCopy));
|
||||
|
||||
if (clickedItem is ThreadMailItemViewModel clickedThreadItem)
|
||||
{
|
||||
// Clicked item is a thread.
|
||||
|
||||
clickedThreadItem.IsThreadExpanded = true;
|
||||
contextMailItems = clickedThreadItem.ThreadItems.Cast<MailItemViewModel>();
|
||||
|
||||
// contextMailItems = clickedThreadItem.GetMailCopies();
|
||||
}
|
||||
else if (clickedItem is MailItemViewModel clickedMailItemViewModel)
|
||||
{
|
||||
// If the clicked item is included in SelectedItems, then we need to thing them as whole.
|
||||
// If there are selected items, but clicked item is not one of them, then it's a single context menu.
|
||||
|
||||
bool includedInSelectedItems = SelectedItems.Contains(clickedItem);
|
||||
|
||||
if (includedInSelectedItems)
|
||||
contextMailItems = SelectedItems;
|
||||
else
|
||||
contextMailItems = [clickedMailItemViewModel];
|
||||
}
|
||||
|
||||
return contextMailItems;
|
||||
}
|
||||
|
||||
public IEnumerable<MailOperationMenuItem> GetAvailableMailActions(IEnumerable<IMailItem> contextMailItems)
|
||||
=> _contextMenuItemService.GetMailItemContextMenuActions(contextMailItems);
|
||||
|
||||
public void ChangeCustomFocusedState(IEnumerable<IMailItem> mailItems, bool isFocused)
|
||||
=> mailItems.OfType<MailItemViewModel>().ForEach(a => a.IsCustomFocused = isFocused);
|
||||
|
||||
private bool ShouldPreventItemAdd(IMailItem mailItem)
|
||||
private bool ShouldPreventItemAdd(MailCopy mailItem)
|
||||
{
|
||||
bool condition = mailItem.IsRead
|
||||
&& SelectedFilterOption.Type == FilterOptionType.Unread
|
||||
@@ -660,7 +625,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
|
||||
await listManipulationSemepahore.WaitAsync();
|
||||
|
||||
await MailCollection.AddAsync(addedMail);
|
||||
// await MailCollection.AddAsync(addedMail);
|
||||
|
||||
await ExecuteUIThread(() => { NotifyItemFoundState(); });
|
||||
}
|
||||
@@ -677,7 +642,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
|
||||
Debug.WriteLine($"Updating {updatedMail.Id}-> {updatedMail.UniqueId}");
|
||||
|
||||
await MailCollection.UpdateMailCopy(updatedMail);
|
||||
// await MailCollection.UpdateMailCopy(updatedMail);
|
||||
|
||||
await ExecuteUIThread(() => { SetupTopBarActions(); });
|
||||
}
|
||||
@@ -712,12 +677,12 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
nextItem = MailCollection.GetNextItem(removedMail);
|
||||
// nextItem = MailCollection.GetNextItem(removedMail);
|
||||
});
|
||||
}
|
||||
|
||||
// Remove the deleted item from the list.
|
||||
await MailCollection.RemoveAsync(removedMail);
|
||||
// await MailCollection.RemoveAsync(removedMail);
|
||||
|
||||
if (nextItem != null)
|
||||
WeakReferenceMessenger.Default.Send(new SelectMailItemContainerEvent(nextItem, ScrollToItem: true));
|
||||
@@ -748,11 +713,11 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
// Otherwise the draft mail item will be duplicated on the next add execution.
|
||||
await listManipulationSemepahore.WaitAsync();
|
||||
|
||||
// Create the item. Draft folder navigation is already done at this point.
|
||||
await MailCollection.AddAsync(draftMail);
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
// Create the item. Draft folder navigation is already done at this point.
|
||||
MailCollection.AddEmail(new MailItemViewModel(draftMail));
|
||||
|
||||
// New draft is created by user. Select the item.
|
||||
Messenger.Send(new MailItemNavigationRequested(draftMail.UniqueId, ScrollToItem: true));
|
||||
|
||||
@@ -765,15 +730,16 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<IMailItem> PrepareMailViewModels(IEnumerable<IMailItem> mailItems)
|
||||
private IEnumerable<MailItemViewModel> PrepareMailViewModels(IEnumerable<MailCopy> mailItems)
|
||||
{
|
||||
foreach (var item in mailItems)
|
||||
{
|
||||
if (item is MailCopy singleMailItem)
|
||||
yield return new MailItemViewModel(singleMailItem);
|
||||
else if (item is ThreadMailItem threadMailItem)
|
||||
yield return new ThreadMailItemViewModel(threadMailItem);
|
||||
}
|
||||
return mailItems.Select(a => new MailItemViewModel(a));
|
||||
//foreach (var item in mailItems)
|
||||
//{
|
||||
// if (item is MailCopy singleMailItem)
|
||||
// yield return new MailItemViewModel(singleMailItem);
|
||||
// else if (item is ThreadMailItem threadMailItem)
|
||||
// yield return new ThreadMailItemViewModel(threadMailItem);
|
||||
//}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -793,7 +759,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
try
|
||||
{
|
||||
MailCollection.Clear();
|
||||
MailCollection.MailCopyIdHashSet.Clear();
|
||||
|
||||
SelectedItems.Clear();
|
||||
|
||||
@@ -816,17 +781,9 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
|
||||
await listManipulationSemepahore.WaitAsync(cancellationToken);
|
||||
|
||||
// Setup MailCollection configuration.
|
||||
|
||||
// Don't pass any threading strategy if disabled in settings.
|
||||
MailCollection.ThreadingStrategyProvider = PreferencesService.IsThreadingEnabled ? _threadingStrategyProvider : null;
|
||||
|
||||
// TODO: This should go inside
|
||||
MailCollection.PruneSingleNonDraftItems = ActiveFolder.SpecialFolderType == SpecialFolderType.Draft;
|
||||
|
||||
// Here items are sorted and filtered.
|
||||
|
||||
List<IMailItem> items = null;
|
||||
List<MailCopy> items = null;
|
||||
List<MailCopy> onlineSearchItems = null;
|
||||
|
||||
bool isDoingSearch = !string.IsNullOrEmpty(SearchQuery);
|
||||
@@ -894,7 +851,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
PreferencesService.IsThreadingEnabled,
|
||||
SelectedFolderPivot.IsFocused,
|
||||
SearchQuery,
|
||||
MailCollection.MailCopyIdHashSet,
|
||||
default,
|
||||
onlineSearchItems);
|
||||
|
||||
items = await _mailService.FetchMailsAsync(initializationOptions, cancellationToken).ConfigureAwait(false);
|
||||
@@ -909,7 +866,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
MailCollection.AddRange(viewModels, true);
|
||||
MailCollection.AddEmails(viewModels);
|
||||
|
||||
if (isDoingSearch && !isDoingOnlineSearch)
|
||||
{
|
||||
@@ -1055,15 +1012,16 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var mailContainer = MailCollection.GetMailItemContainer(message.UniqueMailId);
|
||||
// TODO: Get container.
|
||||
//var mailContainer = MailCollection.GetMailItemContainer(message.UniqueMailId);
|
||||
|
||||
if (mailContainer != null)
|
||||
{
|
||||
navigatingMailItem = mailContainer.ItemViewModel;
|
||||
threadMailItemViewModel = mailContainer.ThreadViewModel;
|
||||
//if (mailContainer != null)
|
||||
//{
|
||||
// navigatingMailItem = mailContainer.ItemViewModel;
|
||||
// threadMailItemViewModel = mailContainer.ThreadViewModel;
|
||||
|
||||
break;
|
||||
}
|
||||
// break;
|
||||
//}
|
||||
}
|
||||
|
||||
if (threadMailItemViewModel != null)
|
||||
@@ -1138,5 +1096,8 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(ThumbnailAdded message) => MailCollection.UpdateThumbnails(message.Email);
|
||||
public void Receive(ThumbnailAdded message)
|
||||
{
|
||||
// MailCollection.UpdateThumbnails(message.Email);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
||||
|
||||
public bool ShouldDisplayDownloadProgress => IsIndetermineProgress || (CurrentDownloadPercentage > 0 && CurrentDownloadPercentage <= 100);
|
||||
public bool CanUnsubscribe => CurrentRenderModel?.UnsubscribeInfo?.CanUnsubscribe ?? false;
|
||||
public bool IsJunkMail => initializedMailItemViewModel?.AssignedFolder != null && initializedMailItemViewModel.AssignedFolder.SpecialFolderType == SpecialFolderType.Junk;
|
||||
public bool IsJunkMail => initializedMailItemViewModel?.MailCopy.AssignedFolder != null && initializedMailItemViewModel.MailCopy.AssignedFolder.SpecialFolderType == SpecialFolderType.Junk;
|
||||
|
||||
public bool IsImageRenderingDisabled
|
||||
{
|
||||
@@ -283,9 +283,9 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
||||
}
|
||||
};
|
||||
|
||||
var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(initializedMailItemViewModel.AssignedAccount.Id, draftOptions).ConfigureAwait(false);
|
||||
var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(initializedMailItemViewModel.MailCopy.AssignedAccount.Id, draftOptions).ConfigureAwait(false);
|
||||
|
||||
var draftPreparationRequest = new DraftPreparationRequest(initializedMailItemViewModel.AssignedAccount, draftMailCopy, draftBase64MimeMessage, draftOptions.Reason, initializedMailItemViewModel.MailCopy);
|
||||
var draftPreparationRequest = new DraftPreparationRequest(initializedMailItemViewModel.MailCopy.AssignedAccount, draftMailCopy, draftBase64MimeMessage, draftOptions.Reason, initializedMailItemViewModel.MailCopy);
|
||||
|
||||
await _requestDelegator.ExecuteAsync(draftPreparationRequest);
|
||||
|
||||
@@ -375,7 +375,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
||||
private async Task RenderAsync(MailItemViewModel mailItemViewModel, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ResetProgress();
|
||||
var isMimeExists = await _mimeFileService.IsMimeExistAsync(mailItemViewModel.AssignedAccount.Id, mailItemViewModel.MailCopy.FileId);
|
||||
var isMimeExists = await _mimeFileService.IsMimeExistAsync(mailItemViewModel.MailCopy.AssignedAccount.Id, mailItemViewModel.MailCopy.FileId);
|
||||
|
||||
if (!isMimeExists)
|
||||
{
|
||||
@@ -384,7 +384,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
||||
|
||||
// Find the MIME for this item and render it.
|
||||
var mimeMessageInformation = await _mimeFileService.GetMimeMessageInformationAsync(mailItemViewModel.MailCopy.FileId,
|
||||
mailItemViewModel.AssignedAccount.Id,
|
||||
mailItemViewModel.MailCopy.AssignedAccount.Id,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (mimeMessageInformation == null)
|
||||
@@ -436,12 +436,12 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
||||
FromAddress = message.From.Mailboxes.FirstOrDefault()?.Address ?? Translator.UnknownAddress;
|
||||
FromName = message.From.Mailboxes.FirstOrDefault()?.Name ?? Translator.UnknownSender;
|
||||
CreationDate = message.Date.DateTime;
|
||||
ContactPicture = initializedMailItemViewModel?.SenderContact?.Base64ContactPicture;
|
||||
ContactPicture = initializedMailItemViewModel?.MailCopy.SenderContact?.Base64ContactPicture;
|
||||
|
||||
// Automatically disable images for Junk folder to prevent pixel tracking.
|
||||
// This can only work for selected mail item rendering, not for EML file rendering.
|
||||
if (initializedMailItemViewModel != null &&
|
||||
initializedMailItemViewModel.AssignedFolder.SpecialFolderType == SpecialFolderType.Junk)
|
||||
initializedMailItemViewModel.MailCopy.AssignedFolder.SpecialFolderType == SpecialFolderType.Junk)
|
||||
{
|
||||
renderingOptions.LoadImages = false;
|
||||
}
|
||||
@@ -480,7 +480,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
||||
var contactViewModel = new AccountContactViewModel(foundContact);
|
||||
|
||||
// Make sure that user account first in the list.
|
||||
if (string.Equals(contactViewModel.Address, initializedMailItemViewModel?.AssignedAccount?.Address, StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(contactViewModel.Address, initializedMailItemViewModel?.MailCopy.AssignedAccount?.Address, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
contactViewModel.IsMe = true;
|
||||
accounts.Insert(0, contactViewModel);
|
||||
@@ -563,7 +563,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
||||
}
|
||||
|
||||
// Archive - Unarchive
|
||||
if (initializedMailItemViewModel.AssignedFolder.SpecialFolderType == SpecialFolderType.Archive)
|
||||
if (initializedMailItemViewModel.MailCopy.AssignedFolder.SpecialFolderType == SpecialFolderType.Archive)
|
||||
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.UnArchive));
|
||||
else
|
||||
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.Archive));
|
||||
@@ -594,7 +594,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
||||
|
||||
// Check if the updated mail is the same mail item we are rendering.
|
||||
// This is done with UniqueId to include FolderId into calculations.
|
||||
if (initializedMailItemViewModel.UniqueId != updatedMail.UniqueId) return;
|
||||
if (initializedMailItemViewModel.MailCopy.UniqueId != updatedMail.UniqueId) return;
|
||||
|
||||
// Mail operation might change the mail item like mark read/unread or change flag.
|
||||
// So we need to update the mail item view model when this happens.
|
||||
|
||||
@@ -20,7 +20,6 @@ using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.WinUI;
|
||||
using Wino.Core.WinUI.Controls;
|
||||
using Wino.Extensions;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
using Wino.MenuFlyouts;
|
||||
using Wino.MenuFlyouts.Context;
|
||||
using Wino.Messaging.Client.Accounts;
|
||||
@@ -67,14 +66,14 @@ public sealed partial class AppShell : AppShellAbstract,
|
||||
|
||||
foreach (var item in dragPackage.DraggingMails)
|
||||
{
|
||||
if (item is MailItemViewModel singleMailItemViewModel)
|
||||
{
|
||||
mailCopies.Add(singleMailItemViewModel.MailCopy);
|
||||
}
|
||||
else if (item is ThreadMailItemViewModel threadViewModel)
|
||||
{
|
||||
mailCopies.AddRange(threadViewModel.GetMailCopies());
|
||||
}
|
||||
//if (item is MailItemViewModel singleMailItemViewModel)
|
||||
//{
|
||||
// mailCopies.Add(singleMailItemViewModel.MailCopy);
|
||||
//}
|
||||
//else if (item is ThreadMailItemViewModel threadViewModel)
|
||||
//{
|
||||
// mailCopies.AddRange(threadViewModel.GetMailCopies());
|
||||
//}
|
||||
}
|
||||
|
||||
await ViewModel.PerformMoveOperationAsync(mailCopies, draggingFolder);
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Wino.Mail.WinUI.Controls.Advanced;
|
||||
|
||||
public partial class WinoItemsView : ItemsView
|
||||
{
|
||||
public IEnumerable<object>? CastedItemsSource => ItemsSource as IEnumerable<object>;
|
||||
|
||||
public WinoItemsView()
|
||||
{
|
||||
DefaultStyleKey = typeof(ItemsView);
|
||||
}
|
||||
}
|
||||
@@ -1,418 +0,0 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using MoreLinq;
|
||||
using Serilog;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Extensions;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
using Wino.Mail.ViewModels.Messages;
|
||||
|
||||
namespace Wino.Controls.Advanced;
|
||||
|
||||
/// <summary>
|
||||
/// Custom ListView control that handles multiple selection with Extended/Multiple selection mode
|
||||
/// and supports threads.
|
||||
/// </summary>
|
||||
public partial class WinoListView : ListView, IDisposable
|
||||
{
|
||||
private ILogger logger = Log.ForContext<WinoListView>();
|
||||
|
||||
private const string PART_ScrollViewer = "ScrollViewer";
|
||||
private ScrollViewer internalScrollviewer;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether this ListView belongs to thread items.
|
||||
/// This is important for detecting selected items etc.
|
||||
/// </summary>
|
||||
public bool IsThreadListView
|
||||
{
|
||||
get { return (bool)GetValue(IsThreadListViewProperty); }
|
||||
set { SetValue(IsThreadListViewProperty, value); }
|
||||
}
|
||||
|
||||
public ICommand ItemDeletedCommand
|
||||
{
|
||||
get { return (ICommand)GetValue(ItemDeletedCommandProperty); }
|
||||
set { SetValue(ItemDeletedCommandProperty, value); }
|
||||
}
|
||||
|
||||
public ICommand LoadMoreCommand
|
||||
{
|
||||
get { return (ICommand)GetValue(LoadMoreCommandProperty); }
|
||||
set { SetValue(LoadMoreCommandProperty, value); }
|
||||
}
|
||||
|
||||
public bool IsThreadScrollingEnabled
|
||||
{
|
||||
get { return (bool)GetValue(IsThreadScrollingEnabledProperty); }
|
||||
set { SetValue(IsThreadScrollingEnabledProperty, value); }
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty IsThreadScrollingEnabledProperty = DependencyProperty.Register(nameof(IsThreadScrollingEnabled), typeof(bool), typeof(WinoListView), new PropertyMetadata(false));
|
||||
public static readonly DependencyProperty LoadMoreCommandProperty = DependencyProperty.Register(nameof(LoadMoreCommand), typeof(ICommand), typeof(WinoListView), new PropertyMetadata(null));
|
||||
public static readonly DependencyProperty IsThreadListViewProperty = DependencyProperty.Register(nameof(IsThreadListView), typeof(bool), typeof(WinoListView), new PropertyMetadata(false, new PropertyChangedCallback(OnIsThreadViewChanged)));
|
||||
public static readonly DependencyProperty ItemDeletedCommandProperty = DependencyProperty.Register(nameof(ItemDeletedCommand), typeof(ICommand), typeof(WinoListView), new PropertyMetadata(null));
|
||||
|
||||
public WinoListView()
|
||||
{
|
||||
CanDragItems = true;
|
||||
IsItemClickEnabled = true;
|
||||
IsMultiSelectCheckBoxEnabled = true;
|
||||
IsRightTapEnabled = true;
|
||||
SelectionMode = ListViewSelectionMode.Extended;
|
||||
ShowsScrollingPlaceholders = false;
|
||||
SingleSelectionFollowsFocus = true;
|
||||
|
||||
DragItemsCompleted += ItemDragCompleted;
|
||||
DragItemsStarting += ItemDragStarting;
|
||||
SelectionChanged += SelectedItemsChanged;
|
||||
ProcessKeyboardAccelerators += ProcessDelKey;
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate()
|
||||
{
|
||||
base.OnApplyTemplate();
|
||||
|
||||
internalScrollviewer = GetTemplateChild(PART_ScrollViewer) as ScrollViewer;
|
||||
|
||||
if (internalScrollviewer == null)
|
||||
{
|
||||
logger.Warning("WinoListView does not have an internal ScrollViewer. Infinite scrolling behavior might be effected.");
|
||||
return;
|
||||
}
|
||||
|
||||
internalScrollviewer.ViewChanged -= InternalScrollVeiwerViewChanged;
|
||||
internalScrollviewer.ViewChanged += InternalScrollVeiwerViewChanged;
|
||||
}
|
||||
|
||||
private static void OnIsThreadViewChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
|
||||
{
|
||||
if (obj is WinoListView winoListView)
|
||||
{
|
||||
winoListView.AdjustThreadViewContainerVisuals();
|
||||
}
|
||||
}
|
||||
|
||||
private void AdjustThreadViewContainerVisuals()
|
||||
{
|
||||
if (IsThreadListView)
|
||||
{
|
||||
ItemContainerTransitions.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private double lastestRaisedOffset = 0;
|
||||
private int lastItemSize = 0;
|
||||
|
||||
// TODO: This is buggy. Does not work all the time. Debug.
|
||||
|
||||
private void InternalScrollVeiwerViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
|
||||
{
|
||||
if (internalScrollviewer == null) return;
|
||||
|
||||
// No need to raise init request if there are no items in the list.
|
||||
if (Items.Count == 0) return;
|
||||
|
||||
// If the scrolling is finished, check the current viewport height.
|
||||
if (e.IsIntermediate)
|
||||
{
|
||||
var currentOffset = internalScrollviewer.VerticalOffset;
|
||||
var maxOffset = internalScrollviewer.ScrollableHeight;
|
||||
|
||||
if (currentOffset + 10 >= maxOffset && lastestRaisedOffset != maxOffset && Items.Count != lastItemSize)
|
||||
{
|
||||
// We must load more.
|
||||
lastestRaisedOffset = maxOffset;
|
||||
lastItemSize = Items.Count;
|
||||
|
||||
LoadMoreCommand?.Execute(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessDelKey(UIElement sender, Microsoft.UI.Xaml.Input.ProcessKeyboardAcceleratorEventArgs args)
|
||||
{
|
||||
if (args.Key == Windows.System.VirtualKey.Delete)
|
||||
{
|
||||
args.Handled = true;
|
||||
|
||||
ItemDeletedCommand?.Execute(MailOperation.SoftDelete);
|
||||
}
|
||||
}
|
||||
|
||||
private void ItemDragCompleted(ListViewBase sender, DragItemsCompletedEventArgs args)
|
||||
{
|
||||
if (args.Items.Any(a => a is MailItemViewModel))
|
||||
{
|
||||
args.Items.Cast<MailItemViewModel>().ForEach(a => a.IsCustomFocused = false);
|
||||
}
|
||||
}
|
||||
|
||||
private void ItemDragStarting(object sender, DragItemsStartingEventArgs args)
|
||||
{
|
||||
// Dragging multiple mails from different accounts/folders are supported with the condition below:
|
||||
// All mails belongs to the drag will be matched on the dropped folder's account.
|
||||
// Meaning that if users drag 1 mail from Account A/Inbox and 1 mail from Account B/Inbox,
|
||||
// and drop to Account A/Inbox, the mail from Account B/Inbox will NOT be moved.
|
||||
|
||||
if (IsThreadListView)
|
||||
{
|
||||
var allItems = args.Items.Cast<MailItemViewModel>();
|
||||
|
||||
// Highlight all items
|
||||
allItems.ForEach(a => a.IsCustomFocused = true);
|
||||
|
||||
// Set native drag arg properties.
|
||||
|
||||
var dragPackage = new MailDragPackage(allItems.Cast<IMailItem>());
|
||||
|
||||
args.Data.Properties.Add(nameof(MailDragPackage), dragPackage);
|
||||
}
|
||||
else
|
||||
{
|
||||
var dragPackage = new MailDragPackage(args.Items.Cast<IMailItem>());
|
||||
|
||||
args.Data.Properties.Add(nameof(MailDragPackage), dragPackage);
|
||||
}
|
||||
}
|
||||
|
||||
public void ChangeSelectionMode(ListViewSelectionMode selectionMode)
|
||||
{
|
||||
SelectionMode = selectionMode;
|
||||
|
||||
if (!IsThreadListView)
|
||||
{
|
||||
Items.Where(a => a is ThreadMailItemViewModel).Cast<ThreadMailItemViewModel>().ForEach(c =>
|
||||
{
|
||||
var threadListView = GetThreadInternalListView(c);
|
||||
|
||||
if (threadListView != null)
|
||||
{
|
||||
threadListView.SelectionMode = selectionMode;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the container for given mail item and adds it to selected items.
|
||||
/// </summary>
|
||||
/// <param name="mailItemViewModel">Mail to be added to selected items.</param>
|
||||
/// <returns>Whether selection was successful or not.</returns>
|
||||
public bool SelectMailItemContainer(MailItemViewModel mailItemViewModel)
|
||||
{
|
||||
var itemContainer = ContainerFromItem(mailItemViewModel);
|
||||
|
||||
// This item might be in thread container.
|
||||
if (itemContainer == null)
|
||||
{
|
||||
bool found = false;
|
||||
|
||||
Items.OfType<ThreadMailItemViewModel>().ForEach(c =>
|
||||
{
|
||||
if (!found)
|
||||
{
|
||||
var threadListView = GetThreadInternalListView(c);
|
||||
|
||||
if (threadListView != null)
|
||||
found = threadListView.SelectMailItemContainer(mailItemViewModel);
|
||||
}
|
||||
});
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
SelectedItems.Add(mailItemViewModel);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively clears all selections except the given mail.
|
||||
/// </summary>
|
||||
/// <param name="exceptViewModel">Exceptional mail item to be not unselected.</param>
|
||||
/// <param name="preserveThreadExpanding">Whether expansion states of thread containers should stay as it is or not.</param>
|
||||
public void ClearSelections(MailItemViewModel exceptViewModel = null, bool preserveThreadExpanding = false)
|
||||
{
|
||||
SelectedItems.Clear();
|
||||
|
||||
Items.Where(a => a is ThreadMailItemViewModel).Cast<ThreadMailItemViewModel>().ForEach(c =>
|
||||
{
|
||||
var threadListView = GetThreadInternalListView(c);
|
||||
|
||||
if (threadListView == null)
|
||||
return;
|
||||
|
||||
if (exceptViewModel != null)
|
||||
{
|
||||
if (!threadListView.SelectedItems.Contains(exceptViewModel))
|
||||
{
|
||||
if (!preserveThreadExpanding)
|
||||
{
|
||||
c.IsThreadExpanded = false;
|
||||
}
|
||||
|
||||
threadListView.SelectedItems.Clear();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!preserveThreadExpanding)
|
||||
{
|
||||
c.IsThreadExpanded = false;
|
||||
}
|
||||
|
||||
threadListView.SelectedItems.Clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively selects all mails, including thread items.
|
||||
/// </summary>
|
||||
public void SelectAllWino()
|
||||
{
|
||||
SelectAll();
|
||||
|
||||
Items.Where(a => a is ThreadMailItemViewModel).Cast<ThreadMailItemViewModel>().ForEach(c =>
|
||||
{
|
||||
c.IsThreadExpanded = true;
|
||||
|
||||
var threadListView = GetThreadInternalListView(c);
|
||||
|
||||
threadListView?.SelectAll();
|
||||
});
|
||||
}
|
||||
|
||||
// SelectedItems changed.
|
||||
private void SelectedItemsChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (e.RemovedItems != null)
|
||||
{
|
||||
foreach (var removedItem in e.RemovedItems)
|
||||
{
|
||||
if (removedItem is MailItemViewModel removedMailItemViewModel)
|
||||
{
|
||||
// Mail item un-selected.
|
||||
|
||||
removedMailItemViewModel.IsSelected = false;
|
||||
WeakReferenceMessenger.Default.Send(new MailItemSelectionRemovedEvent(removedMailItemViewModel));
|
||||
}
|
||||
else if (removedItem is ThreadMailItemViewModel removedThreadItemViewModel)
|
||||
{
|
||||
removedThreadItemViewModel.IsThreadExpanded = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (e.AddedItems != null)
|
||||
{
|
||||
foreach (var addedItem in e.AddedItems)
|
||||
{
|
||||
if (addedItem is MailItemViewModel addedMailItemViewModel)
|
||||
{
|
||||
// Mail item selected.
|
||||
|
||||
addedMailItemViewModel.IsSelected = true;
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new MailItemSelectedEvent(addedMailItemViewModel));
|
||||
}
|
||||
else if (addedItem is ThreadMailItemViewModel threadMailItemViewModel)
|
||||
{
|
||||
if (IsThreadScrollingEnabled)
|
||||
{
|
||||
if (internalScrollviewer != null && ContainerFromItem(threadMailItemViewModel) is FrameworkElement threadFrameworkElement)
|
||||
{
|
||||
internalScrollviewer.ScrollToElement(threadFrameworkElement, true, true, bringToTopOrLeft: true);
|
||||
}
|
||||
}
|
||||
|
||||
// Try to select first item.
|
||||
if (GetThreadInternalListView(threadMailItemViewModel) is WinoListView internalListView)
|
||||
{
|
||||
internalListView.SelectFirstItem();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!IsThreadListView)
|
||||
{
|
||||
if (SelectionMode == ListViewSelectionMode.Extended && SelectedItems.Count == 1)
|
||||
{
|
||||
// Only 1 single item is selected in extended mode for main list view.
|
||||
// We should un-select all thread items.
|
||||
|
||||
Items.Where(a => a is ThreadMailItemViewModel).Cast<ThreadMailItemViewModel>().ForEach(c =>
|
||||
{
|
||||
// c.IsThreadExpanded = false;
|
||||
|
||||
var threadListView = GetThreadInternalListView(c);
|
||||
|
||||
threadListView?.SelectedItems.Clear();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async void SelectFirstItem()
|
||||
{
|
||||
if (Items.Count > 0)
|
||||
{
|
||||
if (Items[0] is MailItemViewModel firstMailItemViewModel)
|
||||
{
|
||||
// Make sure the invisible container is realized.
|
||||
await Task.Delay(250);
|
||||
|
||||
if (ContainerFromItem(firstMailItemViewModel) is ListViewItem firstItemContainer)
|
||||
{
|
||||
firstItemContainer.IsSelected = true;
|
||||
}
|
||||
|
||||
firstMailItemViewModel.IsSelected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private WinoListView GetThreadInternalListView(ThreadMailItemViewModel threadMailItemViewModel)
|
||||
{
|
||||
var itemContainer = ContainerFromItem(threadMailItemViewModel);
|
||||
|
||||
if (itemContainer is ListViewItem listItem)
|
||||
{
|
||||
var expander = listItem.GetChildByName<WinoExpander>("ThreadExpander");
|
||||
|
||||
if (expander != null)
|
||||
return expander.Content as WinoListView;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DragItemsCompleted -= ItemDragCompleted;
|
||||
DragItemsStarting -= ItemDragStarting;
|
||||
SelectionChanged -= SelectedItemsChanged;
|
||||
ProcessKeyboardAccelerators -= ProcessDelKey;
|
||||
|
||||
if (internalScrollviewer != null)
|
||||
{
|
||||
internalScrollviewer.ViewChanged -= InternalScrollVeiwerViewChanged;
|
||||
}
|
||||
|
||||
foreach (var item in Items)
|
||||
{
|
||||
if (item is ThreadMailItemViewModel threadMailItemViewModel)
|
||||
{
|
||||
var threadListView = GetThreadInternalListView(threadMailItemViewModel);
|
||||
threadListView?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Numerics;
|
||||
using System.Windows.Input;
|
||||
using CommunityToolkit.WinUI;
|
||||
using Microsoft.UI.Xaml;
|
||||
@@ -9,7 +8,6 @@ using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Extensions;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
|
||||
namespace Wino.Controls;
|
||||
|
||||
@@ -21,7 +19,6 @@ public sealed partial class MailItemDisplayInformationControl : UserControl
|
||||
|
||||
public static readonly DependencyProperty DisplayModeProperty = DependencyProperty.Register(nameof(DisplayMode), typeof(MailListDisplayMode), typeof(MailItemDisplayInformationControl), new PropertyMetadata(MailListDisplayMode.Spacious));
|
||||
public static readonly DependencyProperty ShowPreviewTextProperty = DependencyProperty.Register(nameof(ShowPreviewText), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(true));
|
||||
public static readonly DependencyProperty IsCustomFocusedProperty = DependencyProperty.Register(nameof(IsCustomFocused), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(false));
|
||||
public static readonly DependencyProperty IsAvatarVisibleProperty = DependencyProperty.Register(nameof(IsAvatarVisible), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(true));
|
||||
public static readonly DependencyProperty IsSubjectVisibleProperty = DependencyProperty.Register(nameof(IsSubjectVisible), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(true));
|
||||
public static readonly DependencyProperty ConnectedExpanderProperty = DependencyProperty.Register(nameof(ConnectedExpander), typeof(WinoExpander), typeof(MailItemDisplayInformationControl), new PropertyMetadata(null));
|
||||
@@ -29,7 +26,7 @@ public sealed partial class MailItemDisplayInformationControl : UserControl
|
||||
public static readonly DependencyProperty CenterHoverActionProperty = DependencyProperty.Register(nameof(CenterHoverAction), typeof(MailOperation), typeof(MailItemDisplayInformationControl), new PropertyMetadata(MailOperation.None));
|
||||
public static readonly DependencyProperty RightHoverActionProperty = DependencyProperty.Register(nameof(RightHoverAction), typeof(MailOperation), typeof(MailItemDisplayInformationControl), new PropertyMetadata(MailOperation.None));
|
||||
public static readonly DependencyProperty HoverActionExecutedCommandProperty = DependencyProperty.Register(nameof(HoverActionExecutedCommand), typeof(ICommand), typeof(MailItemDisplayInformationControl), new PropertyMetadata(null));
|
||||
public static readonly DependencyProperty MailItemProperty = DependencyProperty.Register(nameof(MailItem), typeof(IMailItem), typeof(MailItemDisplayInformationControl), new PropertyMetadata(null, new PropertyChangedCallback(OnMailItemChanged)));
|
||||
public static readonly DependencyProperty MailItemProperty = DependencyProperty.Register(nameof(MailItem), typeof(MailCopy), typeof(MailItemDisplayInformationControl), new PropertyMetadata(null, new PropertyChangedCallback(OnMailItemChanged)));
|
||||
public static readonly DependencyProperty IsHoverActionsEnabledProperty = DependencyProperty.Register(nameof(IsHoverActionsEnabled), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(true));
|
||||
public static readonly DependencyProperty Prefer24HourTimeFormatProperty = DependencyProperty.Register(nameof(Prefer24HourTimeFormat), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(false));
|
||||
public static readonly DependencyProperty IsThreadExpanderVisibleProperty = DependencyProperty.Register(nameof(IsThreadExpanderVisible), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(false));
|
||||
@@ -66,9 +63,9 @@ public sealed partial class MailItemDisplayInformationControl : UserControl
|
||||
set { SetValue(IsHoverActionsEnabledProperty, value); }
|
||||
}
|
||||
|
||||
public IMailItem MailItem
|
||||
public MailCopy MailItem
|
||||
{
|
||||
get { return (IMailItem)GetValue(MailItemProperty); }
|
||||
get { return (MailCopy)GetValue(MailItemProperty); }
|
||||
set { SetValue(MailItemProperty, value); }
|
||||
}
|
||||
|
||||
@@ -114,11 +111,6 @@ public sealed partial class MailItemDisplayInformationControl : UserControl
|
||||
set { SetValue(IsAvatarVisibleProperty, value); }
|
||||
}
|
||||
|
||||
public bool IsCustomFocused
|
||||
{
|
||||
get { return (bool)GetValue(IsCustomFocusedProperty); }
|
||||
set { SetValue(IsCustomFocusedProperty, value); }
|
||||
}
|
||||
|
||||
public bool ShowPreviewText
|
||||
{
|
||||
@@ -189,12 +181,12 @@ public sealed partial class MailItemDisplayInformationControl : UserControl
|
||||
|
||||
MailOperationPreperationRequest package = null;
|
||||
|
||||
if (MailItem is MailCopy mailCopy)
|
||||
package = new MailOperationPreperationRequest(operation, mailCopy, toggleExecution: true);
|
||||
else if (MailItem is ThreadMailItemViewModel threadMailItemViewModel)
|
||||
package = new MailOperationPreperationRequest(operation, threadMailItemViewModel.GetMailCopies(), toggleExecution: true);
|
||||
else if (MailItem is ThreadMailItem threadMailItem)
|
||||
package = new MailOperationPreperationRequest(operation, threadMailItem.ThreadItems.Cast<MailItemViewModel>().Select(a => a.MailCopy), toggleExecution: true);
|
||||
//if (MailItem is MailCopy mailCopy)
|
||||
// package = new MailOperationPreperationRequest(operation, mailCopy, toggleExecution: true);
|
||||
//else if (MailItem is ThreadMailItemViewModel threadMailItemViewModel)
|
||||
// package = new MailOperationPreperationRequest(operation, threadMailItemViewModel.GetMailCopies(), toggleExecution: true);
|
||||
//else if (MailItem is ThreadMailItem threadMailItem)
|
||||
// package = new MailOperationPreperationRequest(operation, threadMailItem.ThreadItems.Cast<MailItemViewModel>().Select(a => a.MailCopy), toggleExecution: true);
|
||||
|
||||
if (package == null) return;
|
||||
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
using System.Linq;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Helpers;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
|
||||
namespace Wino.Controls;
|
||||
|
||||
public partial class WinoSwipeControlItems : SwipeItems
|
||||
{
|
||||
public static readonly DependencyProperty SwipeOperationProperty = DependencyProperty.Register(nameof(SwipeOperation), typeof(MailOperation), typeof(WinoSwipeControlItems), new PropertyMetadata(default(MailOperation), new PropertyChangedCallback(OnItemsChanged)));
|
||||
public static readonly DependencyProperty MailItemProperty = DependencyProperty.Register(nameof(MailItem), typeof(IMailItem), typeof(WinoSwipeControlItems), new PropertyMetadata(null));
|
||||
|
||||
public IMailItem MailItem
|
||||
{
|
||||
get { return (IMailItem)GetValue(MailItemProperty); }
|
||||
set { SetValue(MailItemProperty, value); }
|
||||
}
|
||||
|
||||
|
||||
public MailOperation SwipeOperation
|
||||
{
|
||||
get { return (MailOperation)GetValue(SwipeOperationProperty); }
|
||||
set { SetValue(SwipeOperationProperty, value); }
|
||||
}
|
||||
|
||||
private static void OnItemsChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
|
||||
{
|
||||
if (obj is WinoSwipeControlItems control)
|
||||
{
|
||||
control.BuildSwipeItems();
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildSwipeItems()
|
||||
{
|
||||
this.Clear();
|
||||
|
||||
var swipeItem = GetSwipeItem(SwipeOperation);
|
||||
|
||||
this.Add(swipeItem);
|
||||
}
|
||||
|
||||
private SwipeItem GetSwipeItem(MailOperation operation)
|
||||
{
|
||||
if (MailItem == null) return null;
|
||||
|
||||
var finalOperation = operation;
|
||||
|
||||
bool isSingleItem = MailItem is MailItemViewModel;
|
||||
|
||||
if (isSingleItem)
|
||||
{
|
||||
var singleItem = MailItem as MailItemViewModel;
|
||||
|
||||
if (operation == MailOperation.MarkAsRead && singleItem.IsRead)
|
||||
finalOperation = MailOperation.MarkAsUnread;
|
||||
else if (operation == MailOperation.MarkAsUnread && !singleItem.IsRead)
|
||||
finalOperation = MailOperation.MarkAsRead;
|
||||
}
|
||||
else
|
||||
{
|
||||
var threadItem = MailItem as ThreadMailItemViewModel;
|
||||
|
||||
if (operation == MailOperation.MarkAsRead && threadItem.ThreadItems.All(a => a.IsRead))
|
||||
finalOperation = MailOperation.MarkAsUnread;
|
||||
else if (operation == MailOperation.MarkAsUnread && threadItem.ThreadItems.All(a => !a.IsRead))
|
||||
finalOperation = MailOperation.MarkAsRead;
|
||||
}
|
||||
|
||||
var item = new SwipeItem()
|
||||
{
|
||||
IconSource = new WinoFontIconSource() { Icon = XamlHelpers.GetWinoIconGlyph(finalOperation) },
|
||||
Text = XamlHelpers.GetOperationString(finalOperation),
|
||||
};
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Wino.Mail.ViewModels.Collections;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
|
||||
namespace Wino.Mail.WinUI.Selectors;
|
||||
|
||||
public partial class MailItemContainerSelector : DataTemplateSelector
|
||||
{
|
||||
public DataTemplate? EmailTemplate { get; set; }
|
||||
public DataTemplate? ThreadExpanderTemplate { get; set; }
|
||||
public DataTemplate? DateGroupHeaderTemplate { get; set; }
|
||||
public DataTemplate? SenderGroupHeaderTemplate { get; set; }
|
||||
|
||||
protected override DataTemplate SelectTemplateCore(object item)
|
||||
{
|
||||
return item switch
|
||||
{
|
||||
MailItemViewModel => EmailTemplate ?? throw new ArgumentNullException(nameof(EmailTemplate)),
|
||||
ThreadMailItemViewModel => ThreadExpanderTemplate ?? throw new ArgumentNullException(nameof(ThreadExpanderTemplate)),
|
||||
DateGroupHeader => DateGroupHeaderTemplate ?? throw new ArgumentNullException(nameof(DateGroupHeaderTemplate)),
|
||||
SenderGroupHeader => SenderGroupHeaderTemplate ?? throw new ArgumentNullException(nameof(SenderGroupHeaderTemplate)),
|
||||
_ => base.SelectTemplateCore(item)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,23 +3,26 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:abstract="using:Wino.Views.Abstract"
|
||||
xmlns:advanced="using:Wino.Mail.WinUI.Controls.Advanced"
|
||||
xmlns:animatedvisuals="using:Microsoft.UI.Xaml.Controls.AnimatedVisuals"
|
||||
xmlns:animations="using:CommunityToolkit.WinUI.Animations"
|
||||
xmlns:collections="using:CommunityToolkit.Mvvm.Collections"
|
||||
xmlns:controls="using:Wino.Controls"
|
||||
xmlns:converters="using:Wino.Converters"
|
||||
xmlns:coreControls="using:Wino.Core.WinUI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:data="using:Wino.Mail.ViewModels.Collections"
|
||||
xmlns:domain="using:Wino.Core.Domain"
|
||||
xmlns:enums="using:Wino.Core.Domain.Enums"
|
||||
xmlns:helpers="using:Wino.Helpers"
|
||||
xmlns:i="using:Microsoft.Xaml.Interactivity"
|
||||
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
|
||||
xmlns:listview="using:Wino.Controls.Advanced"
|
||||
xmlns:local="using:Wino.Behaviors"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:menuflyouts="using:Wino.MenuFlyouts"
|
||||
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
|
||||
xmlns:selectors="using:Wino.Selectors"
|
||||
xmlns:selectors1="using:Wino.Mail.WinUI.Selectors"
|
||||
xmlns:toolkit="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:toolkitExt="using:CommunityToolkit.WinUI"
|
||||
xmlns:viewModelData="using:Wino.Mail.ViewModels.Data"
|
||||
@@ -30,17 +33,11 @@
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Page.Resources>
|
||||
<CollectionViewSource
|
||||
x:Name="MailCollectionViewSource"
|
||||
IsSourceGrouped="True"
|
||||
Source="{x:Bind ViewModel.MailCollection.MailItems, Mode=OneWay}" />
|
||||
|
||||
<Thickness x:Key="ExpanderHeaderPadding">0,0,0,0</Thickness>
|
||||
<Thickness x:Key="ExpanderChevronMargin">0,0,12,0</Thickness>
|
||||
<Thickness x:Key="ExpanderHeaderBorderThickness">0,0,0,0</Thickness>
|
||||
|
||||
<SolidColorBrush x:Key="CardBackgroundFillColorDefaultBrush">Transparent</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="CardBackgroundFillColorSecondaryBrush">Transparent</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="ExpanderContentBorderBrush">Transparent</SolidColorBrush>
|
||||
|
||||
<StaticResource x:Key="ExpanderContentBackground" ResourceKey="CardBackgroundFillColorSecondaryBrush" />
|
||||
@@ -49,155 +46,210 @@
|
||||
<SolidColorBrush x:Key="SystemControlRevealFocusVisualBrush" Color="#FF4C4A48" />
|
||||
<SolidColorBrush x:Key="SystemControlFocusVisualSecondaryBrush" Color="#FFFFFFFF" />
|
||||
|
||||
<!-- Header Templates -->
|
||||
<DataTemplate x:Key="MailGroupHeaderDefaultTemplate" x:DataType="collections:IReadOnlyObservableGroup">
|
||||
<Grid
|
||||
Margin="4,2"
|
||||
AllowFocusOnInteraction="False"
|
||||
Background="{ThemeResource MailListHeaderBackgroundColor}"
|
||||
CornerRadius="6">
|
||||
<TextBlock
|
||||
Padding="12"
|
||||
VerticalAlignment="Center"
|
||||
AllowFocusOnInteraction="False"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Text="{x:Bind helpers:XamlHelpers.GetMailGroupDateString(Key)}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- Swipe Items -->
|
||||
<!--
|
||||
TODO: Temporarily disabled. WinUI2 - SwipeControl crash.
|
||||
https://github.com/microsoft/microsoft-ui-xaml/issues/2527
|
||||
-->
|
||||
|
||||
<!--<SwipeItems x:Key="LeftSwipeItems" Mode="Execute">
|
||||
<SwipeItem BehaviorOnInvoked="Close"
|
||||
Invoked="LeftSwipeItemInvoked"
|
||||
Text="{x:Bind domain:Translator.MailOperation_Delete}">
|
||||
<SwipeItem.IconSource>
|
||||
<coreControls:WinoFontIconSource Icon="Delete" />
|
||||
</SwipeItem.IconSource>
|
||||
<SwipeItem.Background>
|
||||
<LinearGradientBrush StartPoint="0,0.5" EndPoint="1,0.5">
|
||||
<GradientStop Offset="0.0" Color="#FFF5A6A6" />
|
||||
<GradientStop Offset="0.5" Color="#FFEF4444" />
|
||||
<GradientStop Offset="1.0" Color="#FFF32525" />
|
||||
</LinearGradientBrush>
|
||||
</SwipeItem.Background>
|
||||
</SwipeItem>
|
||||
</SwipeItems>
|
||||
|
||||
-->
|
||||
|
||||
<!--
|
||||
<SwipeItems x:Key="RightSwipeItems" Mode="Execute">
|
||||
<SwipeItem BehaviorOnInvoked="Close"
|
||||
Invoked="RightSwipeItemInvoked"
|
||||
Text="{x:Bind domain:Translator.MarkReadUnread}">
|
||||
<SwipeItem.IconSource>
|
||||
<coreControls:WinoFontIconSource Icon="MarkRead" />
|
||||
</SwipeItem.IconSource>
|
||||
</SwipeItem>
|
||||
</SwipeItems>-->
|
||||
|
||||
<!-- Single Mail Item Template -->
|
||||
<DataTemplate x:Key="SingleMailItemTemplate" x:DataType="viewModelData:MailItemViewModel">
|
||||
<controls:MailItemDisplayInformationControl
|
||||
x:DefaultBindMode="OneWay"
|
||||
CenterHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.CenterHoverAction, Mode=OneWay}"
|
||||
ContextRequested="MailItemContextRequested"
|
||||
DisplayMode="{Binding ElementName=root, Path=ViewModel.PreferencesService.MailItemDisplayMode, Mode=OneWay}"
|
||||
HoverActionExecutedCommand="{Binding ElementName=root, Path=ViewModel.ExecuteHoverActionCommand}"
|
||||
IsAvatarVisible="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowSenderPicturesEnabled, Mode=OneWay}"
|
||||
IsCustomFocused="{x:Bind IsCustomFocused, Mode=OneWay}"
|
||||
IsHoverActionsEnabled="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsHoverActionsEnabled, Mode=OneWay}"
|
||||
IsThumbnailUpdated="{x:Bind ThumbnailUpdatedEvent, Mode=OneWay}"
|
||||
LeftHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.LeftHoverAction, Mode=OneWay}"
|
||||
MailItem="{x:Bind MailCopy, Mode=OneWay}"
|
||||
Prefer24HourTimeFormat="{Binding ElementName=root, Path=ViewModel.PreferencesService.Prefer24HourTimeFormat, Mode=OneWay}"
|
||||
RightHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.RightHoverAction, Mode=OneWay}"
|
||||
ShowPreviewText="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowPreviewEnabled, Mode=OneWay}" />
|
||||
</DataTemplate>
|
||||
|
||||
<!-- Single Mail Item Template for Threads -->
|
||||
<DataTemplate x:Key="ThreadSingleMailItemTemplate" x:DataType="viewModelData:MailItemViewModel">
|
||||
<controls:MailItemDisplayInformationControl
|
||||
x:DefaultBindMode="OneWay"
|
||||
CenterHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.CenterHoverAction, Mode=OneWay}"
|
||||
ContextRequested="MailItemContextRequested"
|
||||
DisplayMode="{Binding ElementName=root, Path=ViewModel.PreferencesService.MailItemDisplayMode, Mode=OneWay}"
|
||||
FocusVisualMargin="8"
|
||||
FocusVisualPrimaryBrush="{StaticResource SystemControlRevealFocusVisualBrush}"
|
||||
FocusVisualPrimaryThickness="2"
|
||||
FocusVisualSecondaryBrush="{StaticResource SystemControlFocusVisualSecondaryBrush}"
|
||||
FocusVisualSecondaryThickness="1"
|
||||
HoverActionExecutedCommand="{Binding ElementName=root, Path=ViewModel.ExecuteHoverActionCommand}"
|
||||
IsAvatarVisible="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowSenderPicturesEnabled, Mode=OneWay}"
|
||||
IsCustomFocused="{x:Bind IsCustomFocused, Mode=OneWay}"
|
||||
IsHoverActionsEnabled="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsHoverActionsEnabled, Mode=OneWay}"
|
||||
IsThumbnailUpdated="{x:Bind ThumbnailUpdatedEvent, Mode=OneWay}"
|
||||
LeftHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.LeftHoverAction, Mode=OneWay}"
|
||||
MailItem="{x:Bind MailCopy, Mode=OneWay}"
|
||||
Prefer24HourTimeFormat="{Binding ElementName=root, Path=ViewModel.PreferencesService.Prefer24HourTimeFormat, Mode=OneWay}"
|
||||
RightHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.RightHoverAction, Mode=OneWay}"
|
||||
ShowPreviewText="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowPreviewEnabled, Mode=OneWay}" />
|
||||
</DataTemplate>
|
||||
|
||||
<!-- Mail Item Content Selector -->
|
||||
<selectors:MailItemDisplaySelector x:Key="MailItemDisplaySelector" SingleMailItemTemplate="{StaticResource SingleMailItemTemplate}">
|
||||
<selectors:MailItemDisplaySelector.ThreadMailItemTemplate>
|
||||
<DataTemplate x:DataType="viewModelData:ThreadMailItemViewModel">
|
||||
<controls:WinoExpander
|
||||
x:Name="ThreadExpander"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
IsExpanded="{x:Bind IsThreadExpanded, Mode=TwoWay}">
|
||||
<controls:WinoExpander.Header>
|
||||
<controls:MailItemDisplayInformationControl
|
||||
x:DefaultBindMode="OneWay"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
CenterHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.CenterHoverAction, Mode=OneWay}"
|
||||
ConnectedExpander="{Binding ElementName=ThreadExpander}"
|
||||
ContextRequested="MailItemContextRequested"
|
||||
DisplayMode="{Binding ElementName=root, Path=ViewModel.PreferencesService.MailItemDisplayMode, Mode=OneWay}"
|
||||
DragStarting="ThreadHeaderDragStart"
|
||||
DropCompleted="ThreadHeaderDragFinished"
|
||||
HoverActionExecutedCommand="{Binding ElementName=root, Path=ViewModel.ExecuteHoverActionCommand}"
|
||||
IsAvatarVisible="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowSenderPicturesEnabled, Mode=OneWay}"
|
||||
IsHitTestVisible="True"
|
||||
IsHoverActionsEnabled="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsHoverActionsEnabled, Mode=OneWay}"
|
||||
IsThreadExpanded="{x:Bind IsThreadExpanded, Mode=TwoWay}"
|
||||
IsThreadExpanderVisible="True"
|
||||
LeftHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.LeftHoverAction, Mode=OneWay}"
|
||||
MailItem="{x:Bind MailItem, Mode=OneWay}"
|
||||
Prefer24HourTimeFormat="{Binding ElementName=root, Path=ViewModel.PreferencesService.Prefer24HourTimeFormat, Mode=OneWay}"
|
||||
RightHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.RightHoverAction, Mode=OneWay}"
|
||||
ShowPreviewText="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowPreviewEnabled, Mode=OneWay}" />
|
||||
</controls:WinoExpander.Header>
|
||||
<controls:WinoExpander.Content>
|
||||
<listview:WinoListView
|
||||
x:Name="ThreadItemsList"
|
||||
toolkitExt:ListViewExtensions.ItemContainerStretchDirection="Horizontal"
|
||||
IsThreadListView="True"
|
||||
ItemTemplate="{StaticResource ThreadSingleMailItemTemplate}"
|
||||
ItemsSource="{x:Bind ThreadItems}"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Hidden">
|
||||
<ListView.ItemContainerTransitions>
|
||||
<TransitionCollection>
|
||||
<EdgeUIThemeTransition />
|
||||
</TransitionCollection>
|
||||
</ListView.ItemContainerTransitions>
|
||||
</listview:WinoListView>
|
||||
</controls:WinoExpander.Content>
|
||||
</controls:WinoExpander>
|
||||
</DataTemplate>
|
||||
</selectors:MailItemDisplaySelector.ThreadMailItemTemplate>
|
||||
</selectors:MailItemDisplaySelector>
|
||||
|
||||
<SolidColorBrush x:Key="ButtonBackgroundDisabled">Transparent</SolidColorBrush>
|
||||
|
||||
<StackLayout
|
||||
x:Key="DefaultItemsViewLayout"
|
||||
Orientation="Vertical"
|
||||
Spacing="6" />
|
||||
|
||||
<!-- Templates -->
|
||||
<DataTemplate x:Key="EmailTemplate" x:DataType="viewModelData:MailItemViewModel">
|
||||
<ItemContainer
|
||||
HorizontalContentAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch"
|
||||
IsSelected="{x:Bind IsSelected, Mode=TwoWay}">
|
||||
<animations:Implicit.ShowAnimations>
|
||||
<animations:OpacityAnimation To="1.0" Duration="0:0:1" />
|
||||
</animations:Implicit.ShowAnimations>
|
||||
|
||||
<animations:Implicit.HideAnimations>
|
||||
<animations:OpacityAnimation To="0.0" Duration="0:0:1" />
|
||||
</animations:Implicit.HideAnimations>
|
||||
|
||||
<Grid Height="80">
|
||||
<!-- Thread background -->
|
||||
<Grid
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Background="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
Visibility="{x:Bind IsDisplayedInThread, Mode=OneWay}" />
|
||||
|
||||
<Grid Padding="8,4,12,4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Left: PersonPicture -->
|
||||
<PersonPicture
|
||||
Grid.Column="0"
|
||||
Width="40"
|
||||
Height="40"
|
||||
Margin="0,0,12,0"
|
||||
VerticalAlignment="Center"
|
||||
DisplayName="{x:Bind FromName, Mode=OneWay}" />
|
||||
|
||||
<!-- Center: Content -->
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<!-- Subject -->
|
||||
<TextBlock
|
||||
FontWeight="{x:Bind helpers:XamlHelpers.GetFontWeightByReadState(IsRead), Mode=OneWay}"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
MaxLines="1"
|
||||
Text="{x:Bind Subject, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
|
||||
<!-- Sender Name -->
|
||||
<TextBlock
|
||||
FontWeight="{x:Bind helpers:XamlHelpers.GetFontWeightByReadState(IsRead), Mode=OneWay}"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
MaxLines="1"
|
||||
Text="{x:Bind FromName, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
|
||||
<!-- Preview Text -->
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorTertiaryBrush}"
|
||||
MaxLines="1"
|
||||
Text="{x:Bind PreviewText, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Right: Indicators -->
|
||||
<StackPanel
|
||||
Grid.Column="2"
|
||||
Margin="12,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="4">
|
||||
|
||||
<!-- Unread Indicator (show when IsRead is false) -->
|
||||
<Ellipse
|
||||
Width="8"
|
||||
Height="8"
|
||||
Fill="{ThemeResource AccentTextFillColorPrimaryBrush}"
|
||||
Visibility="{x:Bind helpers:XamlHelpers.ReverseBoolToVisibilityConverter(IsRead), Mode=OneWay}" />
|
||||
|
||||
<!-- Flagged Indicator -->
|
||||
<FontIcon
|
||||
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||
FontSize="12"
|
||||
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
|
||||
Glyph=""
|
||||
Visibility="{x:Bind IsFlagged, Mode=OneWay}" />
|
||||
|
||||
<!-- Attachment Indicator -->
|
||||
<FontIcon
|
||||
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||
FontSize="12"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Glyph=""
|
||||
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="" />
|
||||
</Viewbox>
|
||||
|
||||
<!-- Thread content -->
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<TextBlock
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
Text="{x:Bind Subject, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{ThemeResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind LatestEmailDate, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Email count badge -->
|
||||
<Border
|
||||
Grid.Column="2"
|
||||
Margin="8,0,12,0"
|
||||
Padding="6,2"
|
||||
VerticalAlignment="Center"
|
||||
Background="{ThemeResource AccentTextFillColorPrimaryBrush}"
|
||||
CornerRadius="8">
|
||||
<TextBlock
|
||||
Foreground="White"
|
||||
Style="{ThemeResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind EmailCount, Mode=OneWay}" />
|
||||
</Border>
|
||||
</Grid>
|
||||
</ItemContainer>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="DateGroupHeaderTemplate" x:DataType="data:DateGroupHeader">
|
||||
<ItemContainer IsHitTestVisible="False">
|
||||
<Grid Padding="12,8" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}">
|
||||
<TextBlock
|
||||
FontSize="14"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
Style="{ThemeResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind DisplayName, Mode=OneWay}" />
|
||||
</Grid>
|
||||
</ItemContainer>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="SenderGroupHeaderTemplate" x:DataType="data:SenderGroupHeader">
|
||||
<ItemContainer IsHitTestVisible="False">
|
||||
<Grid Padding="12,8" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
Style="{ThemeResource SubtitleTextBlockStyle}"
|
||||
Text="{x:Bind DisplayName, Mode=OneWay}" />
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8">
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{ThemeResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind ItemCount, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</ItemContainer>
|
||||
</DataTemplate>
|
||||
|
||||
<selectors1:MailItemContainerSelector
|
||||
x:Name="MailItemContainerSelector"
|
||||
DateGroupHeaderTemplate="{StaticResource DateGroupHeaderTemplate}"
|
||||
EmailTemplate="{StaticResource EmailTemplate}"
|
||||
SenderGroupHeaderTemplate="{StaticResource SenderGroupHeaderTemplate}"
|
||||
ThreadExpanderTemplate="{StaticResource ThreadExpanderTemplate}" />
|
||||
</Page.Resources>
|
||||
|
||||
<Page.KeyboardAccelerators>
|
||||
@@ -303,8 +355,7 @@
|
||||
VerticalAlignment="Center"
|
||||
Canvas.ZIndex="100"
|
||||
Checked="SelectAllCheckboxChecked"
|
||||
Unchecked="SelectAllCheckboxUnchecked"
|
||||
Visibility="{x:Bind helpers:XamlHelpers.IsSelectionModeMultiple(MailListView.SelectionMode), Mode=OneWay}" />
|
||||
Unchecked="SelectAllCheckboxUnchecked" />
|
||||
|
||||
|
||||
<!-- Folders -->
|
||||
@@ -452,70 +503,14 @@
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<RefreshContainer RefreshRequested="PullToRefreshRequested" Visibility="{x:Bind ViewModel.IsEmpty, Converter={StaticResource ReverseBooleanToVisibilityConverter}, Mode=OneWay}">
|
||||
<SemanticZoom x:Name="SemanticZoomContainer" CanChangeViews="{x:Bind ViewModel.PreferencesService.IsSemanticZoomEnabled, Mode=OneWay}">
|
||||
<SemanticZoom.ZoomedInView>
|
||||
<listview:WinoListView
|
||||
x:Name="MailListView"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
toolkitExt:ListViewExtensions.ItemContainerStretchDirection="Horizontal"
|
||||
toolkitExt:ScrollViewerExtensions.VerticalScrollBarMargin="0"
|
||||
ItemDeletedCommand="{x:Bind ViewModel.ExecuteMailOperationCommand}"
|
||||
ItemTemplateSelector="{StaticResource MailItemDisplaySelector}"
|
||||
ItemsSource="{x:Bind MailCollectionViewSource.View, Mode=OneWay}"
|
||||
LoadMoreCommand="{x:Bind ViewModel.LoadMoreItemsCommand}"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Auto">
|
||||
<ListView.ItemContainerTransitions>
|
||||
<TransitionCollection>
|
||||
<AddDeleteThemeTransition />
|
||||
</TransitionCollection>
|
||||
</ListView.ItemContainerTransitions>
|
||||
<ListView.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<ItemsStackPanel Margin="8,0,12,0" AreStickyGroupHeadersEnabled="True" />
|
||||
</ItemsPanelTemplate>
|
||||
</ListView.ItemsPanel>
|
||||
<ListView.Resources>
|
||||
<ResourceDictionary>
|
||||
<Style BasedOn="{StaticResource MailListHeaderStyle}" TargetType="ListViewHeaderItem" />
|
||||
</ResourceDictionary>
|
||||
</ListView.Resources>
|
||||
<ListView.GroupStyle>
|
||||
<GroupStyle HeaderTemplate="{StaticResource MailGroupHeaderDefaultTemplate}" HidesIfEmpty="True" />
|
||||
</ListView.GroupStyle>
|
||||
|
||||
</listview:WinoListView>
|
||||
</SemanticZoom.ZoomedInView>
|
||||
<SemanticZoom.ZoomedOutView>
|
||||
<ListView
|
||||
x:Name="ZoomOutList"
|
||||
x:Load="{x:Bind helpers:XamlHelpers.ReverseBoolConverter(SemanticZoomContainer.IsZoomedInViewActive), Mode=OneWay}"
|
||||
ItemsSource="{x:Bind MailCollectionViewSource.View.CollectionGroups}">
|
||||
<ListView.Resources>
|
||||
<Style TargetType="ListViewItem">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="Margin" Value="12" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
</Style>
|
||||
</ListView.Resources>
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="ICollectionViewGroup">
|
||||
<Grid Background="{ThemeResource MailListHeaderBackgroundColor}" CornerRadius="6">
|
||||
<TextBlock
|
||||
Margin="12,0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontWeight="SemiBold"
|
||||
Text="{x:Bind helpers:XamlHelpers.GetMailGroupDateString(Group)}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</SemanticZoom.ZoomedOutView>
|
||||
</SemanticZoom>
|
||||
</RefreshContainer>
|
||||
<advanced:WinoItemsView
|
||||
x:Name="MailListView"
|
||||
Margin="4"
|
||||
ItemTemplate="{StaticResource MailItemContainerSelector}"
|
||||
ItemsSource="{x:Bind ViewModel.MailCollection.Items, Mode=OneTime}"
|
||||
Layout="{StaticResource DefaultItemsViewLayout}"
|
||||
SelectionChanged="ListSelectionChanged"
|
||||
SelectionMode="Extended" />
|
||||
|
||||
<!-- Try online search panel. -->
|
||||
<Grid Grid.Row="1" Visibility="{x:Bind ViewModel.IsOnlineSearchButtonVisible, Mode=OneWay}">
|
||||
|
||||
@@ -7,19 +7,19 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Hosting;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media.Animation;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using MoreLinq;
|
||||
using Windows.Foundation;
|
||||
using Wino.Controls;
|
||||
using Wino.Controls.Advanced;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models.Menus;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Helpers;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
using Wino.Mail.ViewModels.Messages;
|
||||
using Wino.Mail.WinUI;
|
||||
@@ -91,7 +91,7 @@ public sealed partial class MailListPage : MailListPageAbstract,
|
||||
SelectAllCheckbox.Checked -= SelectAllCheckboxChecked;
|
||||
SelectAllCheckbox.Unchecked -= SelectAllCheckboxUnchecked;
|
||||
|
||||
SelectAllCheckbox.IsChecked = MailListView.Items.Count > 0 && MailListView.SelectedItems.Count == MailListView.Items.Count;
|
||||
SelectAllCheckbox.IsChecked = MailListView.CastedItemsSource?.Count() > 0 && MailListView.SelectedItems.Count == MailListView.CastedItemsSource.Count();
|
||||
|
||||
SelectAllCheckbox.Checked += SelectAllCheckboxChecked;
|
||||
SelectAllCheckbox.Unchecked += SelectAllCheckboxUnchecked;
|
||||
@@ -120,10 +120,10 @@ public sealed partial class MailListPage : MailListPageAbstract,
|
||||
}
|
||||
}
|
||||
|
||||
SelectAllCheckbox.IsChecked = false;
|
||||
// SelectAllCheckbox.IsChecked = false;
|
||||
SelectionModeToggle.IsChecked = false;
|
||||
|
||||
MailListView.ClearSelections();
|
||||
// MailListView.ClearSelections();
|
||||
|
||||
UpdateSelectAllButtonStatus();
|
||||
ViewModel.SelectedPivotChangedCommand.Execute(null);
|
||||
@@ -131,7 +131,8 @@ public sealed partial class MailListPage : MailListPageAbstract,
|
||||
|
||||
private void ChangeSelectionMode(ListViewSelectionMode mode)
|
||||
{
|
||||
MailListView.ChangeSelectionMode(mode);
|
||||
// ItemsView doesn't have a ChangeSelectionMode method like ListView
|
||||
// The selection mode is set in XAML and doesn't need to change dynamically for ItemsView
|
||||
|
||||
if (ViewModel?.PivotFolders != null)
|
||||
{
|
||||
@@ -146,44 +147,46 @@ public sealed partial class MailListPage : MailListPageAbstract,
|
||||
|
||||
private void SelectAllCheckboxChecked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
MailListView.SelectAllWino();
|
||||
// MailListView.SelectAllWino();
|
||||
}
|
||||
|
||||
private void SelectAllCheckboxUnchecked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
MailListView.ClearSelections();
|
||||
// MailListView.ClearSelections();
|
||||
}
|
||||
|
||||
private async void MailItemContextRequested(UIElement sender, ContextRequestedEventArgs args)
|
||||
{
|
||||
// TODO: New ItemsView implementation.
|
||||
|
||||
// Context is requested from a single mail point, but we might have multiple selected items.
|
||||
// This menu should be calculated based on all selected items by providers.
|
||||
|
||||
if (sender is MailItemDisplayInformationControl control && args.TryGetPosition(sender, out Point p))
|
||||
{
|
||||
await FocusManager.TryFocusAsync(control, FocusState.Keyboard);
|
||||
//if (sender is MailItemDisplayInformationControl control && args.TryGetPosition(sender, out Point p))
|
||||
//{
|
||||
// await FocusManager.TryFocusAsync(control, FocusState.Keyboard);
|
||||
|
||||
if (control.DataContext is IMailItem clickedMailItemContext)
|
||||
{
|
||||
var targetItems = ViewModel.GetTargetMailItemViewModels(clickedMailItemContext);
|
||||
var availableActions = ViewModel.GetAvailableMailActions(targetItems);
|
||||
// if (control.DataContext is IMailItem clickedMailItemContext)
|
||||
// {
|
||||
// var targetItems = ViewModel.GetTargetMailItemViewModels(clickedMailItemContext);
|
||||
// var availableActions = ViewModel.GetAvailableMailActions(targetItems);
|
||||
|
||||
if (!availableActions?.Any() ?? false) return;
|
||||
var t = targetItems.ElementAt(0);
|
||||
// if (!availableActions?.Any() ?? false) return;
|
||||
// var t = targetItems.ElementAt(0);
|
||||
|
||||
ViewModel.ChangeCustomFocusedState(targetItems, true);
|
||||
// ViewModel.ChangeCustomFocusedState(targetItems, true);
|
||||
|
||||
var clickedOperation = await GetMailOperationFromFlyoutAsync(availableActions, control, p.X, p.Y);
|
||||
// var clickedOperation = await GetMailOperationFromFlyoutAsync(availableActions, control, p.X, p.Y);
|
||||
|
||||
ViewModel.ChangeCustomFocusedState(targetItems, false);
|
||||
// ViewModel.ChangeCustomFocusedState(targetItems, false);
|
||||
|
||||
if (clickedOperation == null) return;
|
||||
// if (clickedOperation == null) return;
|
||||
|
||||
var prepRequest = new MailOperationPreperationRequest(clickedOperation.Operation, targetItems.Select(a => a.MailCopy));
|
||||
// var prepRequest = new MailOperationPreperationRequest(clickedOperation.Operation, targetItems.Select(a => a.MailCopy));
|
||||
|
||||
await ViewModel.ExecuteMailOperationAsync(prepRequest);
|
||||
}
|
||||
}
|
||||
// await ViewModel.ExecuteMailOperationAsync(prepRequest);
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
||||
private async Task<MailOperationMenuItem> GetMailOperationFromFlyoutAsync(IEnumerable<MailOperationMenuItem> availableActions,
|
||||
@@ -206,7 +209,7 @@ public sealed partial class MailListPage : MailListPageAbstract,
|
||||
|
||||
void IRecipient<ClearMailSelectionsRequested>.Receive(ClearMailSelectionsRequested message)
|
||||
{
|
||||
MailListView.ClearSelections(null, preserveThreadExpanding: true);
|
||||
// MailListView.ClearSelections(null, preserveThreadExpanding: true);
|
||||
}
|
||||
|
||||
void IRecipient<ActiveMailItemChangedEvent>.Receive(ActiveMailItemChangedEvent message)
|
||||
@@ -309,46 +312,46 @@ public sealed partial class MailListPage : MailListPageAbstract,
|
||||
{
|
||||
if (message.SelectedMailViewModel == null) return;
|
||||
|
||||
await ViewModel.ExecuteUIThread(async () =>
|
||||
{
|
||||
MailListView.ClearSelections(message.SelectedMailViewModel, true);
|
||||
//await ViewModel.ExecuteUIThread(async () =>
|
||||
//{
|
||||
// MailListView.ClearSelections(message.SelectedMailViewModel, true);
|
||||
|
||||
int retriedSelectionCount = 0;
|
||||
trySelection:
|
||||
// int retriedSelectionCount = 0;
|
||||
//trySelection:
|
||||
|
||||
bool isSelected = MailListView.SelectMailItemContainer(message.SelectedMailViewModel);
|
||||
// bool isSelected = MailListView.SelectMailItemContainer(message.SelectedMailViewModel);
|
||||
|
||||
if (!isSelected)
|
||||
{
|
||||
for (int i = retriedSelectionCount; i < 5;)
|
||||
{
|
||||
// Retry with delay until the container is realized. Max 1 second.
|
||||
await Task.Delay(200);
|
||||
// if (!isSelected)
|
||||
// {
|
||||
// for (int i = retriedSelectionCount; i < 5;)
|
||||
// {
|
||||
// // Retry with delay until the container is realized. Max 1 second.
|
||||
// await Task.Delay(200);
|
||||
|
||||
retriedSelectionCount++;
|
||||
// retriedSelectionCount++;
|
||||
|
||||
goto trySelection;
|
||||
}
|
||||
}
|
||||
// goto trySelection;
|
||||
// }
|
||||
// }
|
||||
|
||||
// Automatically scroll to the selected item.
|
||||
// This is useful when creating draft.
|
||||
if (isSelected && message.ScrollToItem)
|
||||
{
|
||||
var collectionContainer = ViewModel.MailCollection.GetMailItemContainer(message.SelectedMailViewModel.UniqueId);
|
||||
// // Automatically scroll to the selected item.
|
||||
// // This is useful when creating draft.
|
||||
// if (isSelected && message.ScrollToItem)
|
||||
// {
|
||||
// var collectionContainer = ViewModel.MailCollection.GetMailItemContainer(message.SelectedMailViewModel.UniqueId);
|
||||
|
||||
// Scroll to thread if available.
|
||||
if (collectionContainer.ThreadViewModel != null)
|
||||
{
|
||||
MailListView.ScrollIntoView(collectionContainer.ThreadViewModel, ScrollIntoViewAlignment.Default);
|
||||
}
|
||||
else if (collectionContainer.ItemViewModel != null)
|
||||
{
|
||||
MailListView.ScrollIntoView(collectionContainer.ItemViewModel, ScrollIntoViewAlignment.Default);
|
||||
}
|
||||
// // Scroll to thread if available.
|
||||
// if (collectionContainer.ThreadViewModel != null)
|
||||
// {
|
||||
// MailListView.StartBringItemIntoView(collectionContainer.ThreadViewModel, new BringIntoViewOptions());
|
||||
// }
|
||||
// else if (collectionContainer.ItemViewModel != null)
|
||||
// {
|
||||
// MailListView.StartBringItemIntoView(collectionContainer.ItemViewModel, new BringIntoViewOptions());
|
||||
// }
|
||||
|
||||
}
|
||||
});
|
||||
// }
|
||||
//});
|
||||
}
|
||||
|
||||
private void SearchBoxFocused(object sender, RoutedEventArgs e)
|
||||
@@ -367,32 +370,29 @@ public sealed partial class MailListPage : MailListPageAbstract,
|
||||
/// </summary>
|
||||
private void ThreadHeaderDragStart(UIElement sender, DragStartingEventArgs args)
|
||||
{
|
||||
if (sender is MailItemDisplayInformationControl control
|
||||
&& control.ConnectedExpander?.Content is WinoListView contentListView)
|
||||
{
|
||||
var allItems = contentListView.Items.Where(a => a is IMailItem);
|
||||
//if (sender is MailItemDisplayInformationControl control
|
||||
// && control.ConnectedExpander?.Content is WinoListView contentListView)
|
||||
//{
|
||||
// var allItems = contentListView.Items.Where(a => a is MailCopy);
|
||||
|
||||
// Highlight all items.
|
||||
allItems.Cast<MailItemViewModel>().ForEach(a => a.IsCustomFocused = true);
|
||||
// // Highlight all items.
|
||||
// allItems.Cast<MailItemViewModel>().ForEach(a => a.IsCustomFocused = true);
|
||||
|
||||
// Set native drag arg properties.
|
||||
args.AllowedOperations = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move;
|
||||
// // Set native drag arg properties.
|
||||
// args.AllowedOperations = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move;
|
||||
|
||||
var dragPackage = new MailDragPackage(allItems.Cast<IMailItem>());
|
||||
// var dragPackage = new MailDragPackage(allItems.Cast<MailCopy>());
|
||||
|
||||
args.Data.Properties.Add(nameof(MailDragPackage), dragPackage);
|
||||
args.DragUI.SetContentFromDataPackage();
|
||||
// args.Data.Properties.Add(nameof(MailDragPackage), dragPackage);
|
||||
// args.DragUI.SetContentFromDataPackage();
|
||||
|
||||
control.ConnectedExpander.IsExpanded = true;
|
||||
}
|
||||
// control.ConnectedExpander.IsExpanded = true;
|
||||
//}
|
||||
}
|
||||
|
||||
private void ThreadHeaderDragFinished(UIElement sender, DropCompletedEventArgs args)
|
||||
{
|
||||
if (sender is MailItemDisplayInformationControl control && control.ConnectedExpander != null && control.ConnectedExpander.Content is WinoListView contentListView)
|
||||
{
|
||||
contentListView.Items.Where(a => a is MailItemViewModel).Cast<MailItemViewModel>().ForEach(a => a.IsCustomFocused = false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private async void LeftSwipeItemInvoked(Microsoft.UI.Xaml.Controls.SwipeItem sender, Microsoft.UI.Xaml.Controls.SwipeItemInvokedEventArgs args)
|
||||
@@ -410,7 +410,7 @@ public sealed partial class MailListPage : MailListPageAbstract,
|
||||
}
|
||||
else if (swipeControl.Tag is ThreadMailItemViewModel threadMailItemViewModel)
|
||||
{
|
||||
var package = new MailOperationPreperationRequest(MailOperation.SoftDelete, threadMailItemViewModel.GetMailCopies());
|
||||
var package = new MailOperationPreperationRequest(MailOperation.SoftDelete, threadMailItemViewModel.ThreadEmails.Select(a => a.MailCopy));
|
||||
await ViewModel.ExecuteMailOperationAsync(package);
|
||||
}
|
||||
}
|
||||
@@ -432,19 +432,15 @@ public sealed partial class MailListPage : MailListPageAbstract,
|
||||
}
|
||||
else if (swipeControl.Tag is ThreadMailItemViewModel threadMailItemViewModel)
|
||||
{
|
||||
bool isAllRead = threadMailItemViewModel.ThreadItems.All(a => a.IsRead);
|
||||
bool isAllRead = threadMailItemViewModel.ThreadEmails.All(a => a.IsRead);
|
||||
|
||||
var operation = isAllRead ? MailOperation.MarkAsUnread : MailOperation.MarkAsRead;
|
||||
var package = new MailOperationPreperationRequest(operation, threadMailItemViewModel.GetMailCopies());
|
||||
var package = new MailOperationPreperationRequest(operation, threadMailItemViewModel.ThreadEmails.Select(a => a.MailCopy));
|
||||
|
||||
await ViewModel.ExecuteMailOperationAsync(package);
|
||||
}
|
||||
}
|
||||
|
||||
private void PullToRefreshRequested(Microsoft.UI.Xaml.Controls.RefreshContainer sender, Microsoft.UI.Xaml.Controls.RefreshRequestedEventArgs args)
|
||||
{
|
||||
ViewModel.SyncFolderCommand?.Execute(null);
|
||||
}
|
||||
|
||||
private async void SearchBar_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
|
||||
{
|
||||
@@ -504,8 +500,65 @@ public sealed partial class MailListPage : MailListPageAbstract,
|
||||
}
|
||||
|
||||
private void SelectAllInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
|
||||
=> MailListView.SelectAllWino();
|
||||
{
|
||||
// MailListView.SelectAllWino();
|
||||
}
|
||||
|
||||
private void DeleteAllInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
|
||||
=> ViewModel.ExecuteMailOperationCommand.Execute(MailOperation.SoftDelete);
|
||||
|
||||
private void ThreadContainerTapped(object sender, TappedRoutedEventArgs e)
|
||||
{
|
||||
if (sender is ItemContainer container && container.Tag is ThreadMailItemViewModel expander)
|
||||
{
|
||||
// Toggle expansion state
|
||||
expander.IsThreadExpanded = !expander.IsThreadExpanded;
|
||||
|
||||
// Find the expander icon and animate its rotation using Composition APIs
|
||||
var expanderIcon = WinoVisualTreeHelper.GetChildObject<FontIcon>(container, "ExpanderIcon");
|
||||
if (expanderIcon != null)
|
||||
{
|
||||
var targetAngle = expander.IsThreadExpanded ? 90f : 0f;
|
||||
AnimateRotationWithComposition(expanderIcon, targetAngle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Animates the rotation using high-performance Composition APIs
|
||||
/// </summary>
|
||||
private void AnimateRotationWithComposition(FrameworkElement element, float targetAngleInDegrees)
|
||||
{
|
||||
// Get the element's visual from the composition layer
|
||||
var visual = ElementCompositionPreview.GetElementVisual(element);
|
||||
var compositor = visual.Compositor;
|
||||
|
||||
// Set the center point for rotation (center of the element)
|
||||
visual.CenterPoint = new System.Numerics.Vector3(
|
||||
(float)element.ActualWidth / 2f,
|
||||
(float)element.ActualHeight / 2f,
|
||||
0f);
|
||||
|
||||
// Create a rotation animation
|
||||
var rotationAnimation = compositor.CreateScalarKeyFrameAnimation();
|
||||
rotationAnimation.Target = "RotationAngleInDegrees";
|
||||
rotationAnimation.Duration = TimeSpan.FromMilliseconds(200);
|
||||
|
||||
// Add easing function for smooth animation
|
||||
var easingFunction = compositor.CreateCubicBezierEasingFunction(
|
||||
new System.Numerics.Vector2(0.25f, 0.1f), // Control point 1
|
||||
new System.Numerics.Vector2(0.25f, 1f)); // Control point 2 (similar to CircleEase)
|
||||
|
||||
// Insert keyframe with the target angle and easing
|
||||
rotationAnimation.InsertKeyFrame(1.0f, targetAngleInDegrees, easingFunction);
|
||||
|
||||
// Start the animation
|
||||
visual.StartAnimation("RotationAngleInDegrees", rotationAnimation);
|
||||
}
|
||||
|
||||
private void ListSelectionChanged(ItemsView sender, ItemsViewSelectionChangedEventArgs args)
|
||||
{
|
||||
UpdateSelectAllButtonStatus();
|
||||
UpdateAdaptiveness();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
|
||||
namespace Wino.Messaging.Client.Accounts;
|
||||
|
||||
@@ -10,4 +10,4 @@ namespace Wino.Messaging.Client.Accounts;
|
||||
/// <param name="AutoSelectAccount">Account to extend menu item for.</param>
|
||||
/// <param name="FolderId">Folder to select after expansion.</param>
|
||||
/// <param name="NavigateMailItem">Mail item to select if possible in the expanded folder.</param>
|
||||
public record AccountMenuItemExtended(Guid FolderId, IMailItem NavigateMailItem);
|
||||
public record AccountMenuItemExtended(Guid FolderId, MailCopy NavigateMailItem);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
|
||||
namespace Wino.Messaging.Server;
|
||||
|
||||
@@ -10,4 +10,4 @@ namespace Wino.Messaging.Server;
|
||||
/// </summary>
|
||||
/// <param name="AccountId">Account id for corresponding synchronizer.</param>
|
||||
/// <param name="MailCopyId">Mail copy id to download.</param>
|
||||
public record DownloadMissingMessageRequested(Guid AccountId, IMailItem MailItem) : IClientMessage;
|
||||
public record DownloadMissingMessageRequested(Guid AccountId, MailCopy MailItem) : IClientMessage;
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Text.Json" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Client\Authorization\" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models.Menus;
|
||||
|
||||
namespace Wino.Services;
|
||||
@@ -35,7 +35,7 @@ public class ContextMenuItemService : IContextMenuItemService
|
||||
|
||||
return list;
|
||||
}
|
||||
public virtual IEnumerable<MailOperationMenuItem> GetMailItemContextMenuActions(IEnumerable<IMailItem> selectedMailItems)
|
||||
public virtual IEnumerable<MailOperationMenuItem> GetMailItemContextMenuActions(IEnumerable<MailCopy> selectedMailItems)
|
||||
{
|
||||
if (selectedMailItems == null)
|
||||
return default;
|
||||
@@ -50,7 +50,7 @@ public class ContextMenuItemService : IContextMenuItemService
|
||||
|
||||
bool isSingleItem = selectedMailItems.Count() == 1;
|
||||
|
||||
IMailItem singleItem = selectedMailItems.FirstOrDefault();
|
||||
MailCopy singleItem = selectedMailItems.FirstOrDefault();
|
||||
|
||||
// Archive button.
|
||||
|
||||
@@ -125,7 +125,7 @@ public class ContextMenuItemService : IContextMenuItemService
|
||||
|
||||
return operationList;
|
||||
}
|
||||
public virtual IEnumerable<MailOperationMenuItem> GetMailItemRenderMenuActions(IMailItem mailItem, bool isDarkEditor)
|
||||
public virtual IEnumerable<MailOperationMenuItem> GetMailItemRenderMenuActions(MailCopy mailItem, bool isDarkEditor)
|
||||
{
|
||||
var actionList = new List<MailOperationMenuItem>();
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Exceptions;
|
||||
using Wino.Core.Domain.Extensions;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Comparers;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Messaging.UI;
|
||||
using Wino.Services.Extensions;
|
||||
@@ -29,7 +28,6 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
private readonly IContactService _contactService;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly ISignatureService _signatureService;
|
||||
private readonly IThreadingStrategyProvider _threadingStrategyProvider;
|
||||
private readonly IMimeFileService _mimeFileService;
|
||||
private readonly IPreferencesService _preferencesService;
|
||||
|
||||
@@ -40,7 +38,6 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
IContactService contactService,
|
||||
IAccountService accountService,
|
||||
ISignatureService signatureService,
|
||||
IThreadingStrategyProvider threadingStrategyProvider,
|
||||
IMimeFileService mimeFileService,
|
||||
IPreferencesService preferencesService) : base(databaseService)
|
||||
{
|
||||
@@ -48,7 +45,6 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
_contactService = contactService;
|
||||
_accountService = accountService;
|
||||
_signatureService = signatureService;
|
||||
_threadingStrategyProvider = threadingStrategyProvider;
|
||||
_mimeFileService = mimeFileService;
|
||||
_preferencesService = preferencesService;
|
||||
}
|
||||
@@ -209,7 +205,7 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
return query.GetRawQuery();
|
||||
}
|
||||
|
||||
public async Task<List<IMailItem>> FetchMailsAsync(MailListInitializationOptions options, CancellationToken cancellationToken = default)
|
||||
public async Task<List<MailCopy>> FetchMailsAsync(MailListInitializationOptions options, CancellationToken cancellationToken = default)
|
||||
{
|
||||
List<MailCopy> mails = null;
|
||||
|
||||
@@ -241,68 +237,20 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
// Remove items that has no assigned account or folder.
|
||||
mails.RemoveAll(a => a.AssignedAccount == null || a.AssignedFolder == null);
|
||||
|
||||
if (!options.CreateThreads)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Threading is disabled. Just return everything as it is.
|
||||
mails.Sort(options.SortingOptionType == SortingOptionType.ReceiveDate ? new DateComparer() : new NameComparer());
|
||||
|
||||
return [.. mails];
|
||||
}
|
||||
|
||||
// Populate threaded items.
|
||||
|
||||
List<IMailItem> threadedItems = [];
|
||||
|
||||
// Each account items must be threaded separately.
|
||||
foreach (var group in mails.GroupBy(a => a.AssignedAccount.Id))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var accountId = group.Key;
|
||||
var groupAccount = mails.First(a => a.AssignedAccount.Id == accountId).AssignedAccount;
|
||||
|
||||
var threadingStrategy = _threadingStrategyProvider.GetStrategy(groupAccount.ProviderType);
|
||||
|
||||
// Only thread items from Draft and Sent folders must present here.
|
||||
// Otherwise this strategy will fetch the items that are in Deleted folder as well.
|
||||
var accountThreadedItems = await threadingStrategy.ThreadItemsAsync([.. group], options.Folders.First());
|
||||
|
||||
// Populate threaded items with folder and account assignments.
|
||||
// Almost everything already should be in cache from initial population.
|
||||
foreach (var mail in accountThreadedItems)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
await LoadAssignedPropertiesWithCacheAsync(mail, folderCache, accountCache, contactCache).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (accountThreadedItems != null)
|
||||
{
|
||||
threadedItems.AddRange(accountThreadedItems);
|
||||
}
|
||||
}
|
||||
|
||||
threadedItems.Sort(options.SortingOptionType == SortingOptionType.ReceiveDate ? new DateComparer() : new NameComparer());
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
return threadedItems;
|
||||
// Threading is disabled. Just return everything as it is.
|
||||
// mails.Sort(options.SortingOptionType == SortingOptionType.ReceiveDate ? new DateComparer() : new NameComparer());
|
||||
|
||||
return [.. mails];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method should used for operations with multiple mailItems. Don't use this for single mail items.
|
||||
/// Called method should provide own instances for caches.
|
||||
/// </summary>
|
||||
private async Task LoadAssignedPropertiesWithCacheAsync(IMailItem mail, Dictionary<Guid, MailItemFolder> folderCache, Dictionary<Guid, MailAccount> accountCache, Dictionary<string, AccountContact> contactCache)
|
||||
private async Task LoadAssignedPropertiesWithCacheAsync(MailCopy mail, Dictionary<Guid, MailItemFolder> folderCache, Dictionary<Guid, MailAccount> accountCache, Dictionary<string, AccountContact> contactCache)
|
||||
{
|
||||
if (mail is ThreadMailItem threadMailItem)
|
||||
{
|
||||
foreach (var childMail in threadMailItem.ThreadItems)
|
||||
{
|
||||
await LoadAssignedPropertiesWithCacheAsync(childMail, folderCache, accountCache, contactCache).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (mail is MailCopy mailCopy)
|
||||
{
|
||||
var isFolderCached = folderCache.TryGetValue(mailCopy.FolderId, out MailItemFolder folderAssignment);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Services.Threading;
|
||||
|
||||
namespace Wino.Services;
|
||||
|
||||
@@ -24,12 +23,5 @@ public static class ServicesContainerSetup
|
||||
services.AddTransient<ISignatureService, SignatureService>();
|
||||
services.AddTransient<IContextMenuItemService, ContextMenuItemService>();
|
||||
services.AddTransient<ISpecialImapProviderConfigResolver, SpecialImapProviderConfigResolver>();
|
||||
|
||||
services.AddSingleton<IThreadingStrategyProvider, ThreadingStrategyProvider>();
|
||||
services.AddTransient<IOutlookThreadingStrategy, OutlookThreadingStrategy>();
|
||||
services.AddTransient<IGmailThreadingStrategy, GmailThreadingStrategy>();
|
||||
services.AddTransient<IImapThreadingStrategy, ImapThreadingStrategy>();
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Services;
|
||||
|
||||
namespace Wino.Services.Threading;
|
||||
|
||||
public class APIThreadingStrategy : IThreadingStrategy
|
||||
{
|
||||
private readonly IDatabaseService _databaseService;
|
||||
private readonly IFolderService _folderService;
|
||||
|
||||
public APIThreadingStrategy(IDatabaseService databaseService, IFolderService folderService)
|
||||
{
|
||||
_databaseService = databaseService;
|
||||
_folderService = folderService;
|
||||
}
|
||||
|
||||
public virtual bool ShouldThreadWithItem(IMailItem originalItem, IMailItem targetItem)
|
||||
{
|
||||
return originalItem.ThreadId != null && originalItem.ThreadId == targetItem.ThreadId;
|
||||
}
|
||||
|
||||
///<inheritdoc/>
|
||||
public async Task<List<IMailItem>> ThreadItemsAsync(List<MailCopy> items, IMailItemFolder threadingForFolder)
|
||||
{
|
||||
var assignedAccount = items[0].AssignedAccount;
|
||||
|
||||
var sentFolder = await _folderService.GetSpecialFolderByAccountIdAsync(assignedAccount.Id, SpecialFolderType.Sent);
|
||||
var draftFolder = await _folderService.GetSpecialFolderByAccountIdAsync(assignedAccount.Id, SpecialFolderType.Draft);
|
||||
|
||||
if (sentFolder == null || draftFolder == null) return default;
|
||||
|
||||
// True: Non threaded items.
|
||||
// False: Potentially threaded items.
|
||||
var nonThreadedOrThreadedMails = items
|
||||
.Distinct()
|
||||
.GroupBy(x => string.IsNullOrEmpty(x.ThreadId))
|
||||
.ToDictionary(x => x.Key, x => x);
|
||||
|
||||
_ = nonThreadedOrThreadedMails.TryGetValue(true, out var nonThreadedMails);
|
||||
var isThreadedItems = nonThreadedOrThreadedMails.TryGetValue(false, out var potentiallyThreadedMails);
|
||||
|
||||
List<IMailItem> resultList = nonThreadedMails is null ? [] : [.. nonThreadedMails];
|
||||
|
||||
if (isThreadedItems)
|
||||
{
|
||||
var threadItems = (await GetThreadItemsAsync(potentiallyThreadedMails.Select(x => (x.ThreadId, x.AssignedFolder)).ToList(), assignedAccount.Id, sentFolder.Id, draftFolder.Id))
|
||||
.GroupBy(x => x.ThreadId);
|
||||
|
||||
foreach (var threadItem in threadItems)
|
||||
{
|
||||
if (threadItem.Count() == 1)
|
||||
{
|
||||
resultList.Add(threadItem.First());
|
||||
continue;
|
||||
}
|
||||
|
||||
var thread = new ThreadMailItem();
|
||||
|
||||
foreach (var childThreadItem in threadItem)
|
||||
{
|
||||
if (thread.ThreadItems.Any(a => a.Id == childThreadItem.Id))
|
||||
{
|
||||
// Mail already exist in the thread.
|
||||
// There should be only 1 instance of the mail in the thread.
|
||||
// Make sure we add the correct one.
|
||||
|
||||
// Add the one with threading folder.
|
||||
var threadingFolderItem = threadItem.FirstOrDefault(a => a.Id == childThreadItem.Id && a.FolderId == threadingForFolder.Id);
|
||||
|
||||
if (threadingFolderItem == null) continue;
|
||||
|
||||
// Remove the existing one.
|
||||
thread.ThreadItems.Remove(thread.ThreadItems.First(a => a.Id == childThreadItem.Id));
|
||||
|
||||
// Add the correct one for listing.
|
||||
thread.AddThreadItem(threadingFolderItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
thread.AddThreadItem(childThreadItem);
|
||||
}
|
||||
}
|
||||
|
||||
if (thread.ThreadItems.Count > 1)
|
||||
{
|
||||
resultList.Add(thread);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Don't make threads if the thread has only one item.
|
||||
// Gmail has may have multiple assignments for the same item.
|
||||
|
||||
resultList.Add(thread.ThreadItems.First());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resultList;
|
||||
}
|
||||
|
||||
private async Task<List<MailCopy>> GetThreadItemsAsync(List<(string threadId, MailItemFolder threadingFolder)> potentialThread,
|
||||
Guid accountId,
|
||||
Guid sentFolderId,
|
||||
Guid draftFolderId)
|
||||
{
|
||||
// Only items from the folder that we are threading for, sent and draft folder items must be included.
|
||||
// This is important because deleted items or item assignments that belongs to different folder is
|
||||
// affecting the thread creation here.
|
||||
|
||||
// If the threading is done from Sent or Draft folder, include everything...
|
||||
|
||||
// TODO: Convert to SQLKata query.
|
||||
|
||||
var query = @$"SELECT DISTINCT MC.* FROM MailCopy MC
|
||||
INNER JOIN MailItemFolder MF on MF.Id = MC.FolderId
|
||||
WHERE MF.MailAccountId == '{accountId}' AND
|
||||
({string.Join(" OR ", potentialThread.Select(x => ConditionForItem(x, sentFolderId, draftFolderId)))})";
|
||||
|
||||
return await _databaseService.Connection.QueryAsync<MailCopy>(query);
|
||||
|
||||
static string ConditionForItem((string threadId, MailItemFolder threadingFolder) potentialThread, Guid sentFolderId, Guid draftFolderId)
|
||||
{
|
||||
if (potentialThread.threadingFolder.SpecialFolderType == SpecialFolderType.Draft || potentialThread.threadingFolder.SpecialFolderType == SpecialFolderType.Sent)
|
||||
return $"(MC.ThreadId = '{potentialThread.threadId}')";
|
||||
|
||||
return $"(MC.ThreadId = '{potentialThread.threadId}' AND MC.FolderId IN ('{potentialThread.threadingFolder.Id}','{sentFolderId}','{draftFolderId}'))";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Services.Threading;
|
||||
|
||||
public class GmailThreadingStrategy : APIThreadingStrategy, IGmailThreadingStrategy
|
||||
{
|
||||
public GmailThreadingStrategy(IDatabaseService databaseService, IFolderService folderService) : base(databaseService, folderService) { }
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using SqlKata;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Services.Extensions;
|
||||
|
||||
namespace Wino.Services.Threading;
|
||||
|
||||
public class ImapThreadingStrategy : BaseDatabaseService, IImapThreadingStrategy
|
||||
{
|
||||
private readonly IFolderService _folderService;
|
||||
|
||||
public ImapThreadingStrategy(IDatabaseService databaseService, IFolderService folderService) : base(databaseService)
|
||||
{
|
||||
_folderService = folderService;
|
||||
}
|
||||
|
||||
private Task<MailCopy> GetReplyParentAsync(IMailItem replyItem, Guid accountId, Guid threadingFolderId, Guid sentFolderId, Guid draftFolderId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(replyItem?.MessageId)) return Task.FromResult<MailCopy>(null);
|
||||
|
||||
var query = new Query("MailCopy")
|
||||
.Distinct()
|
||||
.Take(1)
|
||||
.Join("MailItemFolder", "MailItemFolder.Id", "MailCopy.FolderId")
|
||||
.Where("MailItemFolder.MailAccountId", accountId)
|
||||
.WhereIn("MailItemFolder.Id", new List<Guid> { threadingFolderId, sentFolderId, draftFolderId })
|
||||
.Where("MailCopy.MessageId", replyItem.InReplyTo)
|
||||
.WhereNot("MailCopy.Id", replyItem.Id)
|
||||
.Select("MailCopy.*");
|
||||
|
||||
return Connection.FindWithQueryAsync<MailCopy>(query.GetRawQuery());
|
||||
}
|
||||
|
||||
private Task<MailCopy> GetInReplyToReplyAsync(IMailItem originalItem, Guid accountId, Guid threadingFolderId, Guid sentFolderId, Guid draftFolderId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(originalItem?.MessageId)) return Task.FromResult<MailCopy>(null);
|
||||
|
||||
var query = new Query("MailCopy")
|
||||
.Distinct()
|
||||
.Take(1)
|
||||
.Join("MailItemFolder", "MailItemFolder.Id", "MailCopy.FolderId")
|
||||
.WhereNot("MailCopy.Id", originalItem.Id)
|
||||
.Where("MailItemFolder.MailAccountId", accountId)
|
||||
.Where("MailCopy.InReplyTo", originalItem.MessageId)
|
||||
.WhereIn("MailItemFolder.Id", new List<Guid> { threadingFolderId, sentFolderId, draftFolderId })
|
||||
.Select("MailCopy.*");
|
||||
|
||||
var raq = query.GetRawQuery();
|
||||
|
||||
return Connection.FindWithQueryAsync<MailCopy>(query.GetRawQuery());
|
||||
}
|
||||
|
||||
public async Task<List<IMailItem>> ThreadItemsAsync(List<MailCopy> items, IMailItemFolder threadingForFolder)
|
||||
{
|
||||
var threads = new List<ThreadMailItem>();
|
||||
|
||||
var account = items.First().AssignedAccount;
|
||||
var accountId = account.Id;
|
||||
|
||||
// Child -> Parent approach.
|
||||
|
||||
var mailLookupTable = new Dictionary<string, bool>();
|
||||
|
||||
// Fill up the mail lookup table to prevent double thread creation.
|
||||
foreach (var mail in items)
|
||||
if (!mailLookupTable.ContainsKey(mail.Id))
|
||||
mailLookupTable.Add(mail.Id, false);
|
||||
|
||||
var sentFolder = await _folderService.GetSpecialFolderByAccountIdAsync(accountId, SpecialFolderType.Sent);
|
||||
var draftFolder = await _folderService.GetSpecialFolderByAccountIdAsync(accountId, SpecialFolderType.Draft);
|
||||
|
||||
// Threading is not possible. Return items as it is.
|
||||
|
||||
if (sentFolder == null || draftFolder == null) return new List<IMailItem>(items);
|
||||
|
||||
foreach (var replyItem in items)
|
||||
{
|
||||
if (mailLookupTable[replyItem.Id])
|
||||
continue;
|
||||
|
||||
mailLookupTable[replyItem.Id] = true;
|
||||
|
||||
var threadItem = new ThreadMailItem();
|
||||
|
||||
threadItem.AddThreadItem(replyItem);
|
||||
|
||||
var replyToChild = await GetReplyParentAsync(replyItem, accountId, replyItem.AssignedFolder.Id, sentFolder.Id, draftFolder.Id);
|
||||
|
||||
// Build up
|
||||
while (replyToChild != null)
|
||||
{
|
||||
replyToChild.AssignedAccount = account;
|
||||
|
||||
if (replyToChild.FolderId == draftFolder.Id)
|
||||
replyToChild.AssignedFolder = draftFolder;
|
||||
|
||||
if (replyToChild.FolderId == sentFolder.Id)
|
||||
replyToChild.AssignedFolder = sentFolder;
|
||||
|
||||
if (replyToChild.FolderId == replyItem.AssignedFolder.Id)
|
||||
replyToChild.AssignedFolder = replyItem.AssignedFolder;
|
||||
|
||||
threadItem.AddThreadItem(replyToChild);
|
||||
|
||||
if (mailLookupTable.ContainsKey(replyToChild.Id))
|
||||
mailLookupTable[replyToChild.Id] = true;
|
||||
|
||||
replyToChild = await GetReplyParentAsync(replyToChild, accountId, replyToChild.AssignedFolder.Id, sentFolder.Id, draftFolder.Id);
|
||||
}
|
||||
|
||||
// Build down
|
||||
var replyToParent = await GetInReplyToReplyAsync(replyItem, accountId, replyItem.AssignedFolder.Id, sentFolder.Id, draftFolder.Id);
|
||||
|
||||
while (replyToParent != null)
|
||||
{
|
||||
replyToParent.AssignedAccount = account;
|
||||
|
||||
if (replyToParent.FolderId == draftFolder.Id)
|
||||
replyToParent.AssignedFolder = draftFolder;
|
||||
|
||||
if (replyToParent.FolderId == sentFolder.Id)
|
||||
replyToParent.AssignedFolder = sentFolder;
|
||||
|
||||
if (replyToParent.FolderId == replyItem.AssignedFolder.Id)
|
||||
replyToParent.AssignedFolder = replyItem.AssignedFolder;
|
||||
|
||||
threadItem.AddThreadItem(replyToParent);
|
||||
|
||||
if (mailLookupTable.ContainsKey(replyToParent.Id))
|
||||
mailLookupTable[replyToParent.Id] = true;
|
||||
|
||||
replyToParent = await GetInReplyToReplyAsync(replyToParent, accountId, replyToParent.AssignedFolder.Id, sentFolder.Id, draftFolder.Id);
|
||||
}
|
||||
|
||||
// It's a thread item.
|
||||
|
||||
if (threadItem.ThreadItems.Count > 1 && !threads.Exists(a => a.Id == threadItem.Id))
|
||||
{
|
||||
threads.Add(threadItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
// False alert. This is not a thread item.
|
||||
mailLookupTable[replyItem.Id] = false;
|
||||
|
||||
// TODO: Here potentially check other algorithms for threading like References.
|
||||
}
|
||||
}
|
||||
|
||||
// At this points all mails in the list belong to single items.
|
||||
// Merge with threads.
|
||||
// Last sorting will be done later on in MailService.
|
||||
|
||||
// Remove single mails that are included in thread.
|
||||
items.RemoveAll(a => mailLookupTable.ContainsKey(a.Id) && mailLookupTable[a.Id]);
|
||||
|
||||
var finalList = new List<IMailItem>(items);
|
||||
|
||||
finalList.AddRange(threads);
|
||||
|
||||
return finalList;
|
||||
}
|
||||
|
||||
public bool ShouldThreadWithItem(IMailItem originalItem, IMailItem targetItem)
|
||||
{
|
||||
bool isChild = originalItem.InReplyTo != null && originalItem.InReplyTo == targetItem.MessageId;
|
||||
bool isParent = originalItem.MessageId != null && originalItem.MessageId == targetItem.InReplyTo;
|
||||
|
||||
return isChild || isParent;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Services.Threading;
|
||||
|
||||
// Outlook and Gmail is using the same threading strategy.
|
||||
// Outlook: ConversationId -> it's set as ThreadId
|
||||
// Gmail: ThreadId
|
||||
|
||||
public class OutlookThreadingStrategy : APIThreadingStrategy, IOutlookThreadingStrategy
|
||||
{
|
||||
public OutlookThreadingStrategy(IDatabaseService databaseService, IFolderService folderService) : base(databaseService, folderService) { }
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Services;
|
||||
|
||||
public class ThreadingStrategyProvider : IThreadingStrategyProvider
|
||||
{
|
||||
private readonly IOutlookThreadingStrategy _outlookThreadingStrategy;
|
||||
private readonly IGmailThreadingStrategy _gmailThreadingStrategy;
|
||||
private readonly IImapThreadingStrategy _imapThreadStrategy;
|
||||
|
||||
public ThreadingStrategyProvider(IOutlookThreadingStrategy outlookThreadingStrategy,
|
||||
IGmailThreadingStrategy gmailThreadingStrategy,
|
||||
IImapThreadingStrategy imapThreadStrategy)
|
||||
{
|
||||
_outlookThreadingStrategy = outlookThreadingStrategy;
|
||||
_gmailThreadingStrategy = gmailThreadingStrategy;
|
||||
_imapThreadStrategy = imapThreadStrategy;
|
||||
}
|
||||
|
||||
public IThreadingStrategy GetStrategy(MailProviderType mailProviderType)
|
||||
{
|
||||
return mailProviderType switch
|
||||
{
|
||||
MailProviderType.Outlook => _outlookThreadingStrategy,
|
||||
MailProviderType.Gmail => _gmailThreadingStrategy,
|
||||
_ => _imapThreadStrategy,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user