diff --git a/Wino.Core.Domain/Entities/Mail/MailCopy.cs b/Wino.Core.Domain/Entities/Mail/MailCopy.cs index e8082268..8fe4f9f0 100644 --- a/Wino.Core.Domain/Entities/Mail/MailCopy.cs +++ b/Wino.Core.Domain/Entities/Mail/MailCopy.cs @@ -172,6 +172,9 @@ public class MailCopy [Ignore] public Guid? ReadReceiptMessageUniqueId { get; set; } + [Ignore] + public List Categories { get; set; } = []; + public IEnumerable GetContainingIds() => [UniqueId]; public override string ToString() => $"{Subject} <-> {Id}"; } diff --git a/Wino.Core.Domain/Enums/MailCopyChangeFlags.cs b/Wino.Core.Domain/Enums/MailCopyChangeFlags.cs index 0ceaddfb..6a68e022 100644 --- a/Wino.Core.Domain/Enums/MailCopyChangeFlags.cs +++ b/Wino.Core.Domain/Enums/MailCopyChangeFlags.cs @@ -32,6 +32,7 @@ public enum MailCopyChangeFlags SenderContact = 1 << 23, UniqueId = 1 << 24, ReadReceiptState = 1 << 25, + Categories = 1 << 26, All = Id | FolderId | ThreadId | @@ -57,5 +58,6 @@ public enum MailCopyChangeFlags AssignedAccount | SenderContact | UniqueId | - ReadReceiptState + ReadReceiptState | + Categories } diff --git a/Wino.Core.Domain/Interfaces/IMailCategoryService.cs b/Wino.Core.Domain/Interfaces/IMailCategoryService.cs index a21aba3e..9ed9cab7 100644 --- a/Wino.Core.Domain/Interfaces/IMailCategoryService.cs +++ b/Wino.Core.Domain/Interfaces/IMailCategoryService.cs @@ -23,6 +23,7 @@ public interface IMailCategoryService Task AssignCategoryAsync(Guid categoryId, IEnumerable mailCopyUniqueIds); Task UnassignCategoryAsync(Guid categoryId, IEnumerable mailCopyUniqueIds); Task> GetCategoriesForMailAsync(Guid accountId, IEnumerable mailCopyUniqueIds); + Task>> GetCategoriesByMailAsync(Guid accountId, IEnumerable mailCopyUniqueIds); Task> GetAssignedCategoryIdsForAllAsync(IEnumerable mailCopyUniqueIds); Task> GetCategoryNamesForMailAsync(Guid mailCopyUniqueId); Task> GetMailCopiesForCategoryAsync(Guid categoryId); diff --git a/Wino.Core.Domain/Interfaces/IMailItemDisplayInformation.cs b/Wino.Core.Domain/Interfaces/IMailItemDisplayInformation.cs index b8ec5e08..7faeb87f 100644 --- a/Wino.Core.Domain/Interfaces/IMailItemDisplayInformation.cs +++ b/Wino.Core.Domain/Interfaces/IMailItemDisplayInformation.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.ComponentModel; +using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Entities.Shared; namespace Wino.Core.Domain.Interfaces; @@ -27,4 +29,6 @@ public interface IMailItemDisplayInformation : INotifyPropertyChanged bool HasReadReceiptTracking { get; } bool IsReadReceiptAcknowledged { get; } string ReadReceiptDisplayText { get; } + IReadOnlyList Categories { get; } + bool HasCategories { get; } } diff --git a/Wino.Core.ViewModels/PersonalizationPageViewModel.cs b/Wino.Core.ViewModels/PersonalizationPageViewModel.cs index bde4b56a..45aff032 100644 --- a/Wino.Core.ViewModels/PersonalizationPageViewModel.cs +++ b/Wino.Core.ViewModels/PersonalizationPageViewModel.cs @@ -389,6 +389,24 @@ public partial class PersonalizationPageViewModel : CoreBaseViewModel public bool HasReadReceiptTracking { get; } = false; public bool IsReadReceiptAcknowledged { get; } = false; public string ReadReceiptDisplayText { get; } = string.Empty; + public IReadOnlyList Categories { get; } = + [ + new() + { + Id = Guid.NewGuid(), + Name = "Follow Up", + BackgroundColorHex = "#DCEBFF", + TextColorHex = "#0B5CAD" + }, + new() + { + Id = Guid.NewGuid(), + Name = "Planning", + BackgroundColorHex = "#DDF5D7", + TextColorHex = "#236A1E" + } + ]; + public bool HasCategories => Categories.Count > 0; public AccountContact SenderContact { get; } = null; event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged { diff --git a/Wino.Mail.ViewModels/Data/AccountContactViewModel.cs b/Wino.Mail.ViewModels/Data/AccountContactViewModel.cs index 507d7408..92ad888c 100644 --- a/Wino.Mail.ViewModels/Data/AccountContactViewModel.cs +++ b/Wino.Mail.ViewModels/Data/AccountContactViewModel.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using CommunityToolkit.Mvvm.ComponentModel; using Wino.Core.Domain; +using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Interfaces; @@ -71,6 +73,8 @@ public partial class AccountContactViewModel : ObservableObject, IMailItemDispla public bool HasReadReceiptTracking => false; public bool IsReadReceiptAcknowledged => false; public string ReadReceiptDisplayText => string.Empty; + public IReadOnlyList Categories => []; + public bool HasCategories => false; public AccountContact SenderContact => new() { Address = Address, diff --git a/Wino.Mail.ViewModels/Data/MailItemViewModel.cs b/Wino.Mail.ViewModels/Data/MailItemViewModel.cs index e11200ea..e620685d 100644 --- a/Wino.Mail.ViewModels/Data/MailItemViewModel.cs +++ b/Wino.Mail.ViewModels/Data/MailItemViewModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using CommunityToolkit.Mvvm.ComponentModel; using Wino.Core.Domain; using Wino.Core.Domain.Entities.Mail; @@ -39,6 +40,8 @@ public partial class MailItemViewModel(MailCopy mailCopy) : ObservableRecipient, [NotifyPropertyChangedFor(nameof(UniqueId))] [NotifyPropertyChangedFor(nameof(ContactPictureFileId))] [NotifyPropertyChangedFor(nameof(SenderContact))] + [NotifyPropertyChangedFor(nameof(Categories))] + [NotifyPropertyChangedFor(nameof(HasCategories))] public partial MailCopy MailCopy { get; set; } = mailCopy; [ObservableProperty] @@ -124,6 +127,10 @@ public partial class MailItemViewModel(MailCopy mailCopy) : ObservableRecipient, _ => string.Empty }; + public IReadOnlyList Categories => MailCopy.Categories; + + public bool HasCategories => Categories.Count > 0; + public string DraftId { get => MailCopy.DraftId; @@ -262,6 +269,7 @@ public partial class MailItemViewModel(MailCopy mailCopy) : ObservableRecipient, nameof(FolderId) => MailCopyChangeFlags.FolderId, nameof(UniqueId) => MailCopyChangeFlags.UniqueId, nameof(ContactPictureFileId) or nameof(SenderContact) => MailCopyChangeFlags.SenderContact, + nameof(Categories) or nameof(HasCategories) => MailCopyChangeFlags.Categories, _ => MailCopyChangeFlags.None }; } @@ -474,9 +482,21 @@ public partial class MailItemViewModel(MailCopy mailCopy) : ObservableRecipient, Queue(nameof(SenderContact)); } + if ((changedFlags & MailCopyChangeFlags.Categories) != 0) + { + Queue(nameof(Categories)); + Queue(nameof(HasCategories)); + } + foreach (var changedProperty in changedProperties) { OnPropertyChanged(changedProperty); } } + + public void UpdateCategories(IReadOnlyList categories) + { + MailCopy.Categories = categories?.ToList() ?? []; + RaisePropertyChanges(MailCopyChangeFlags.Categories); + } } diff --git a/Wino.Mail.ViewModels/Data/ThreadMailItemViewModel.cs b/Wino.Mail.ViewModels/Data/ThreadMailItemViewModel.cs index 82797f58..0e5ee760 100644 --- a/Wino.Mail.ViewModels/Data/ThreadMailItemViewModel.cs +++ b/Wino.Mail.ViewModels/Data/ThreadMailItemViewModel.cs @@ -5,6 +5,7 @@ using System.ComponentModel; using System.Linq; using CommunityToolkit.Mvvm.ComponentModel; 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.Interfaces; @@ -111,6 +112,13 @@ public partial class ThreadMailItemViewModel : ObservableRecipient, IMailListIte public bool IsReadReceiptAcknowledged => newestMailViewModel?.IsReadReceiptAcknowledged ?? false; public string ReadReceiptDisplayText => newestMailViewModel?.ReadReceiptDisplayText ?? string.Empty; + public IReadOnlyList Categories => ThreadEmails + .SelectMany(a => a.Categories) + .GroupBy(a => a.Id) + .Select(a => a.First()) + .OrderBy(a => a.Name) + .ToList(); + public bool HasCategories => ThreadEmails.Any(a => a.HasCategories); /// /// Gets whether any email in this thread is a draft @@ -206,6 +214,8 @@ public partial class ThreadMailItemViewModel : ObservableRecipient, IMailListIte [NotifyPropertyChangedFor(nameof(UniqueId))] [NotifyPropertyChangedFor(nameof(ContactPictureFileId))] [NotifyPropertyChangedFor(nameof(SenderContact))] + [NotifyPropertyChangedFor(nameof(Categories))] + [NotifyPropertyChangedFor(nameof(HasCategories))] public partial ObservableCollection ThreadEmails { get; set; } = []; private MailItemViewModel newestMailViewModel => _cachedNewestMailViewModel; @@ -467,6 +477,12 @@ public partial class ThreadMailItemViewModel : ObservableRecipient, IMailListIte Queue(nameof(ContactPictureFileId)); Queue(nameof(SenderContact)); } + + if ((changedFlags & MailCopyChangeFlags.Categories) != 0) + { + Queue(nameof(Categories)); + Queue(nameof(HasCategories)); + } } } @@ -495,6 +511,12 @@ public partial class ThreadMailItemViewModel : ObservableRecipient, IMailListIte if ((changedFlags & MailCopyChangeFlags.IsDraft) != 0 || changedFlags == MailCopyChangeFlags.All) Queue(nameof(IsDraft)); + if ((changedFlags & MailCopyChangeFlags.Categories) != 0 || changedFlags == MailCopyChangeFlags.All) + { + Queue(nameof(Categories)); + Queue(nameof(HasCategories)); + } + foreach (var changedProperty in changedProperties) { OnPropertyChanged(changedProperty); diff --git a/Wino.Mail.ViewModels/MailListPageViewModel.cs b/Wino.Mail.ViewModels/MailListPageViewModel.cs index a671f181..d01d0de7 100644 --- a/Wino.Mail.ViewModels/MailListPageViewModel.cs +++ b/Wino.Mail.ViewModels/MailListPageViewModel.cs @@ -1329,6 +1329,8 @@ public partial class MailListPageViewModel : MailBaseViewModel, private async Task> PrepareMailViewModelsAsync(IEnumerable mailItems, CancellationToken cancellationToken = default) { + await PopulateMailCategoriesAsync(mailItems, cancellationToken).ConfigureAwait(false); + // Run ViewModel creation on background thread to avoid blocking UI return await Task.Run(() => { @@ -1342,6 +1344,38 @@ public partial class MailListPageViewModel : MailBaseViewModel, }, cancellationToken).ConfigureAwait(false); } + private async Task PopulateMailCategoriesAsync(IEnumerable mailItems, CancellationToken cancellationToken) + { + var mails = mailItems?.Where(a => a != null).ToList() ?? []; + if (mails.Count == 0) + return; + + var accountIdsByFolderId = ActiveFolder?.HandlingFolders? + .GroupBy(a => a.Id) + .ToDictionary(a => a.Key, a => a.First().MailAccountId) ?? new Dictionary(); + + var mailsByAccount = mails + .GroupBy(mail => ResolveMailAccountId(mail, accountIdsByFolderId)) + .Where(group => group.Key != Guid.Empty) + .ToList(); + + foreach (var groupedMails in mailsByAccount) + { + cancellationToken.ThrowIfCancellationRequested(); + + var categoriesByMail = await _mailCategoryService + .GetCategoriesByMailAsync(groupedMails.Key, groupedMails.Select(a => a.UniqueId)) + .ConfigureAwait(false); + + foreach (var mail in groupedMails) + { + mail.Categories = categoriesByMail.TryGetValue(mail.UniqueId, out var categories) + ? categories.ToList() + : []; + } + } + } + private async Task> GetPendingOperationUniqueIdsForActiveFolderAccountsAsync(CancellationToken cancellationToken = default) { var pendingOperationUniqueIds = new HashSet(); diff --git a/Wino.Mail.ViewModels/MessageListPageViewModel.cs b/Wino.Mail.ViewModels/MessageListPageViewModel.cs index 2695e4fe..8ecf7e3c 100644 --- a/Wino.Mail.ViewModels/MessageListPageViewModel.cs +++ b/Wino.Mail.ViewModels/MessageListPageViewModel.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using System.Threading.Tasks; using CommunityToolkit.Mvvm.Input; 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.Interfaces; @@ -187,6 +188,24 @@ public partial class MessageListPageViewModel : MailBaseViewModel public bool HasReadReceiptTracking => true; public bool IsReadReceiptAcknowledged => false; public string ReadReceiptDisplayText => Translator.MailReceiptStatus_Requested; + public IReadOnlyList Categories => + [ + new() + { + Id = Guid.NewGuid(), + Name = "Urgent", + BackgroundColorHex = "#FFE1DE", + TextColorHex = "#A1260D" + }, + new() + { + Id = Guid.NewGuid(), + Name = "Client", + BackgroundColorHex = "#E4E8FF", + TextColorHex = "#4255C5" + } + ]; + public bool HasCategories => Categories.Count > 0; public AccountContact SenderContact => new() { Address = "hi@bkaan.dev", diff --git a/Wino.Mail.WinUI/Controls/MailItemDisplayInformationControl.xaml b/Wino.Mail.WinUI/Controls/MailItemDisplayInformationControl.xaml index adc2f9ef..11598f53 100644 --- a/Wino.Mail.WinUI/Controls/MailItemDisplayInformationControl.xaml +++ b/Wino.Mail.WinUI/Controls/MailItemDisplayInformationControl.xaml @@ -8,6 +8,7 @@ xmlns:domain="using:Wino.Core.Domain" xmlns:enums="using:Wino.Core.Domain.Enums" xmlns:helpers="using:Wino.Helpers" + xmlns:mail="using:Wino.Core.Domain.Entities.Mail" xmlns:muxc="using:Microsoft.UI.Xaml.Controls" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" @@ -23,6 +24,28 @@ Unloaded="OnUnloaded"> + + + + + + + + + +