From f45580be70566c987867d709951c2246dda993e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Fri, 23 Aug 2024 01:07:00 +0200 Subject: [PATCH 01/15] Adding contact details for loaded mails and fixing background notification actions. --- ...ddressInformation.cs => AccountContact.cs} | 18 +- Wino.Core.Domain/Entities/MailCopy.cs | 9 + .../Interfaces/IBackgroundTaskService.cs | 12 +- Wino.Core.Domain/Models/MailItem/IMailItem.cs | 1 + .../Models/MailItem/ThreadMailItem.cs | 2 + .../Services/BackgroundTaskService.cs | 35 +- Wino.Core/Extensions/MimeExtensions.cs | 6 +- Wino.Core/Services/ContactService.cs | 20 +- Wino.Core/Services/DatabaseService.cs | 2 +- Wino.Core/Services/MailService.cs | 32 +- Wino.Mail.ViewModels/AppShellViewModel.cs | 9 +- .../Collections/WinoMailCollection.cs | 9 +- Wino.Mail.ViewModels/ComposePageViewModel.cs | 14 +- .../Data/MailItemViewModel.cs | 39 +- .../Data/ThreadMailItemViewModel.cs | 1 + .../MailRenderingPageViewModel.cs | 8 +- .../PersonalizationPageViewModel.cs | 9 + Wino.Mail/AppShell.xaml.cs | 24 +- Wino.Mail/Controls/ImagePreviewControl.cs | 66 ++- .../MailItemDisplayInformationControl.xaml | 28 +- .../MailItemDisplayInformationControl.xaml.cs | 95 +--- Wino.Mail/Views/ComposePage.xaml | 4 +- Wino.Mail/Views/ComposePage.xaml.cs | 6 +- Wino.Mail/Views/MailListPage.xaml | 44 +- Wino.Mail/Views/MailRenderingPage.xaml | 440 ++++++++++-------- .../Views/Settings/PersonalizationPage.xaml | 17 +- 26 files changed, 523 insertions(+), 427 deletions(-) rename Wino.Core.Domain/Entities/{AddressInformation.cs => AccountContact.cs} (70%) diff --git a/Wino.Core.Domain/Entities/AddressInformation.cs b/Wino.Core.Domain/Entities/AccountContact.cs similarity index 70% rename from Wino.Core.Domain/Entities/AddressInformation.cs rename to Wino.Core.Domain/Entities/AccountContact.cs index de2bc308..9eae9198 100644 --- a/Wino.Core.Domain/Entities/AddressInformation.cs +++ b/Wino.Core.Domain/Entities/AccountContact.cs @@ -1,6 +1,6 @@ -using SQLite; -using System; +using System; using System.Collections.Generic; +using SQLite; namespace Wino.Core.Domain.Entities { @@ -9,23 +9,23 @@ namespace Wino.Core.Domain.Entities /// These values will be inserted during MIME fetch. /// - // TODO: This can easily evolve to Contact store, just like People app in Windows 10/11. // Do it. - public class AddressInformation : IEquatable + public class AccountContact : IEquatable { [PrimaryKey] public string Address { get; set; } public string Name { get; set; } + public string Base64ContactPicture { get; set; } public string DisplayName => Address == Name ? Address : $"{Name} <{Address}>"; public override bool Equals(object obj) { - return Equals(obj as AddressInformation); + return Equals(obj as AccountContact); } - public bool Equals(AddressInformation other) + public bool Equals(AccountContact other) { return !(other is null) && Address == other.Address && @@ -40,12 +40,12 @@ namespace Wino.Core.Domain.Entities return hashCode; } - public static bool operator ==(AddressInformation left, AddressInformation right) + public static bool operator ==(AccountContact left, AccountContact right) { - return EqualityComparer.Default.Equals(left, right); + return EqualityComparer.Default.Equals(left, right); } - public static bool operator !=(AddressInformation left, AddressInformation right) + public static bool operator !=(AccountContact left, AccountContact right) { return !(left == right); } diff --git a/Wino.Core.Domain/Entities/MailCopy.cs b/Wino.Core.Domain/Entities/MailCopy.cs index eeb81ccd..229932de 100644 --- a/Wino.Core.Domain/Entities/MailCopy.cs +++ b/Wino.Core.Domain/Entities/MailCopy.cs @@ -141,6 +141,15 @@ namespace Wino.Core.Domain.Entities /// [Ignore] public MailAccount AssignedAccount { get; set; } + + /// + /// Contact information of the sender if exists. + /// Warning: This field is not populated by queries. + /// Services or View Models are responsible for populating this field. + /// + [Ignore] + public AccountContact SenderContact { get; set; } + public IEnumerable GetContainingIds() => [UniqueId]; public override string ToString() => $"{Subject} <-> {Id}"; } diff --git a/Wino.Core.Domain/Interfaces/IBackgroundTaskService.cs b/Wino.Core.Domain/Interfaces/IBackgroundTaskService.cs index 67142e69..53fb80b3 100644 --- a/Wino.Core.Domain/Interfaces/IBackgroundTaskService.cs +++ b/Wino.Core.Domain/Interfaces/IBackgroundTaskService.cs @@ -1,10 +1,18 @@ -namespace Wino.Core.Domain.Interfaces +using System.Threading.Tasks; + +namespace Wino.Core.Domain.Interfaces { public interface IBackgroundTaskService { /// - /// Unregisters all existing background tasks. Useful for migrations. + /// Unregisters all background tasks once. + /// This is used to clean up the background tasks when the app is updated. /// void UnregisterAllBackgroundTask(); + + /// + /// Registers required background tasks. + /// + Task RegisterBackgroundTasksAsync(); } } diff --git a/Wino.Core.Domain/Models/MailItem/IMailItem.cs b/Wino.Core.Domain/Models/MailItem/IMailItem.cs index 73f01e02..fbacbb6d 100644 --- a/Wino.Core.Domain/Models/MailItem/IMailItem.cs +++ b/Wino.Core.Domain/Models/MailItem/IMailItem.cs @@ -29,5 +29,6 @@ namespace Wino.Core.Domain.Models.MailItem MailItemFolder AssignedFolder { get; } MailAccount AssignedAccount { get; } + AccountContact SenderContact { get; } } } diff --git a/Wino.Core.Domain/Models/MailItem/ThreadMailItem.cs b/Wino.Core.Domain/Models/MailItem/ThreadMailItem.cs index 7a0d0656..0c59af3a 100644 --- a/Wino.Core.Domain/Models/MailItem/ThreadMailItem.cs +++ b/Wino.Core.Domain/Models/MailItem/ThreadMailItem.cs @@ -85,6 +85,8 @@ namespace Wino.Core.Domain.Models.MailItem public Guid FileId => LatestMailItem?.FileId ?? Guid.Empty; + public AccountContact SenderContact => LatestMailItem?.SenderContact; + #endregion } } diff --git a/Wino.Core.UWP/Services/BackgroundTaskService.cs b/Wino.Core.UWP/Services/BackgroundTaskService.cs index e9afd064..3a94f4e6 100644 --- a/Wino.Core.UWP/Services/BackgroundTaskService.cs +++ b/Wino.Core.UWP/Services/BackgroundTaskService.cs @@ -1,4 +1,7 @@ -using Serilog; +using System; +using System.Linq; +using System.Threading.Tasks; +using Serilog; using Windows.ApplicationModel.Background; using Wino.Core.Domain.Interfaces; @@ -7,6 +10,7 @@ namespace Wino.Core.UWP.Services public class BackgroundTaskService : IBackgroundTaskService { private const string IsBackgroundTasksUnregisteredKey = nameof(IsBackgroundTasksUnregisteredKey); + public const string ToastNotificationActivationHandlerTaskName = "ToastNotificationActivationHandlerTask"; private readonly IConfigurationService _configurationService; @@ -17,7 +21,7 @@ namespace Wino.Core.UWP.Services public void UnregisterAllBackgroundTask() { - if (!_configurationService.Get(IsBackgroundTasksUnregisteredKey, false)) + if (_configurationService.Get(IsBackgroundTasksUnregisteredKey, false)) { foreach (var task in BackgroundTaskRegistration.AllTasks) { @@ -28,5 +32,32 @@ namespace Wino.Core.UWP.Services _configurationService.Set(IsBackgroundTasksUnregisteredKey, true); } } + + public Task RegisterBackgroundTasksAsync() + { + return RegisterToastNotificationHandlerBackgroundTaskAsync(); + } + + public async Task RegisterToastNotificationHandlerBackgroundTaskAsync() + { + // If background task is already registered, do nothing. + if (BackgroundTaskRegistration.AllTasks.Any(i => i.Value.Name.Equals(ToastNotificationActivationHandlerTaskName))) + return; + + // Otherwise request access + BackgroundAccessStatus status = await BackgroundExecutionManager.RequestAccessAsync(); + + // Create the background task + BackgroundTaskBuilder builder = new BackgroundTaskBuilder() + { + Name = ToastNotificationActivationHandlerTaskName + }; + + // Assign the toast action trigger + builder.SetTrigger(new ToastNotificationActionTrigger()); + + // And register the task + BackgroundTaskRegistration registration = builder.Register(); + } } } diff --git a/Wino.Core/Extensions/MimeExtensions.cs b/Wino.Core/Extensions/MimeExtensions.cs index bb7d776a..e7aac948 100644 --- a/Wino.Core/Extensions/MimeExtensions.cs +++ b/Wino.Core/Extensions/MimeExtensions.cs @@ -41,15 +41,15 @@ namespace Wino.Core.Extensions } } - public static AddressInformation ToAddressInformation(this MailboxAddress address) + public static AccountContact ToAddressInformation(this MailboxAddress address) { if (address == null) - return new AddressInformation() { Name = Translator.UnknownSender, Address = Translator.UnknownAddress }; + return new AccountContact() { Name = Translator.UnknownSender, Address = Translator.UnknownAddress }; if (string.IsNullOrEmpty(address.Name)) address.Name = address.Address; - return new AddressInformation() { Name = address.Name, Address = address.Address }; + return new AccountContact() { Name = address.Name, Address = address.Address }; } /// diff --git a/Wino.Core/Services/ContactService.cs b/Wino.Core/Services/ContactService.cs index 889c141f..75c25fac 100644 --- a/Wino.Core/Services/ContactService.cs +++ b/Wino.Core/Services/ContactService.cs @@ -10,8 +10,8 @@ namespace Wino.Core.Services { public interface IContactService { - Task> GetAddressInformationAsync(string queryText); - Task GetAddressInformationByAddressAsync(string address); + Task> GetAddressInformationAsync(string queryText); + Task GetAddressInformationByAddressAsync(string address); Task SaveAddressInformationAsync(MimeMessage message); } @@ -19,24 +19,24 @@ namespace Wino.Core.Services { public ContactService(IDatabaseService databaseService) : base(databaseService) { } - public Task> GetAddressInformationAsync(string queryText) + public Task> GetAddressInformationAsync(string queryText) { if (queryText == null || queryText.Length < 2) - return Task.FromResult>(null); + return Task.FromResult>(null); - var query = new Query(nameof(AddressInformation)); + var query = new Query(nameof(AccountContact)); query.WhereContains("Address", queryText); query.OrWhereContains("Name", queryText); var rawLikeQuery = query.GetRawQuery(); - return Connection.QueryAsync(rawLikeQuery); + return Connection.QueryAsync(rawLikeQuery); } - public async Task GetAddressInformationByAddressAsync(string address) + public async Task GetAddressInformationByAddressAsync(string address) { - return await Connection.Table().Where(a => a.Address == address).FirstOrDefaultAsync() - ?? new AddressInformation() { Name = address, Address = address }; + return await Connection.Table().Where(a => a.Address == address).FirstOrDefaultAsync() + ?? new AccountContact() { Name = address, Address = address }; } public async Task SaveAddressInformationAsync(MimeMessage message) @@ -45,7 +45,7 @@ namespace Wino.Core.Services .GetRecipients(true) .Where(a => !string.IsNullOrEmpty(a.Name) && !string.IsNullOrEmpty(a.Address)); - var addressInformations = recipients.Select(a => new AddressInformation() { Name = a.Name, Address = a.Address }); + var addressInformations = recipients.Select(a => new AccountContact() { Name = a.Name, Address = a.Address }); foreach (var info in addressInformations) await Connection.InsertOrReplaceAsync(info).ConfigureAwait(false); diff --git a/Wino.Core/Services/DatabaseService.cs b/Wino.Core/Services/DatabaseService.cs index f848cd5f..8d7c5453 100644 --- a/Wino.Core/Services/DatabaseService.cs +++ b/Wino.Core/Services/DatabaseService.cs @@ -57,7 +57,7 @@ namespace Wino.Core.Services typeof(MailItemFolder), typeof(MailAccount), typeof(TokenInformation), - typeof(AddressInformation), + typeof(AccountContact), typeof(CustomServerInformation), typeof(AccountSignature), typeof(MergedInbox), diff --git a/Wino.Core/Services/MailService.cs b/Wino.Core/Services/MailService.cs index e2f4ed0c..d4cf70fb 100644 --- a/Wino.Core/Services/MailService.cs +++ b/Wino.Core/Services/MailService.cs @@ -201,6 +201,7 @@ namespace Wino.Core.Services Dictionary folderCache = []; Dictionary accountCache = []; + Dictionary contactCache = []; // Populate Folder Assignment for each single mail, to be able later group by "MailAccountId". // This is needed to execute threading strategy by account type. @@ -255,7 +256,9 @@ namespace Wino.Core.Services return threadedItems; // Recursive function to populate folder and account assignments for each mail item. - async Task LoadAssignedPropertiesWithCacheAsync(IMailItem mail, Dictionary folderCache, Dictionary accountCache) + async Task LoadAssignedPropertiesWithCacheAsync(IMailItem mail, + Dictionary folderCache, + Dictionary accountCache) { if (mail is ThreadMailItem threadMailItem) { @@ -276,6 +279,7 @@ namespace Wino.Core.Services folderAssignment = await _folderService.GetFolderAsync(mailCopy.FolderId).ConfigureAwait(false); _ = folderCache.TryAdd(mailCopy.FolderId, folderAssignment); } + if (folderAssignment != null) { var isAccountCached = accountCache.TryGetValue(folderAssignment.MailAccountId, out accountAssignment); @@ -283,11 +287,21 @@ namespace Wino.Core.Services { accountAssignment = await _accountService.GetAccountAsync(folderAssignment.MailAccountId).ConfigureAwait(false); _ = accountCache.TryAdd(folderAssignment.MailAccountId, accountAssignment); + } } + bool isContactCached = contactCache.TryGetValue(mailCopy.FromAddress, out AccountContact contactAssignment); + + if (!isContactCached && accountAssignment != null) + { + contactAssignment = await GetSenderContactForAccountAsync(accountAssignment, mailCopy.FromAddress).ConfigureAwait(false); + _ = contactCache.TryAdd(mailCopy.FromAddress, contactAssignment); + } + mailCopy.AssignedFolder = folderAssignment; mailCopy.AssignedAccount = accountAssignment; + mailCopy.SenderContact = contactAssignment; } } } @@ -304,11 +318,23 @@ namespace Wino.Core.Services return mailCopies; } + private Task GetSenderContactForAccountAsync(MailAccount account, string fromAddress) + { + if (fromAddress == account.Address) + { + return Task.FromResult(new AccountContact() { Address = account.Address, Name = account.SenderName, Base64ContactPicture = account.Base64ProfilePictureData }); + } + else + { + return _contactService.GetAddressInformationByAddressAsync(fromAddress); + } + } + private async Task LoadAssignedPropertiesAsync(MailCopy mailCopy) { if (mailCopy == null) return; - // Load AssignedAccount and AssignedFolder. + // Load AssignedAccount, AssignedFolder and SenderContact. var folder = await _folderService.GetFolderAsync(mailCopy.FolderId); @@ -320,6 +346,7 @@ namespace Wino.Core.Services mailCopy.AssignedAccount = account; mailCopy.AssignedFolder = folder; + mailCopy.SenderContact = await GetSenderContactForAccountAsync(account, mailCopy.FromAddress).ConfigureAwait(false); } public async Task GetSingleMailItemWithoutFolderAssignmentAsync(string mailCopyId) @@ -579,6 +606,7 @@ namespace Wino.Core.Services mailCopy.UniqueId = Guid.NewGuid(); mailCopy.AssignedAccount = account; mailCopy.AssignedFolder = assignedFolder; + mailCopy.SenderContact = await GetSenderContactForAccountAsync(account, mailCopy.FromAddress).ConfigureAwait(false); mailCopy.FolderId = assignedFolder.Id; // Only save MIME files if they don't exists. diff --git a/Wino.Mail.ViewModels/AppShellViewModel.cs b/Wino.Mail.ViewModels/AppShellViewModel.cs index eab4fa06..a84a1956 100644 --- a/Wino.Mail.ViewModels/AppShellViewModel.cs +++ b/Wino.Mail.ViewModels/AppShellViewModel.cs @@ -246,7 +246,7 @@ namespace Wino.Mail.ViewModels await ForceAllAccountSynchronizationsAsync(); await MakeSureEnableStartupLaunchAsync(); - ConfigureBackgroundTasks(); + await ConfigureBackgroundTasksAsync(); } private async Task MakeSureEnableStartupLaunchAsync() @@ -289,11 +289,14 @@ namespace Wino.Mail.ViewModels } } - private void ConfigureBackgroundTasks() + private async Task ConfigureBackgroundTasksAsync() { try { + // This will only unregister once. Safe to execute multiple times. _backgroundTaskService.UnregisterAllBackgroundTask(); + + await _backgroundTaskService.RegisterBackgroundTasksAsync(); } catch (Exception ex) { @@ -623,7 +626,7 @@ namespace Wino.Mail.ViewModels } } - private async Task ChangeLoadedAccountAsync(IAccountMenuItem clickedBaseAccountMenuItem, bool navigateInbox = true) + public async Task ChangeLoadedAccountAsync(IAccountMenuItem clickedBaseAccountMenuItem, bool navigateInbox = true) { if (clickedBaseAccountMenuItem == null) return; diff --git a/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs b/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs index ba1bcb84..dd991d35 100644 --- a/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs +++ b/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs @@ -193,7 +193,7 @@ namespace Wino.Mail.ViewModels.Collections if (item is MailItemViewModel itemViewModel) { - await ExecuteUIThread(() => { itemViewModel.Update(addedItem); }); + await ExecuteUIThread(() => { itemViewModel.MailCopy = addedItem; }); UpdateUniqueIdHashes(itemViewModel, false); UpdateUniqueIdHashes(addedItem, true); @@ -230,7 +230,7 @@ namespace Wino.Mail.ViewModels.Collections UpdateUniqueIdHashes(itemViewModel, false); UpdateUniqueIdHashes(addedItem, true); - await ExecuteUIThread(() => { itemViewModel.Update(addedItem); }); + await ExecuteUIThread(() => { itemViewModel.MailCopy = addedItem; }); shouldExit = true; } @@ -349,7 +349,10 @@ namespace Wino.Mail.ViewModels.Collections UpdateUniqueIdHashes(itemContainer.ItemViewModel, false); } - itemContainer.ItemViewModel?.Update(updatedMailCopy); + if (itemContainer.ItemViewModel != null) + { + itemContainer.ItemViewModel.MailCopy = updatedMailCopy; + } UpdateUniqueIdHashes(updatedMailCopy, true); diff --git a/Wino.Mail.ViewModels/ComposePageViewModel.cs b/Wino.Mail.ViewModels/ComposePageViewModel.cs index 13643abf..53437458 100644 --- a/Wino.Mail.ViewModels/ComposePageViewModel.cs +++ b/Wino.Mail.ViewModels/ComposePageViewModel.cs @@ -85,9 +85,9 @@ namespace Wino.Mail.ViewModels public ObservableCollection IncludedAttachments { get; set; } = []; public ObservableCollection Accounts { get; set; } = []; - public ObservableCollection ToItems { get; set; } = []; - public ObservableCollection CCItems { get; set; } = []; - public ObservableCollection BCCItems { get; set; } = []; + public ObservableCollection ToItems { get; set; } = []; + public ObservableCollection CCItems { get; set; } = []; + public ObservableCollection BCCItems { get; set; } = []; public List ToolbarSections { get; set; } = @@ -476,7 +476,7 @@ namespace Wino.Mail.ViewModels } } - private void LoadAddressInfo(InternetAddressList list, ObservableCollection collection) + private void LoadAddressInfo(InternetAddressList list, ObservableCollection collection) { foreach (var item in list) { @@ -509,7 +509,7 @@ namespace Wino.Mail.ViewModels } } - private void SaveAddressInfo(IEnumerable addresses, InternetAddressList list) + private void SaveAddressInfo(IEnumerable addresses, InternetAddressList list) { list.Clear(); @@ -517,7 +517,7 @@ namespace Wino.Mail.ViewModels list.Add(new MailboxAddress(item.Name, item.Address)); } - public async Task GetAddressInformationAsync(string tokenText, ObservableCollection collection) + public async Task GetAddressInformationAsync(string tokenText, ObservableCollection collection) { // Get model from the service. This will make sure the name is properly included if there is any record. @@ -550,7 +550,7 @@ namespace Wino.Mail.ViewModels { await ExecuteUIThread(() => { - CurrentMailDraftItem.Update(updatedMail); + CurrentMailDraftItem.MailCopy = updatedMail; DiscardCommand.NotifyCanExecuteChanged(); SendCommand.NotifyCanExecuteChanged(); diff --git a/Wino.Mail.ViewModels/Data/MailItemViewModel.cs b/Wino.Mail.ViewModels/Data/MailItemViewModel.cs index d96e486e..66b84cd6 100644 --- a/Wino.Mail.ViewModels/Data/MailItemViewModel.cs +++ b/Wino.Mail.ViewModels/Data/MailItemViewModel.cs @@ -11,12 +11,13 @@ namespace Wino.Mail.ViewModels.Data /// public partial class MailItemViewModel(MailCopy mailCopy) : ObservableObject, IMailItem { - public MailCopy MailCopy { get; private set; } = mailCopy; + [ObservableProperty] + private MailCopy mailCopy = mailCopy; + // public MailCopy MailCopy { get; private set; } = mailCopy; public Guid UniqueId => ((IMailItem)MailCopy).UniqueId; public string ThreadId => ((IMailItem)MailCopy).ThreadId; public string MessageId => ((IMailItem)MailCopy).MessageId; - public string FromName => ((IMailItem)MailCopy).FromName ?? FromAddress; public DateTime CreationDate => ((IMailItem)MailCopy).CreationDate; public string References => ((IMailItem)MailCopy).References; public string InReplyTo => ((IMailItem)MailCopy).InReplyTo; @@ -33,6 +34,12 @@ namespace Wino.Mail.ViewModels.Data set => SetProperty(MailCopy.IsFlagged, value, MailCopy, (u, n) => u.IsFlagged = n); } + public string FromName + { + get => string.IsNullOrEmpty(MailCopy.FromName) ? MailCopy.FromAddress : MailCopy.FromName; + set => SetProperty(MailCopy.FromName, value, MailCopy, (u, n) => u.FromName = n); + } + public bool IsFocused { get => MailCopy.IsFocused; @@ -93,20 +100,22 @@ namespace Wino.Mail.ViewModels.Data public Guid FileId => ((IMailItem)MailCopy).FileId; - public void Update(MailCopy updatedMailItem) - { - MailCopy = updatedMailItem; + public AccountContact SenderContact => ((IMailItem)MailCopy).SenderContact; - OnPropertyChanged(nameof(IsRead)); - OnPropertyChanged(nameof(IsFocused)); - OnPropertyChanged(nameof(IsFlagged)); - OnPropertyChanged(nameof(IsDraft)); - OnPropertyChanged(nameof(DraftId)); - OnPropertyChanged(nameof(Subject)); - OnPropertyChanged(nameof(PreviewText)); - OnPropertyChanged(nameof(FromAddress)); - OnPropertyChanged(nameof(HasAttachments)); - } + //public void Update(MailCopy updatedMailItem) + //{ + // MailCopy = updatedMailItem; + + // //OnPropertyChanged(nameof(IsRead)); + // //OnPropertyChanged(nameof(IsFocused)); + // //OnPropertyChanged(nameof(IsFlagged)); + // //OnPropertyChanged(nameof(IsDraft)); + // //OnPropertyChanged(nameof(DraftId)); + // //OnPropertyChanged(nameof(Subject)); + // //OnPropertyChanged(nameof(PreviewText)); + // //OnPropertyChanged(nameof(FromAddress)); + // //OnPropertyChanged(nameof(HasAttachments)); + //} public IEnumerable GetContainingIds() => new[] { UniqueId }; } diff --git a/Wino.Mail.ViewModels/Data/ThreadMailItemViewModel.cs b/Wino.Mail.ViewModels/Data/ThreadMailItemViewModel.cs index 3594b40d..5c073f3f 100644 --- a/Wino.Mail.ViewModels/Data/ThreadMailItemViewModel.cs +++ b/Wino.Mail.ViewModels/Data/ThreadMailItemViewModel.cs @@ -15,6 +15,7 @@ namespace Wino.Mail.ViewModels.Data public partial class ThreadMailItemViewModel : ObservableObject, IMailItemThread, IComparable, IComparable { public ObservableCollection ThreadItems => ((IMailItemThread)_threadMailItem).ThreadItems; + public AccountContact SenderContact => ((IMailItemThread)_threadMailItem).SenderContact; private readonly ThreadMailItem _threadMailItem; diff --git a/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs b/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs index 7fe96b2e..3871839e 100644 --- a/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs +++ b/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs @@ -108,9 +108,9 @@ namespace Wino.Mail.ViewModels private DateTime creationDate; - public ObservableCollection ToItems { get; set; } = new ObservableCollection(); - public ObservableCollection CCItemsItems { get; set; } = new ObservableCollection(); - public ObservableCollection BCCItems { get; set; } = new ObservableCollection(); + public ObservableCollection ToItems { get; set; } = new ObservableCollection(); + public ObservableCollection CCItemsItems { get; set; } = new ObservableCollection(); + public ObservableCollection BCCItems { get; set; } = new ObservableCollection(); public ObservableCollection Attachments { get; set; } = new ObservableCollection(); public ObservableCollection MenuItems { get; set; } = new ObservableCollection(); @@ -470,7 +470,7 @@ namespace Wino.Mail.ViewModels StatePersistenceService.IsReadingMail = false; } - private void LoadAddressInfo(InternetAddressList list, ObservableCollection collection) + private void LoadAddressInfo(InternetAddressList list, ObservableCollection collection) { collection.Clear(); diff --git a/Wino.Mail.ViewModels/PersonalizationPageViewModel.cs b/Wino.Mail.ViewModels/PersonalizationPageViewModel.cs index 718cff85..10509ee5 100644 --- a/Wino.Mail.ViewModels/PersonalizationPageViewModel.cs +++ b/Wino.Mail.ViewModels/PersonalizationPageViewModel.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Wino.Core.Domain; +using Wino.Core.Domain.Entities; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Personalization; @@ -21,6 +22,14 @@ namespace Wino.Mail.ViewModels private bool isPropChangeDisabled = false; + // Sample mail copy to use in previewing mail display modes. + public MailCopy DemoPreviewMailCopy { get; } = new MailCopy() + { + FromName = "Sender Name", + Subject = "Mail Subject", + PreviewText = "Thank you for using Wino Mail. We hope you enjoy the experience.", + }; + #region Personalization public bool IsSelectedWindowsAccentColor => SelectedAppColor == Colors.LastOrDefault(); diff --git a/Wino.Mail/AppShell.xaml.cs b/Wino.Mail/AppShell.xaml.cs index 966d1c31..806d7d6c 100644 --- a/Wino.Mail/AppShell.xaml.cs +++ b/Wino.Mail/AppShell.xaml.cs @@ -142,8 +142,6 @@ namespace Wino.Views if (ViewModel.MenuItems.TryGetFolderMenuItem(message.FolderId, out IBaseFolderMenuItem foundMenuItem)) { - if (foundMenuItem == null) return; - foundMenuItem.Expand(); await ViewModel.NavigateFolderAsync(foundMenuItem); @@ -153,7 +151,27 @@ namespace Wino.Views if (message.NavigateMailItem == null) return; // At this point folder is navigated and items are loaded. - WeakReferenceMessenger.Default.Send(new MailItemNavigationRequested(message.NavigateMailItem.UniqueId)); + WeakReferenceMessenger.Default.Send(new MailItemNavigationRequested(message.NavigateMailItem.UniqueId, ScrollToItem: true)); + } + else if (ViewModel.MenuItems.TryGetAccountMenuItem(message.NavigateMailItem.AssignedAccount.Id, out IAccountMenuItem accountMenuItem)) + { + // Loaded account is different. First change the folder items and navigate. + + await ViewModel.ChangeLoadedAccountAsync(accountMenuItem, navigateInbox: false); + + // Find the folder. + + if (ViewModel.MenuItems.TryGetFolderMenuItem(message.FolderId, out IBaseFolderMenuItem accountFolderMenuItem)) + { + accountFolderMenuItem.Expand(); + + await ViewModel.NavigateFolderAsync(accountFolderMenuItem); + + navigationView.SelectedItem = accountFolderMenuItem; + + // At this point folder is navigated and items are loaded. + WeakReferenceMessenger.Default.Send(new MailItemNavigationRequested(message.NavigateMailItem.UniqueId, ScrollToItem: true)); + } } }); } diff --git a/Wino.Mail/Controls/ImagePreviewControl.cs b/Wino.Mail/Controls/ImagePreviewControl.cs index 9c0fa0db..2be1108f 100644 --- a/Wino.Mail/Controls/ImagePreviewControl.cs +++ b/Wino.Mail/Controls/ImagePreviewControl.cs @@ -1,6 +1,9 @@ using System; +using System.IO; using System.Text.RegularExpressions; +using System.Threading.Tasks; using Fernandezja.ColorHashSharp; +using Serilog; using Windows.UI; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; @@ -23,6 +26,16 @@ namespace Wino.Controls public static readonly DependencyProperty FromNameProperty = DependencyProperty.Register(nameof(FromName), typeof(string), typeof(ImagePreviewControl), new PropertyMetadata(string.Empty, OnAddressInformationChanged)); public static readonly DependencyProperty FromAddressProperty = DependencyProperty.Register(nameof(FromAddress), typeof(string), typeof(ImagePreviewControl), new PropertyMetadata(string.Empty, OnAddressInformationChanged)); public static readonly DependencyProperty IsKnownProperty = DependencyProperty.Register(nameof(IsKnown), typeof(bool), typeof(ImagePreviewControl), new PropertyMetadata(false)); + public static readonly DependencyProperty SenderContactPictureProperty = DependencyProperty.Register(nameof(SenderContactPicture), typeof(string), typeof(ImagePreviewControl), new PropertyMetadata(string.Empty, new PropertyChangedCallback(OnAddressInformationChanged))); + + /// + /// Gets or sets base64 string of the sender contact picture. + /// + public string SenderContactPicture + { + get { return (string)GetValue(SenderContactPictureProperty); } + set { SetValue(SenderContactPictureProperty, value); } + } public string FromName { @@ -42,8 +55,6 @@ namespace Wino.Controls set { SetValue(IsKnownProperty, value); } } - - #endregion private Ellipse Ellipse; @@ -74,7 +85,8 @@ namespace Wino.Controls control.UpdateInformation(); } - private void UpdateInformation() + + private async void UpdateInformation() { if (KnownHostImage == null || InitialsGrid == null || InitialsTextblock == null || (string.IsNullOrEmpty(FromName) && string.IsNullOrEmpty(FromAddress))) return; @@ -104,15 +116,55 @@ namespace Wino.Controls KnownHostImage.Visibility = Visibility.Collapsed; InitialsGrid.Visibility = Visibility.Visible; - var colorHash = new ColorHash(); - var rgb = colorHash.Rgb(FromAddress); + bool isContactImageLoadingHandled = !string.IsNullOrEmpty(SenderContactPicture) && await TryUpdateProfileImageAsync(); - Ellipse.Fill = new SolidColorBrush(Color.FromArgb(rgb.A, rgb.R, rgb.G, rgb.B)); + if (!isContactImageLoadingHandled) + { + var colorHash = new ColorHash(); + var rgb = colorHash.Rgb(FromAddress); - InitialsTextblock.Text = ExtractInitialsFromName(FromName); + Ellipse.Fill = new SolidColorBrush(Color.FromArgb(rgb.A, rgb.R, rgb.G, rgb.B)); + InitialsTextblock.Text = ExtractInitialsFromName(FromName); + } } } + /// + /// Tries to update contact image with the provided base64 image string. + /// + /// True if updated, false if not. + private async Task TryUpdateProfileImageAsync() + { + try + { + // Load the image from base64 string. + var bitmapImage = new BitmapImage(); + + var imageArray = Convert.FromBase64String(SenderContactPicture); + var imageStream = new MemoryStream(imageArray); + var randomAccessImageStream = imageStream.AsRandomAccessStream(); + + randomAccessImageStream.Seek(0); + + await bitmapImage.SetSourceAsync(randomAccessImageStream); + + Ellipse.Fill = new ImageBrush() + { + ImageSource = bitmapImage + }; + + InitialsTextblock.Text = string.Empty; + + return true; + } + catch (Exception ex) + { + Log.Error(ex, "Failed to load contact image from base64 string."); + } + + return false; + } + public string ExtractInitialsFromName(string name) { // Change from name to from address in case of name doesn't exists. diff --git a/Wino.Mail/Controls/MailItemDisplayInformationControl.xaml b/Wino.Mail/Controls/MailItemDisplayInformationControl.xaml index 2e0e45b5..889f633d 100644 --- a/Wino.Mail/Controls/MailItemDisplayInformationControl.xaml +++ b/Wino.Mail/Controls/MailItemDisplayInformationControl.xaml @@ -5,6 +5,11 @@ xmlns:controls="using:Wino.Controls" xmlns:enums="using:Wino.Core.Domain.Enums" xmlns:domain="using:Wino.Core.Domain" + FocusVisualMargin="8" + FocusVisualPrimaryBrush="{StaticResource SystemControlRevealFocusVisualBrush}" + FocusVisualPrimaryThickness="2" + FocusVisualSecondaryBrush="{StaticResource SystemControlFocusVisualSecondaryBrush}" + FocusVisualSecondaryThickness="1" xmlns:helpers="using:Wino.Helpers" PointerEntered="ControlPointerEntered" PointerExited="ControlPointerExited"> @@ -61,8 +66,9 @@ HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="14" - FromAddress="{x:Bind FromAddress, Mode=OneWay}" - FromName="{x:Bind DisplayName, Mode=OneWay}" + SenderContactPicture="{x:Bind MailItem.SenderContact.Base64ContactPicture}" + FromAddress="{x:Bind MailItem.FromAddress, Mode=OneWay}" + FromName="{x:Bind MailItem.FromName, Mode=OneWay}" Visibility="{x:Bind IsAvatarVisible, Mode=OneWay}" /> @@ -100,7 +106,7 @@ @@ -148,7 +154,7 @@ + Text="{x:Bind helpers:XamlHelpers.GetMailItemDisplaySummaryForListing(MailItem.IsDraft, MailItem.CreationDate, Prefer24HourTimeFormat)}" /> @@ -173,10 +179,10 @@ @@ -190,12 +196,12 @@ @@ -209,7 +215,7 @@ - + diff --git a/Wino.Mail/Controls/MailItemDisplayInformationControl.xaml.cs b/Wino.Mail/Controls/MailItemDisplayInformationControl.xaml.cs index 4aee47b7..4b080e1c 100644 --- a/Wino.Mail/Controls/MailItemDisplayInformationControl.xaml.cs +++ b/Wino.Mail/Controls/MailItemDisplayInformationControl.xaml.cs @@ -1,11 +1,10 @@ -using System; -using System.ComponentModel; -using System.Numerics; +using System.Numerics; using System.Windows.Input; using Microsoft.UI.Xaml.Controls; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Input; +using Wino.Core.Domain.Entities; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Models.MailItem; using Wino.Extensions; @@ -13,22 +12,13 @@ using Wino.Mail.ViewModels.Data; namespace Wino.Controls { - public sealed partial class MailItemDisplayInformationControl : UserControl, INotifyPropertyChanged + public sealed partial class MailItemDisplayInformationControl : UserControl { public ImagePreviewControl GetImagePreviewControl() => ContactImage; 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 SnippetProperty = DependencyProperty.Register(nameof(Snippet), typeof(string), typeof(MailItemDisplayInformationControl), new PropertyMetadata(string.Empty)); - public static readonly DependencyProperty FromNameProperty = DependencyProperty.Register(nameof(FromName), typeof(string), typeof(MailItemDisplayInformationControl), new PropertyMetadata(string.Empty)); - public static readonly DependencyProperty SubjectProperty = DependencyProperty.Register(nameof(Subject), typeof(string), typeof(MailItemDisplayInformationControl), new PropertyMetadata("(no-subject)")); - public static readonly DependencyProperty IsReadProperty = DependencyProperty.Register(nameof(IsRead), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(false)); - public static readonly DependencyProperty IsFlaggedProperty = DependencyProperty.Register(nameof(IsFlagged), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(false)); - public static readonly DependencyProperty FromAddressProperty = DependencyProperty.Register(nameof(FromAddress), typeof(string), typeof(MailItemDisplayInformationControl), new PropertyMetadata(string.Empty)); - public static readonly DependencyProperty HasAttachmentsProperty = DependencyProperty.Register(nameof(HasAttachments), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(false)); public static readonly DependencyProperty IsCustomFocusedProperty = DependencyProperty.Register(nameof(IsCustomFocused), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(false)); - public static readonly DependencyProperty ReceivedDateProperty = DependencyProperty.Register(nameof(ReceivedDate), typeof(DateTime), typeof(MailItemDisplayInformationControl), new PropertyMetadata(default(DateTime))); - public static readonly DependencyProperty IsDraftProperty = DependencyProperty.Register(nameof(IsDraft), 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(Expander), typeof(MailItemDisplayInformationControl), new PropertyMetadata(null)); @@ -83,8 +73,6 @@ namespace Wino.Controls } - public event PropertyChangedEventHandler PropertyChanged; - public Expander ConnectedExpander { get { return (Expander)GetValue(ConnectedExpanderProperty); } @@ -103,85 +91,12 @@ namespace Wino.Controls set { SetValue(IsAvatarVisibleProperty, value); } } - public bool IsDraft - { - get { return (bool)GetValue(IsDraftProperty); } - set { SetValue(IsDraftProperty, value); } - } - - public DateTime ReceivedDate - { - get { return (DateTime)GetValue(ReceivedDateProperty); } - set { SetValue(ReceivedDateProperty, value); } - } public bool IsCustomFocused { get { return (bool)GetValue(IsCustomFocusedProperty); } set { SetValue(IsCustomFocusedProperty, value); } } - public bool HasAttachments - { - get { return (bool)GetValue(HasAttachmentsProperty); } - set { SetValue(HasAttachmentsProperty, value); } - } - - public bool IsRead - { - get { return (bool)GetValue(IsReadProperty); } - set { SetValue(IsReadProperty, value); } - } - - public bool IsFlagged - { - get { return (bool)GetValue(IsFlaggedProperty); } - set { SetValue(IsFlaggedProperty, value); } - } - - public string FromAddress - { - get { return (string)GetValue(FromAddressProperty); } - set - { - SetValue(FromAddressProperty, value); - - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DisplayName))); - } - } - - public string DisplayName - { - get - { - if (string.IsNullOrEmpty(FromName)) - return FromAddress; - - return FromName; - } - } - public string FromName - { - get => (string)GetValue(FromNameProperty); - set - { - SetValue(FromNameProperty, value); - - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DisplayName))); - } - } - - public string Subject - { - get { return (string)GetValue(SubjectProperty); } - set { SetValue(SubjectProperty, value); } - } - - public string Snippet - { - get { return (string)GetValue(SnippetProperty); } - set { SetValue(SnippetProperty, value); } - } - public bool ShowPreviewText { get { return (bool)GetValue(ShowPreviewTextProperty); } @@ -234,8 +149,8 @@ namespace Wino.Controls { MailOperationPreperationRequest package = null; - if (MailItem is MailItemViewModel mailItemViewModel) - package = new MailOperationPreperationRequest(operation, mailItemViewModel.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); diff --git a/Wino.Mail/Views/ComposePage.xaml b/Wino.Mail/Views/ComposePage.xaml index 60aac3a0..8abfe33f 100644 --- a/Wino.Mail/Views/ComposePage.xaml +++ b/Wino.Mail/Views/ComposePage.xaml @@ -20,7 +20,7 @@ mc:Ignorable="d"> - + @@ -37,7 +37,7 @@ Text="{x:Bind Name}" /> - + diff --git a/Wino.Mail/Views/ComposePage.xaml.cs b/Wino.Mail/Views/ComposePage.xaml.cs index 166b1722..0e5d591b 100644 --- a/Wino.Mail/Views/ComposePage.xaml.cs +++ b/Wino.Mail/Views/ComposePage.xaml.cs @@ -574,7 +574,7 @@ namespace Wino.Views var deferal = args.GetDeferral(); - AddressInformation addedItem = null; + AccountContact addedItem = null; var boxTag = sender.Tag?.ToString(); @@ -644,8 +644,8 @@ namespace Wino.Views { var boxTag = tokenizingTextBox.Tag?.ToString(); - AddressInformation addedItem = null; - ObservableCollection addressCollection = null; + AccountContact addedItem = null; + ObservableCollection addressCollection = null; if (boxTag == "ToBox") addressCollection = ViewModel.ToItems; diff --git a/Wino.Mail/Views/MailListPage.xaml b/Wino.Mail/Views/MailListPage.xaml index d14307ac..4f706ed7 100644 --- a/Wino.Mail/Views/MailListPage.xaml +++ b/Wino.Mail/Views/MailListPage.xaml @@ -118,29 +118,15 @@ 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" - FromAddress="{x:Bind FromAddress}" - FromName="{x:Bind FromName}" - HasAttachments="{x:Bind HasAttachments}" HoverActionExecutedCommand="{Binding ElementName=root, Path=ViewModel.ExecuteHoverActionCommand}" IsAvatarVisible="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowSenderPicturesEnabled, Mode=OneWay}" IsCustomFocused="{x:Bind IsCustomFocused, Mode=OneWay}" - IsDraft="{x:Bind IsDraft, Mode=OneWay}" - IsFlagged="{x:Bind IsFlagged, Mode=OneWay}" IsHoverActionsEnabled="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsHoverActionsEnabled, Mode=OneWay}" - IsRead="{x:Bind IsRead, Mode=OneWay}" LeftHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.LeftHoverAction, Mode=OneWay}" - MailItem="{Binding}" + MailItem="{x:Bind MailCopy, Mode=OneWay}" Prefer24HourTimeFormat="{Binding ElementName=root, Path=ViewModel.PreferencesService.Prefer24HourTimeFormat, Mode=OneWay}" - ReceivedDate="{x:Bind CreationDate}" RightHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.RightHoverAction, Mode=OneWay}" - ShowPreviewText="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowPreviewEnabled, Mode=OneWay}" - Snippet="{x:Bind PreviewText}" - Subject="{x:Bind Subject}" /> + ShowPreviewText="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowPreviewEnabled, Mode=OneWay}" /> @@ -155,24 +141,15 @@ FocusVisualPrimaryThickness="2" FocusVisualSecondaryBrush="{StaticResource SystemControlFocusVisualSecondaryBrush}" FocusVisualSecondaryThickness="1" - FromAddress="{x:Bind FromAddress}" - FromName="{x:Bind FromName}" - HasAttachments="{x:Bind HasAttachments}" HoverActionExecutedCommand="{Binding ElementName=root, Path=ViewModel.ExecuteHoverActionCommand}" IsAvatarVisible="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowSenderPicturesEnabled, Mode=OneWay}" IsCustomFocused="{x:Bind IsCustomFocused, Mode=OneWay}" - IsDraft="{x:Bind IsDraft, Mode=OneWay}" - IsFlagged="{x:Bind IsFlagged, Mode=OneWay}" IsHoverActionsEnabled="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsHoverActionsEnabled, Mode=OneWay}" - IsRead="{x:Bind IsRead, Mode=OneWay}" LeftHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.LeftHoverAction, Mode=OneWay}" - MailItem="{Binding}" + MailItem="{x:Bind MailCopy, Mode=OneWay}" Prefer24HourTimeFormat="{Binding ElementName=root, Path=ViewModel.PreferencesService.Prefer24HourTimeFormat, Mode=OneWay}" - ReceivedDate="{x:Bind CreationDate}" RightHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.RightHoverAction, Mode=OneWay}" - ShowPreviewText="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowPreviewEnabled, Mode=OneWay}" - Snippet="{x:Bind PreviewText}" - Subject="{x:Bind Subject}" /> + ShowPreviewText="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowPreviewEnabled, Mode=OneWay}" /> @@ -199,24 +176,15 @@ DisplayMode="{Binding ElementName=root, Path=ViewModel.PreferencesService.MailItemDisplayMode, Mode=OneWay}" DragStarting="ThreadHeaderDragStart" DropCompleted="ThreadHeaderDragFinished" - FromAddress="{x:Bind FromAddress}" - FromName="{x:Bind FromName}" - HasAttachments="{x:Bind HasAttachments}" HoverActionExecutedCommand="{Binding ElementName=root, Path=ViewModel.ExecuteHoverActionCommand}" IsAvatarVisible="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowSenderPicturesEnabled, Mode=OneWay}" - IsDraft="{x:Bind IsDraft}" - IsFlagged="{x:Bind IsFlagged}" IsHitTestVisible="True" IsHoverActionsEnabled="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsHoverActionsEnabled, Mode=OneWay}" - IsRead="{x:Bind IsRead}" LeftHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.LeftHoverAction, Mode=OneWay}" - MailItem="{Binding}" + MailItem="{Binding Mode=OneWay}" Prefer24HourTimeFormat="{Binding ElementName=root, Path=ViewModel.PreferencesService.Prefer24HourTimeFormat, Mode=OneWay}" - ReceivedDate="{x:Bind CreationDate}" RightHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.RightHoverAction, Mode=OneWay}" - ShowPreviewText="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowPreviewEnabled, Mode=OneWay}" - Snippet="{x:Bind PreviewText}" - Subject="{x:Bind Subject}" /> + ShowPreviewText="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowPreviewEnabled, Mode=OneWay}" /> + - + - - + + - + @@ -63,26 +67,29 @@ - + - + - + @@ -95,41 +102,46 @@ - + - + - + - + - + @@ -137,18 +149,20 @@ - + - + @@ -163,35 +177,40 @@ - + - + - + - + - + @@ -204,21 +223,24 @@ - + - + - + @@ -230,25 +252,29 @@ - + - + - - + + @@ -258,13 +284,15 @@ - - + + @@ -274,13 +302,15 @@ - - + + @@ -297,15 +327,16 @@ - + @@ -313,57 +344,64 @@ - + - + -