Adding contact details for loaded mails and fixing background notification actions.

This commit is contained in:
Burak Kaan Köse
2024-08-23 01:07:00 +02:00
parent 0fbeb11304
commit f45580be70
26 changed files with 523 additions and 427 deletions

View File

@@ -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.
/// </summary>
// TODO: This can easily evolve to Contact store, just like People app in Windows 10/11.
// Do it.
public class AddressInformation : IEquatable<AddressInformation>
public class AccountContact : IEquatable<AccountContact>
{
[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<AddressInformation>.Default.Equals(left, right);
return EqualityComparer<AccountContact>.Default.Equals(left, right);
}
public static bool operator !=(AddressInformation left, AddressInformation right)
public static bool operator !=(AccountContact left, AccountContact right)
{
return !(left == right);
}

View File

@@ -141,6 +141,15 @@ namespace Wino.Core.Domain.Entities
/// </summary>
[Ignore]
public MailAccount AssignedAccount { get; set; }
/// <summary>
/// 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.
/// </summary>
[Ignore]
public AccountContact SenderContact { get; set; }
public IEnumerable<Guid> GetContainingIds() => [UniqueId];
public override string ToString() => $"{Subject} <-> {Id}";
}

View File

@@ -1,10 +1,18 @@
namespace Wino.Core.Domain.Interfaces
using System.Threading.Tasks;
namespace Wino.Core.Domain.Interfaces
{
public interface IBackgroundTaskService
{
/// <summary>
/// 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.
/// </summary>
void UnregisterAllBackgroundTask();
/// <summary>
/// Registers required background tasks.
/// </summary>
Task RegisterBackgroundTasksAsync();
}
}

View File

@@ -29,5 +29,6 @@ namespace Wino.Core.Domain.Models.MailItem
MailItemFolder AssignedFolder { get; }
MailAccount AssignedAccount { get; }
AccountContact SenderContact { get; }
}
}

View File

@@ -85,6 +85,8 @@ namespace Wino.Core.Domain.Models.MailItem
public Guid FileId => LatestMailItem?.FileId ?? Guid.Empty;
public AccountContact SenderContact => LatestMailItem?.SenderContact;
#endregion
}
}

View File

@@ -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();
}
}
}

View File

@@ -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 };
}
/// <summary>

View File

@@ -10,8 +10,8 @@ namespace Wino.Core.Services
{
public interface IContactService
{
Task<List<AddressInformation>> GetAddressInformationAsync(string queryText);
Task<AddressInformation> GetAddressInformationByAddressAsync(string address);
Task<List<AccountContact>> GetAddressInformationAsync(string queryText);
Task<AccountContact> GetAddressInformationByAddressAsync(string address);
Task SaveAddressInformationAsync(MimeMessage message);
}
@@ -19,24 +19,24 @@ namespace Wino.Core.Services
{
public ContactService(IDatabaseService databaseService) : base(databaseService) { }
public Task<List<AddressInformation>> GetAddressInformationAsync(string queryText)
public Task<List<AccountContact>> GetAddressInformationAsync(string queryText)
{
if (queryText == null || queryText.Length < 2)
return Task.FromResult<List<AddressInformation>>(null);
return Task.FromResult<List<AccountContact>>(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<AddressInformation>(rawLikeQuery);
return Connection.QueryAsync<AccountContact>(rawLikeQuery);
}
public async Task<AddressInformation> GetAddressInformationByAddressAsync(string address)
public async Task<AccountContact> GetAddressInformationByAddressAsync(string address)
{
return await Connection.Table<AddressInformation>().Where(a => a.Address == address).FirstOrDefaultAsync()
?? new AddressInformation() { Name = address, Address = address };
return await Connection.Table<AccountContact>().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);

View File

@@ -57,7 +57,7 @@ namespace Wino.Core.Services
typeof(MailItemFolder),
typeof(MailAccount),
typeof(TokenInformation),
typeof(AddressInformation),
typeof(AccountContact),
typeof(CustomServerInformation),
typeof(AccountSignature),
typeof(MergedInbox),

View File

@@ -201,6 +201,7 @@ namespace Wino.Core.Services
Dictionary<Guid, MailItemFolder> folderCache = [];
Dictionary<Guid, MailAccount> accountCache = [];
Dictionary<string, AccountContact> 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<Guid, MailItemFolder> folderCache, Dictionary<Guid, MailAccount> accountCache)
async Task LoadAssignedPropertiesWithCacheAsync(IMailItem mail,
Dictionary<Guid, MailItemFolder> folderCache,
Dictionary<Guid, MailAccount> 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<AccountContact> 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<MailCopy> 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.

View File

@@ -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;

View File

@@ -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);

View File

@@ -85,9 +85,9 @@ namespace Wino.Mail.ViewModels
public ObservableCollection<MailAttachmentViewModel> IncludedAttachments { get; set; } = [];
public ObservableCollection<MailAccount> Accounts { get; set; } = [];
public ObservableCollection<AddressInformation> ToItems { get; set; } = [];
public ObservableCollection<AddressInformation> CCItems { get; set; } = [];
public ObservableCollection<AddressInformation> BCCItems { get; set; } = [];
public ObservableCollection<AccountContact> ToItems { get; set; } = [];
public ObservableCollection<AccountContact> CCItems { get; set; } = [];
public ObservableCollection<AccountContact> BCCItems { get; set; } = [];
public List<EditorToolbarSection> ToolbarSections { get; set; } =
@@ -476,7 +476,7 @@ namespace Wino.Mail.ViewModels
}
}
private void LoadAddressInfo(InternetAddressList list, ObservableCollection<AddressInformation> collection)
private void LoadAddressInfo(InternetAddressList list, ObservableCollection<AccountContact> collection)
{
foreach (var item in list)
{
@@ -509,7 +509,7 @@ namespace Wino.Mail.ViewModels
}
}
private void SaveAddressInfo(IEnumerable<AddressInformation> addresses, InternetAddressList list)
private void SaveAddressInfo(IEnumerable<AccountContact> addresses, InternetAddressList list)
{
list.Clear();
@@ -517,7 +517,7 @@ namespace Wino.Mail.ViewModels
list.Add(new MailboxAddress(item.Name, item.Address));
}
public async Task<AddressInformation> GetAddressInformationAsync(string tokenText, ObservableCollection<AddressInformation> collection)
public async Task<AccountContact> GetAddressInformationAsync(string tokenText, ObservableCollection<AccountContact> 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();

View File

@@ -11,12 +11,13 @@ namespace Wino.Mail.ViewModels.Data
/// </summary>
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<Guid> GetContainingIds() => new[] { UniqueId };
}

View File

@@ -15,6 +15,7 @@ namespace Wino.Mail.ViewModels.Data
public partial class ThreadMailItemViewModel : ObservableObject, IMailItemThread, IComparable<string>, IComparable<DateTime>
{
public ObservableCollection<IMailItem> ThreadItems => ((IMailItemThread)_threadMailItem).ThreadItems;
public AccountContact SenderContact => ((IMailItemThread)_threadMailItem).SenderContact;
private readonly ThreadMailItem _threadMailItem;

View File

@@ -108,9 +108,9 @@ namespace Wino.Mail.ViewModels
private DateTime creationDate;
public ObservableCollection<AddressInformation> ToItems { get; set; } = new ObservableCollection<AddressInformation>();
public ObservableCollection<AddressInformation> CCItemsItems { get; set; } = new ObservableCollection<AddressInformation>();
public ObservableCollection<AddressInformation> BCCItems { get; set; } = new ObservableCollection<AddressInformation>();
public ObservableCollection<AccountContact> ToItems { get; set; } = new ObservableCollection<AccountContact>();
public ObservableCollection<AccountContact> CCItemsItems { get; set; } = new ObservableCollection<AccountContact>();
public ObservableCollection<AccountContact> BCCItems { get; set; } = new ObservableCollection<AccountContact>();
public ObservableCollection<MailAttachmentViewModel> Attachments { get; set; } = new ObservableCollection<MailAttachmentViewModel>();
public ObservableCollection<MailOperationMenuItem> MenuItems { get; set; } = new ObservableCollection<MailOperationMenuItem>();
@@ -470,7 +470,7 @@ namespace Wino.Mail.ViewModels
StatePersistenceService.IsReadingMail = false;
}
private void LoadAddressInfo(InternetAddressList list, ObservableCollection<AddressInformation> collection)
private void LoadAddressInfo(InternetAddressList list, ObservableCollection<AccountContact> collection)
{
collection.Clear();

View File

@@ -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();

View File

@@ -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));
}
}
});
}

View File

@@ -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)));
/// <summary>
/// Gets or sets base64 string of the sender contact picture.
/// </summary>
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);
}
}
}
/// <summary>
/// Tries to update contact image with the provided base64 image string.
/// </summary>
/// <returns>True if updated, false if not.</returns>
private async Task<bool> 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.

View File

@@ -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}" />
<Grid
@@ -90,7 +96,7 @@
<TextBlock
x:Name="DraftTitle"
Margin="0,0,4,0"
x:Load="{x:Bind IsDraft, Mode=OneWay}"
x:Load="{x:Bind MailItem.IsDraft, Mode=OneWay}"
Foreground="{StaticResource DeleteBrush}">
<Run Text="[" /><Run Text="{x:Bind domain:Translator.Draft}" /><Run Text="]" /> <Run Text=" " />
@@ -100,7 +106,7 @@
<TextBlock
x:Name="SenderText"
Grid.Column="1"
Text="{x:Bind DisplayName}"
Text="{x:Bind MailItem.FromName}"
TextTrimming="WordEllipsis" />
<!-- Hover button -->
@@ -148,7 +154,7 @@
<TextBlock
x:Name="TitleText"
MaxLines="1"
Text="{x:Bind Subject}"
Text="{x:Bind MailItem.Subject}"
TextTrimming="CharacterEllipsis" />
<TextBlock
@@ -157,7 +163,7 @@
VerticalAlignment="Center"
FontSize="11"
Opacity="0.7"
Text="{x:Bind helpers:XamlHelpers.GetMailItemDisplaySummaryForListing(IsDraft, ReceivedDate, Prefer24HourTimeFormat)}" />
Text="{x:Bind helpers:XamlHelpers.GetMailItemDisplaySummaryForListing(MailItem.IsDraft, MailItem.CreationDate, Prefer24HourTimeFormat)}" />
</Grid>
<!-- Message -->
@@ -173,10 +179,10 @@
<Grid x:Name="PreviewTextContainer">
<TextBlock
x:Name="PreviewTextblock"
x:Load="{x:Bind helpers:XamlHelpers.ShouldDisplayPreview(Snippet), Mode=OneWay}"
x:Load="{x:Bind helpers:XamlHelpers.ShouldDisplayPreview(MailItem.PreviewText), Mode=OneWay}"
MaxLines="1"
Opacity="0.7"
Text="{x:Bind Snippet}"
Text="{x:Bind MailItem.PreviewText}"
TextTrimming="CharacterEllipsis" />
</Grid>
@@ -190,12 +196,12 @@
<ContentPresenter
x:Name="HasAttachmentContent"
x:Load="{x:Bind HasAttachments, Mode=OneWay}"
x:Load="{x:Bind MailItem.HasAttachments, Mode=OneWay}"
ContentTemplate="{StaticResource AttachmentSymbolControlTemplate}" />
<ContentPresenter
x:Name="IsFlaggedContent"
x:Load="{x:Bind IsFlagged, Mode=OneWay}"
x:Load="{x:Bind MailItem.IsFlagged, Mode=OneWay}"
ContentTemplate="{StaticResource FlaggedSymbolControlTemplate}" />
</StackPanel>
</Grid>
@@ -209,7 +215,7 @@
<VisualStateGroup x:Name="ReadStates">
<VisualState x:Name="Unread">
<VisualState.StateTriggers>
<StateTrigger IsActive="{x:Bind IsRead, Converter={StaticResource ReverseBooleanConverter}, Mode=OneWay}" />
<StateTrigger IsActive="{x:Bind MailItem.IsRead, Converter={StaticResource ReverseBooleanConverter}, Mode=OneWay}" />
</VisualState.StateTriggers>
<VisualState.Setters>

View File

@@ -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);

View File

@@ -20,7 +20,7 @@
mc:Ignorable="d">
<Page.Resources>
<DataTemplate x:Key="TokenBoxTemplate" x:DataType="entities:AddressInformation">
<DataTemplate x:Key="TokenBoxTemplate" x:DataType="entities:AccountContact">
<Grid>
<ToolTipService.ToolTip>
<ToolTip Content="{x:Bind Address}" />
@@ -37,7 +37,7 @@
Text="{x:Bind Name}" />
</Grid>
</DataTemplate>
<DataTemplate x:Key="SuggestionBoxTemplate" x:DataType="entities:AddressInformation">
<DataTemplate x:Key="SuggestionBoxTemplate" x:DataType="entities:AccountContact">
<Grid Margin="0,12" ColumnSpacing="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />

View File

@@ -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<AddressInformation> addressCollection = null;
AccountContact addedItem = null;
ObservableCollection<AccountContact> addressCollection = null;
if (boxTag == "ToBox")
addressCollection = ViewModel.ToItems;

View File

@@ -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}" />
</DataTemplate>
<!-- Single Mail Item Template for Threads -->
@@ -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}" />
</DataTemplate>
<!-- Mail Item Content Selector -->
@@ -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}" />
</muxc:Expander.Header>
<muxc:Expander.Content>
<listview:WinoListView

File diff suppressed because one or more lines are too long

View File

@@ -7,6 +7,7 @@
xmlns:controls1="using:Wino.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:domain="using:Wino.Core.Domain"
x:Name="root"
xmlns:enums="using:Wino.Core.Domain.Enums"
xmlns:helpers="using:Wino.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -141,28 +142,22 @@
<DataTemplate x:Key="CompactDisplayModePreviewTemplate" x:DataType="enums:MailListDisplayMode">
<controls1:MailItemDisplayInformationControl
DisplayMode="Compact"
FromName="{x:Bind domain:Translator.SettingsPersonalizationMailDisplayCompactMode}"
ShowPreviewText="False"
Snippet="Thank you for using Wino Mail."
Subject="Welcome to Wino Mail" />
MailItem="{Binding ElementName=root, Path=ViewModel.DemoPreviewMailCopy}"
ShowPreviewText="False" />
</DataTemplate>
<DataTemplate x:Key="MediumDisplayModePreviewTemplate" x:DataType="enums:MailListDisplayMode">
<controls1:MailItemDisplayInformationControl
DisplayMode="Medium"
FromName="{x:Bind domain:Translator.SettingsPersonalizationMailDisplayMediumMode}"
ShowPreviewText="True"
Snippet="Thank you for using Wino Mail."
Subject="Welcome to Wino Mail" />
MailItem="{Binding ElementName=root, Path=ViewModel.DemoPreviewMailCopy}" />
</DataTemplate>
<DataTemplate x:Key="SpaciousDisplayModePreviewTemplate" x:DataType="enums:MailListDisplayMode">
<controls1:MailItemDisplayInformationControl
DisplayMode="Spacious"
FromName="{x:Bind domain:Translator.SettingsPersonalizationMailDisplaySpaciousMode}"
ShowPreviewText="True"
Snippet="Thank you for using Wino Mail."
Subject="Welcome to Wino Mail" />
MailItem="{Binding ElementName=root, Path=ViewModel.DemoPreviewMailCopy}"
ShowPreviewText="True" />
</DataTemplate>
<selectors:MailItemDisplayModePreviewTemplateSelector