From 256fd1cce24994e1e85ba7ec898f1da1732f7890 Mon Sep 17 00:00:00 2001 From: Maicol Battistini Date: Sat, 21 Jun 2025 01:40:25 +0200 Subject: [PATCH] feat: Enhanced sender avatars with gravatar and favicons integration (#685) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Enhanced sender avatars with gravatar and favicons integration * chore: Remove unused known companies thumbnails * feat(thumbnail): add IThumbnailService and refactor usage - Introduced a new interface `IThumbnailService` for handling thumbnail-related functionalities. - Registered `IThumbnailService` with its implementation `ThumbnailService` in the service container. - Updated `NotificationBuilder` to use an instance of `IThumbnailService` instead of static methods. - Refactored `ThumbnailService` from a static class to a regular class with instance methods and variables. - Modified `ImagePreviewControl` to utilize the new `IThumbnailService` instance. - Completed integration of `IThumbnailService` in the application by registering it in `App.xaml.cs`. * style: Show favicons as squares - Changed `hintCrop` in `NotificationBuilder` to `None` for app logo display. - Added `FaviconSquircle`, `FaviconImage`, and `isFavicon` to `ImagePreviewControl` for favicon handling. - Updated `UpdateInformation` method to manage favicon visibility. - Introduced `GetBitmapImageAsync` for converting Base64 to Bitmap images. - Enhanced XAML to include `FaviconSquircle` for improved UI appearance. * refactor thumbnail service * Removed old code and added clear method * added prefetch function * Change key from host to email * Remove redundant code * Test event * Fixed an issue with the thumbnail updated event. * Fix cutted favicons * exclude some domain from favicons * add yandex.ru * fix buttons in settings * remove prefetch method * Added thumbnails propagation to mailRenderingPage * Revert MailItemViewModel to object * Remove redundant code * spaces * await load parameter added * fix spaces * fix case sensativity for mail list thumbnails * change duckdns to google * Some cleanup. --------- Co-authored-by: Aleh Khantsevich Co-authored-by: Burak Kaan Köse --- Directory.Packages.props | 1 + Wino.Core.Domain/Entities/Shared/Thumbnail.cs | 14 ++ .../Interfaces/IPreferencesService.cs | 13 +- .../Interfaces/IThumbnailService.cs | 20 ++ .../Translations/en_US/resources.json | 5 + Wino.Core.UWP/CoreUWPContainerSetup.cs | 2 +- Wino.Core.UWP/Services/NativeAppService.cs | 1 - Wino.Core.UWP/Services/NotificationBuilder.cs | 33 ++- Wino.Core.UWP/Services/PreferencesService.cs | 21 +- Wino.Core.UWP/Services/ThumbnailService.cs | 213 ++++++++++++++---- Wino.Core.UWP/Wino.Core.UWP.csproj | 1 + Wino.Core.UWP/WinoApplication.cs | 2 + .../AppPreferencesPageViewModel.cs | 1 - .../Collections/WinoMailCollection.cs | 23 +- .../Data/AccountContactViewModel.cs | 14 +- .../Data/MailItemViewModel.cs | 9 +- Wino.Mail.ViewModels/MailListPageViewModel.cs | 5 +- .../MailRenderingPageViewModel.cs | 25 ++ .../MessageListPageViewModel.cs | 24 +- Wino.Mail/Assets/Thumbnails/airbnb.com.png | Bin 2385 -> 0 bytes Wino.Mail/Assets/Thumbnails/apple.com.png | Bin 2113 -> 0 bytes Wino.Mail/Assets/Thumbnails/google.com.png | Bin 2722 -> 0 bytes Wino.Mail/Assets/Thumbnails/microsoft.com.png | Bin 574 -> 0 bytes .../Assets/Thumbnails/steampowered.com.png | Bin 3396 -> 0 bytes Wino.Mail/Assets/Thumbnails/uber.com.png | Bin 1392 -> 0 bytes Wino.Mail/Assets/Thumbnails/youtube.com.png | Bin 2641 -> 0 bytes Wino.Mail/Controls/ImagePreviewControl.cs | 143 +++++++----- .../MailItemDisplayInformationControl.xaml | 1 + .../MailItemDisplayInformationControl.xaml.cs | 8 + Wino.Mail/Styles/ImagePreviewControl.xaml | 16 +- Wino.Mail/Views/MailListPage.xaml | 2 + Wino.Mail/Views/MailRenderingPage.xaml | 3 +- Wino.Mail/Views/Settings/MessageListPage.xaml | 28 ++- Wino.Mail/Wino.Mail.csproj | 14 -- Wino.Messages/UI/ThumbnailAdded.cs | 4 + Wino.Server/App.xaml.cs | 1 + Wino.Server/Wino.Server.csproj | 11 +- Wino.Services/DatabaseService.cs | 3 +- 38 files changed, 489 insertions(+), 172 deletions(-) create mode 100644 Wino.Core.Domain/Entities/Shared/Thumbnail.cs create mode 100644 Wino.Core.Domain/Interfaces/IThumbnailService.cs delete mode 100644 Wino.Mail/Assets/Thumbnails/airbnb.com.png delete mode 100644 Wino.Mail/Assets/Thumbnails/apple.com.png delete mode 100644 Wino.Mail/Assets/Thumbnails/google.com.png delete mode 100644 Wino.Mail/Assets/Thumbnails/microsoft.com.png delete mode 100644 Wino.Mail/Assets/Thumbnails/steampowered.com.png delete mode 100644 Wino.Mail/Assets/Thumbnails/uber.com.png delete mode 100644 Wino.Mail/Assets/Thumbnails/youtube.com.png create mode 100644 Wino.Messages/UI/ThumbnailAdded.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 8b347682..f97ada3f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -19,6 +19,7 @@ + diff --git a/Wino.Core.Domain/Entities/Shared/Thumbnail.cs b/Wino.Core.Domain/Entities/Shared/Thumbnail.cs new file mode 100644 index 00000000..3fd68c0f --- /dev/null +++ b/Wino.Core.Domain/Entities/Shared/Thumbnail.cs @@ -0,0 +1,14 @@ +using System; +using SQLite; + +namespace Wino.Core.Domain.Entities.Shared; + +public class Thumbnail +{ + [PrimaryKey] + public string Domain { get; set; } + + public string Gravatar { get; set; } + public string Favicon { get; set; } + public DateTime LastUpdated { get; set; } +} diff --git a/Wino.Core.Domain/Interfaces/IPreferencesService.cs b/Wino.Core.Domain/Interfaces/IPreferencesService.cs index 11575f8f..345eccb1 100644 --- a/Wino.Core.Domain/Interfaces/IPreferencesService.cs +++ b/Wino.Core.Domain/Interfaces/IPreferencesService.cs @@ -1,11 +1,12 @@ using System; +using System.ComponentModel; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Models.Calendar; using Wino.Core.Domain.Models.Reader; namespace Wino.Core.Domain.Interfaces; -public interface IPreferencesService +public interface IPreferencesService: INotifyPropertyChanged { /// /// When any of the preferences are changed. @@ -193,6 +194,16 @@ public interface IPreferencesService /// bool IsShowActionLabelsEnabled { get; set; } + /// + /// Setting: Enable/disable Gravatar for sender avatars. + /// + bool IsGravatarEnabled { get; set; } + + /// + /// Setting: Enable/disable Favicon for sender avatars. + /// + bool IsFaviconEnabled { get; set; } + #endregion #region Calendar diff --git a/Wino.Core.Domain/Interfaces/IThumbnailService.cs b/Wino.Core.Domain/Interfaces/IThumbnailService.cs new file mode 100644 index 00000000..7c95a2f8 --- /dev/null +++ b/Wino.Core.Domain/Interfaces/IThumbnailService.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; + +namespace Wino.Core.Domain.Interfaces; + +public interface IThumbnailService +{ + /// + /// Clears the thumbnail cache. + /// + Task ClearCache(); + + /// + /// Gets thumbnail + /// + /// Address for thumbnail + /// Force to wait for thumbnail loading. + /// Should be used in non-UI threads or where delay is acceptable + /// + ValueTask GetThumbnailAsync(string email, bool awaitLoad = false); +} diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json index 7b67d5eb..3fc21dca 100644 --- a/Wino.Core.Domain/Translations/en_US/resources.json +++ b/Wino.Core.Domain/Translations/en_US/resources.json @@ -623,6 +623,11 @@ "SettingsShowPreviewText_Title": "Show Preview Text", "SettingsShowSenderPictures_Description": "Hide/show the thumbnail sender pictures.", "SettingsShowSenderPictures_Title": "Show Sender Avatars", + "SettingsEnableGravatarAvatars_Title": "Gravatar", + "SettingsEnableGravatarAvatars_Description": "Use gravatar (if available) as sender picture", + "SettingsEnableFavicons_Title": "Domain icons (Favicons)", + "SettingsEnableFavicons_Description": "Use domain favicons (if available) as sender picture", + "SettingsMailList_ClearAvatarsCache_Button": "Clear cached avatars", "SettingsSignature_AddCustomSignature_Button": "Add signature", "SettingsSignature_AddCustomSignature_Title": "Add custom signature", "SettingsSignature_DeleteSignature_Title": "Delete signature", diff --git a/Wino.Core.UWP/CoreUWPContainerSetup.cs b/Wino.Core.UWP/CoreUWPContainerSetup.cs index ddb230b6..5869d67e 100644 --- a/Wino.Core.UWP/CoreUWPContainerSetup.cs +++ b/Wino.Core.UWP/CoreUWPContainerSetup.cs @@ -25,7 +25,7 @@ public static class CoreUWPContainerSetup services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - + services.AddSingleton(); services.AddSingleton(); services.AddTransient(); services.AddTransient(); diff --git a/Wino.Core.UWP/Services/NativeAppService.cs b/Wino.Core.UWP/Services/NativeAppService.cs index 63666cc3..eb38b19e 100644 --- a/Wino.Core.UWP/Services/NativeAppService.cs +++ b/Wino.Core.UWP/Services/NativeAppService.cs @@ -45,7 +45,6 @@ public class NativeAppService : INativeAppService return _mimeMessagesFolder; } - public async Task GetEditorBundlePathAsync() { if (string.IsNullOrEmpty(_editorBundlePath)) diff --git a/Wino.Core.UWP/Services/NotificationBuilder.cs b/Wino.Core.UWP/Services/NotificationBuilder.cs index 3504869e..6770c297 100644 --- a/Wino.Core.UWP/Services/NotificationBuilder.cs +++ b/Wino.Core.UWP/Services/NotificationBuilder.cs @@ -11,7 +11,7 @@ using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.MailItem; -using Wino.Core.Services; +using System.IO; namespace Wino.Core.UWP.Services; @@ -23,16 +23,19 @@ public class NotificationBuilder : INotificationBuilder private readonly IAccountService _accountService; private readonly IFolderService _folderService; private readonly IMailService _mailService; + private readonly IThumbnailService _thumbnailService; public NotificationBuilder(IUnderlyingThemeService underlyingThemeService, IAccountService accountService, IFolderService folderService, - IMailService mailService) + IMailService mailService, + IThumbnailService thumbnailService) { _underlyingThemeService = underlyingThemeService; _accountService = accountService; _folderService = folderService; _mailService = mailService; + _thumbnailService = thumbnailService; } public async Task CreateNotificationsAsync(Guid inboxFolderId, IEnumerable downloadedMailItems) @@ -83,24 +86,16 @@ public class NotificationBuilder : INotificationBuilder var builder = new ToastContentBuilder(); builder.SetToastScenario(ToastScenario.Default); - var host = ThumbnailService.GetHost(mailItem.FromAddress); - - var knownTuple = ThumbnailService.CheckIsKnown(host); - - bool isKnown = knownTuple.Item1; - host = knownTuple.Item2; - - if (isKnown) - builder.AddAppLogoOverride(new System.Uri(ThumbnailService.GetKnownHostImage(host)), hintCrop: ToastGenericAppLogoCrop.Default); - else + var avatarThumbnail = await _thumbnailService.GetThumbnailAsync(mailItem.FromAddress, awaitLoad: true); + if (!string.IsNullOrEmpty(avatarThumbnail)) { - // TODO: https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts?tabs=toolkit - // Follow official guides for icons/theme. - - bool isOSDarkTheme = _underlyingThemeService.IsUnderlyingThemeDark(); - string profileLogoName = isOSDarkTheme ? "profile-dark.png" : "profile-light.png"; - - builder.AddAppLogoOverride(new System.Uri($"ms-appx:///Assets/NotificationIcons/{profileLogoName}"), hintCrop: ToastGenericAppLogoCrop.Circle); + var tempFile = await Windows.Storage.ApplicationData.Current.TemporaryFolder.CreateFileAsync($"{Guid.NewGuid()}.png", Windows.Storage.CreationCollisionOption.ReplaceExisting); + await using (var stream = await tempFile.OpenStreamForWriteAsync()) + { + var bytes = Convert.FromBase64String(avatarThumbnail); + await stream.WriteAsync(bytes); + } + builder.AddAppLogoOverride(new Uri($"ms-appdata:///temp/{tempFile.Name}"), hintCrop: ToastGenericAppLogoCrop.Default); } // Override system notification timetamp with received date of the mail. diff --git a/Wino.Core.UWP/Services/PreferencesService.cs b/Wino.Core.UWP/Services/PreferencesService.cs index bdeffed2..ea88910e 100644 --- a/Wino.Core.UWP/Services/PreferencesService.cs +++ b/Wino.Core.UWP/Services/PreferencesService.cs @@ -13,17 +13,12 @@ using Wino.Services; namespace Wino.Core.UWP.Services; -public class PreferencesService : ObservableObject, IPreferencesService +public class PreferencesService(IConfigurationService configurationService) : ObservableObject, IPreferencesService { - private readonly IConfigurationService _configurationService; + private readonly IConfigurationService _configurationService = configurationService; public event EventHandler PreferenceChanged; - public PreferencesService(IConfigurationService configurationService) - { - _configurationService = configurationService; - } - protected override void OnPropertyChanged(PropertyChangedEventArgs e) { base.OnPropertyChanged(e); @@ -181,6 +176,18 @@ public class PreferencesService : ObservableObject, IPreferencesService set => SetPropertyAndSave(nameof(IsMailkitProtocolLoggerEnabled), value); } + public bool IsGravatarEnabled + { + get => _configurationService.Get(nameof(IsGravatarEnabled), true); + set => SetPropertyAndSave(nameof(IsGravatarEnabled), value); + } + + public bool IsFaviconEnabled + { + get => _configurationService.Get(nameof(IsFaviconEnabled), true); + set => SetPropertyAndSave(nameof(IsFaviconEnabled), value); + } + public Guid? StartupEntityId { get => _configurationService.Get(nameof(StartupEntityId), null); diff --git a/Wino.Core.UWP/Services/ThumbnailService.cs b/Wino.Core.UWP/Services/ThumbnailService.cs index dd042b79..7598252a 100644 --- a/Wino.Core.UWP/Services/ThumbnailService.cs +++ b/Wino.Core.UWP/Services/ThumbnailService.cs @@ -1,63 +1,198 @@ using System; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Net.Mail; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.Messaging; +using Gravatar; +using Windows.Networking.Connectivity; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Interfaces; +using Wino.Messaging.UI; +using Wino.Services; namespace Wino.Core.UWP.Services; -public static class ThumbnailService +public class ThumbnailService(IPreferencesService preferencesService, IDatabaseService databaseService) : IThumbnailService { - private static string[] knownCompanies = new string[] + private readonly IPreferencesService _preferencesService = preferencesService; + private readonly IDatabaseService _databaseService = databaseService; + private static readonly HttpClient _httpClient = new(); + private bool _isInitialized = false; + + private ConcurrentDictionary _cache; + private readonly ConcurrentDictionary _requests = []; + + private static readonly List _excludedFaviconDomains = [ + "gmail.com", + "outlook.com", + "hotmail.com", + "live.com", + "yahoo.com", + "icloud.com", + "aol.com", + "protonmail.com", + "zoho.com", + "mail.com", + "gmx.com", + "yandex.com", + "yandex.ru", + "tutanota.com", + "mail.ru", + "rediffmail.com" + ]; + + public async ValueTask GetThumbnailAsync(string email, bool awaitLoad = false) { - "microsoft.com", "apple.com", "google.com", "steampowered.com", "airbnb.com", "youtube.com", "uber.com" - }; + if (string.IsNullOrWhiteSpace(email)) + return null; - public static bool IsKnown(string mailHost) => !string.IsNullOrEmpty(mailHost) && knownCompanies.Contains(mailHost); + if (!_preferencesService.IsShowSenderPicturesEnabled) + return null; - public static string GetHost(string address) - { - if (string.IsNullOrEmpty(address)) - return string.Empty; - - if (address.Contains('@')) + if (!_isInitialized) { - var splitted = address.Split('@'); + var thumbnailsList = await _databaseService.Connection.Table().ToListAsync(); - if (splitted.Length >= 2 && !string.IsNullOrEmpty(splitted[1])) - { - try - { - return new MailAddress(address).Host; - } - catch (Exception) - { - // TODO: Exceptions are ignored for now. - } - } + _cache = new ConcurrentDictionary( + thumbnailsList.ToDictionary(x => x.Domain, x => (x.Gravatar, x.Favicon))); + _isInitialized = true; } - return string.Empty; + var sanitizedEmail = email.Trim().ToLowerInvariant(); + + var (gravatar, favicon) = await GetThumbnailInternal(sanitizedEmail, awaitLoad); + + if (_preferencesService.IsGravatarEnabled && !string.IsNullOrEmpty(gravatar)) + { + return gravatar; + } + + if (_preferencesService.IsFaviconEnabled && !string.IsNullOrEmpty(favicon)) + { + return favicon; + } + + return null; } - public static Tuple CheckIsKnown(string host) + public async Task ClearCache() { - // Check known hosts. - // Apply company logo if available. + _cache?.Clear(); + _requests.Clear(); + await _databaseService.Connection.DeleteAllAsync(); + } + private async ValueTask<(string gravatar, string favicon)> GetThumbnailInternal(string email, bool awaitLoad) + { + if (_cache.TryGetValue(email, out var cached)) + return cached; + + // No network available, skip fetching Gravatar + // Do not cache it, since network can be available later + bool isInternetAvailable = GetIsInternetAvailable(); + + if (!isInternetAvailable) + return default; + + if (!_requests.TryGetValue(email, out var request)) + { + request = Task.Run(() => RequestNewThumbnail(email)); + _requests[email] = request; + } + + if (awaitLoad) + { + await request; + _cache.TryGetValue(email, out cached); + return cached; + } + + return default; + + static bool GetIsInternetAvailable() + { + var connection = NetworkInformation.GetInternetConnectionProfile(); + return connection != null && connection.GetNetworkConnectivityLevel() == NetworkConnectivityLevel.InternetAccess; + } + } + + private async Task RequestNewThumbnail(string email) + { + var gravatarBase64 = await GetGravatarBase64(email); + var faviconBase64 = await GetFaviconBase64(email); + + await _databaseService.Connection.InsertOrReplaceAsync(new Thumbnail + { + Domain = email, + Gravatar = gravatarBase64, + Favicon = faviconBase64, + LastUpdated = DateTime.UtcNow + }); + _ = _cache.TryAdd(email, (gravatarBase64, faviconBase64)); + + WeakReferenceMessenger.Default.Send(new ThumbnailAdded(email)); + } + + private static async Task GetGravatarBase64(string email) + { try { - var last = host.Split('.'); - - if (last.Length > 2) - host = $"{last[last.Length - 2]}.{last[last.Length - 1]}"; + var gravatarUrl = GravatarHelper.GetAvatarUrl( + email, + size: 128, + defaultValue: GravatarAvatarDefault.Blank, + withFileExtension: false).ToString().Replace("d=blank", "d=404"); + var response = await _httpClient.GetAsync(gravatarUrl); + if (response.IsSuccessStatusCode) + { + var bytes = response.Content.ReadAsByteArrayAsync().Result; + return Convert.ToBase64String(bytes); + } } - catch (Exception) - { - return new Tuple(false, host); - } - - return new Tuple(IsKnown(host), host); + catch { } + return null; } - public static string GetKnownHostImage(string host) - => $"ms-appx:///Assets/Thumbnails/{host}.png"; + private static async Task GetFaviconBase64(string email) + { + try + { + var host = GetHost(email); + + if (string.IsNullOrEmpty(host)) + return null; + + // Do not fetch favicon for specific default domains of major platforms + if (_excludedFaviconDomains.Contains(host, StringComparer.OrdinalIgnoreCase)) + return null; + + var primaryDomain = string.Join('.', host.Split('.')[^2..]); + + var googleFaviconUrl = $"https://www.google.com/s2/favicons?sz=128&domain_url={primaryDomain}"; + var response = await _httpClient.GetAsync(googleFaviconUrl); + if (response.IsSuccessStatusCode) + { + var bytes = response.Content.ReadAsByteArrayAsync().Result; + return Convert.ToBase64String(bytes); + } + } + catch { } + return null; + } + + private static string GetHost(string email) + { + if (!string.IsNullOrEmpty(email) && email.Contains('@')) + { + var split = email.Split('@'); + if (split.Length >= 2 && !string.IsNullOrEmpty(split[1])) + { + try { return new MailAddress(email).Host; } catch { } + } + } + return string.Empty; + } } diff --git a/Wino.Core.UWP/Wino.Core.UWP.csproj b/Wino.Core.UWP/Wino.Core.UWP.csproj index 1bb7391b..e6b2d5af 100644 --- a/Wino.Core.UWP/Wino.Core.UWP.csproj +++ b/Wino.Core.UWP/Wino.Core.UWP.csproj @@ -85,6 +85,7 @@ + diff --git a/Wino.Core.UWP/WinoApplication.cs b/Wino.Core.UWP/WinoApplication.cs index 4eae38cd..6cdaa9dc 100644 --- a/Wino.Core.UWP/WinoApplication.cs +++ b/Wino.Core.UWP/WinoApplication.cs @@ -40,6 +40,7 @@ public abstract class WinoApplication : Application, IRecipient protected IWinoServerConnectionManager AppServiceConnectionManager { get; } public IThemeService ThemeService { get; } public IUnderlyingThemeService UnderlyingThemeService { get; } + public IThumbnailService ThumbnailService { get; } protected IDatabaseService DatabaseService { get; } protected ITranslationService TranslationService { get; } @@ -64,6 +65,7 @@ public abstract class WinoApplication : Application, IRecipient DatabaseService = Services.GetService(); TranslationService = Services.GetService(); UnderlyingThemeService = Services.GetService(); + ThumbnailService = Services.GetService(); // Make sure the paths are setup on app start. AppConfiguration.ApplicationDataFolderPath = ApplicationData.Current.LocalFolder.Path; diff --git a/Wino.Mail.ViewModels/AppPreferencesPageViewModel.cs b/Wino.Mail.ViewModels/AppPreferencesPageViewModel.cs index f80f0ee3..41dcd0e4 100644 --- a/Wino.Mail.ViewModels/AppPreferencesPageViewModel.cs +++ b/Wino.Mail.ViewModels/AppPreferencesPageViewModel.cs @@ -18,7 +18,6 @@ public partial class AppPreferencesPageViewModel : MailBaseViewModel [ObservableProperty] private List _appTerminationBehavior; - [ObservableProperty] public partial List SearchModes { get; set; } diff --git a/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs b/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs index 9f2a132c..3425bcfa 100644 --- a/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs +++ b/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs @@ -129,11 +129,11 @@ public class WinoMailCollection private async Task HandleExistingThreadAsync(ObservableGroup group, ThreadMailItemViewModel threadViewModel, MailCopy addedItem) { var existingGroupKey = GetGroupingKey(threadViewModel); - + await ExecuteUIThread(() => { threadViewModel.AddMailItemViewModel(addedItem); }); var newGroupKey = GetGroupingKey(threadViewModel); - + if (!existingGroupKey.Equals(newGroupKey)) { await MoveThreadToNewGroupAsync(group, threadViewModel, newGroupKey); @@ -294,6 +294,25 @@ public class WinoMailCollection return null; } + public void UpdateThumbnails(string address) + { + if (CoreDispatcher == null) return; + + CoreDispatcher.ExecuteOnUIThread(() => + { + foreach (var group in _mailItemSource) + { + foreach (var item in group) + { + if (item is MailItemViewModel mailItemViewModel && mailItemViewModel.MailCopy.FromAddress.Equals(address, StringComparison.OrdinalIgnoreCase)) + { + mailItemViewModel.ThumbnailUpdatedEvent = !mailItemViewModel.ThumbnailUpdatedEvent; + } + } + } + }); + } + /// /// Fins the item container that updated mail copy belongs to and updates it. /// diff --git a/Wino.Mail.ViewModels/Data/AccountContactViewModel.cs b/Wino.Mail.ViewModels/Data/AccountContactViewModel.cs index 262d68a8..cc9ff979 100644 --- a/Wino.Mail.ViewModels/Data/AccountContactViewModel.cs +++ b/Wino.Mail.ViewModels/Data/AccountContactViewModel.cs @@ -1,10 +1,17 @@ -using Wino.Core.Domain; +using System; +using CommunityToolkit.Mvvm.ComponentModel; +using Wino.Core.Domain; using Wino.Core.Domain.Entities.Shared; namespace Wino.Mail.ViewModels.Data; -public class AccountContactViewModel : AccountContact +public partial class AccountContactViewModel : ObservableObject { + public string Address { get; set; } + public string Name { get; set; } + public string Base64ContactPicture { get; set; } + public bool IsRootContact { get; set; } + public AccountContactViewModel(AccountContact contact) { Address = contact.Address; @@ -39,4 +46,7 @@ public class AccountContactViewModel : AccountContact /// Display name of the contact in a format: Name
. /// public string DisplayName => Address == Name || string.IsNullOrWhiteSpace(Name) ? Address.ToLowerInvariant() : $"{Name} <{Address.ToLowerInvariant()}>"; + + [ObservableProperty] + public partial bool ThumbnailUpdatedEvent { get; set; } } diff --git a/Wino.Mail.ViewModels/Data/MailItemViewModel.cs b/Wino.Mail.ViewModels/Data/MailItemViewModel.cs index 5d28029c..66344587 100644 --- a/Wino.Mail.ViewModels/Data/MailItemViewModel.cs +++ b/Wino.Mail.ViewModels/Data/MailItemViewModel.cs @@ -13,7 +13,7 @@ namespace Wino.Mail.ViewModels.Data; public partial class MailItemViewModel(MailCopy mailCopy) : ObservableObject, IMailItem { [ObservableProperty] - private MailCopy mailCopy = mailCopy; + public partial MailCopy MailCopy { get; set; } = mailCopy; public Guid UniqueId => ((IMailItem)MailCopy).UniqueId; public string ThreadId => ((IMailItem)MailCopy).ThreadId; @@ -23,10 +23,13 @@ public partial class MailItemViewModel(MailCopy mailCopy) : ObservableObject, IM public string InReplyTo => ((IMailItem)MailCopy).InReplyTo; [ObservableProperty] - private bool isCustomFocused; + public partial bool ThumbnailUpdatedEvent { get; set; } = false; [ObservableProperty] - private bool isSelected; + public partial bool IsCustomFocused { get; set; } + + [ObservableProperty] + public partial bool IsSelected { get; set; } public bool IsFlagged { diff --git a/Wino.Mail.ViewModels/MailListPageViewModel.cs b/Wino.Mail.ViewModels/MailListPageViewModel.cs index bc825ea8..b4fd27ad 100644 --- a/Wino.Mail.ViewModels/MailListPageViewModel.cs +++ b/Wino.Mail.ViewModels/MailListPageViewModel.cs @@ -41,7 +41,8 @@ public partial class MailListPageViewModel : MailBaseViewModel, IRecipient, IRecipient, IRecipient, - IRecipient + IRecipient, + IRecipient { private bool isChangingFolder = false; @@ -1140,4 +1141,6 @@ public partial class MailListPageViewModel : MailBaseViewModel, }); } } + + public void Receive(ThumbnailAdded message) => MailCollection.UpdateThumbnails(message.Email); } diff --git a/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs b/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs index 5a2141a7..3a2818c6 100644 --- a/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs +++ b/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs @@ -25,12 +25,14 @@ using Wino.Mail.ViewModels.Data; using Wino.Mail.ViewModels.Messages; using Wino.Messaging.Client.Mails; using Wino.Messaging.Server; +using Wino.Messaging.UI; using IMailService = Wino.Core.Domain.Interfaces.IMailService; namespace Wino.Mail.ViewModels; public partial class MailRenderingPageViewModel : MailBaseViewModel, IRecipient, + IRecipient, ITransferProgress // For listening IMAP message download progress. { private readonly IMailDialogService _dialogService; @@ -788,4 +790,27 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel, Log.Error(ex, "Failed to render mail."); } } + + public void Receive(ThumbnailAdded message) + { + UpdateThumbnails(ToItems, message.Email); + UpdateThumbnails(CcItems, message.Email); + UpdateThumbnails(BccItems, message.Email); + } + + private void UpdateThumbnails(ObservableCollection items, string email) + { + if (Dispatcher == null || items.Count == 0) return; + + Dispatcher.ExecuteOnUIThread(() => + { + foreach (var item in items) + { + if (item.Address.Equals(email, StringComparison.OrdinalIgnoreCase)) + { + item.ThumbnailUpdatedEvent = !item.ThumbnailUpdatedEvent; + } + } + }); + } } diff --git a/Wino.Mail.ViewModels/MessageListPageViewModel.cs b/Wino.Mail.ViewModels/MessageListPageViewModel.cs index daaf04ae..b1bf22bb 100644 --- a/Wino.Mail.ViewModels/MessageListPageViewModel.cs +++ b/Wino.Mail.ViewModels/MessageListPageViewModel.cs @@ -1,17 +1,19 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.Input; using Wino.Core.Domain; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; namespace Wino.Mail.ViewModels; -public class MessageListPageViewModel : MailBaseViewModel +public partial class MessageListPageViewModel : MailBaseViewModel { public IPreferencesService PreferencesService { get; } + private readonly IThumbnailService _thumbnailService; private int selectedMarkAsOptionIndex; - public int SelectedMarkAsOptionIndex { get => selectedMarkAsOptionIndex; @@ -46,9 +48,7 @@ public class MessageListPageViewModel : MailBaseViewModel ]; #region Properties - private int leftHoverActionIndex; - public int LeftHoverActionIndex { get => leftHoverActionIndex; @@ -61,9 +61,7 @@ public class MessageListPageViewModel : MailBaseViewModel } } - private int centerHoverActionIndex; - public int CenterHoverActionIndex { get => centerHoverActionIndex; @@ -77,7 +75,6 @@ public class MessageListPageViewModel : MailBaseViewModel } private int rightHoverActionIndex; - public int RightHoverActionIndex { get => rightHoverActionIndex; @@ -89,18 +86,21 @@ public class MessageListPageViewModel : MailBaseViewModel } } } - #endregion - public MessageListPageViewModel(IMailDialogService dialogService, - IPreferencesService preferencesService) + public MessageListPageViewModel(IPreferencesService preferencesService, IThumbnailService thumbnailService) { PreferencesService = preferencesService; - + _thumbnailService = thumbnailService; leftHoverActionIndex = availableHoverActions.IndexOf(PreferencesService.LeftHoverAction); centerHoverActionIndex = availableHoverActions.IndexOf(PreferencesService.CenterHoverAction); rightHoverActionIndex = availableHoverActions.IndexOf(PreferencesService.RightHoverAction); - SelectedMarkAsOptionIndex = Array.IndexOf(Enum.GetValues(), PreferencesService.MarkAsPreference); } + + [RelayCommand] + private async Task ClearAvatarsCacheAsync() + { + await _thumbnailService.ClearCache(); + } } diff --git a/Wino.Mail/Assets/Thumbnails/airbnb.com.png b/Wino.Mail/Assets/Thumbnails/airbnb.com.png deleted file mode 100644 index 9713a7afa0a145ddba9f718a45d2fe6c8e919dcd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2385 zcmV-X39j~uP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!TD)HG@_9YCdQ~(qM!uX zMOGI;Rs}?G!-Y*`Ey&^ujQ{_fd+lwRY3IFpuL+%Ba?^9)Oq+MlJ@0JyH6n3{4jNh) zKZaoR#AuF@hmnPGCh;#u5ym$d3or^JQwsOuX9-Fu;MAZYRU#4@1MzW;vtt)44}W4z zh(@F1GNw+Za$@|i04zQSi=PGY2E9^gS&q>c)^M1Um;$i)EG&Kz#LB*S%L-URH(Ejl zXU6Q)W$~m_BoY}*Az~cfFBp~WnJZiY?19da0kf68Z{KTrMF1iHu`xKxAfe2ksr3|JVe5`fXqzYeaiwQoG(p8=CMt8 z|Kv%LAAizopDokxP$};3gh==b=+a4~-uWu;-6t}2n#%EbXR652qbgt4M5H||CuG7^ zK-H>vp8J^zm_9?~#0fKr!yhQvX3sMdK!=$T?k$G<*Sp^+l2=RR9Xmxrw7B3aq ze?aB>7a(iDR^^ZgPXTA2g`%^UnSg22)wz`iEo26(a>D>vP?hk~l<*YLD_`WCb5#ER z2aye5shkWeR*LN2t@62bM7niRIRwH{K#dy6vXS&y-pB5Hfze_5Ofvy}5hCeNLLeLk z^v3RIS5tZ8Cba;sto=gs1X2rrfzRD{ajk%kHDF&5tO6MJ?Cx*C z?)}V8TZ+(!xKZrBT1AQk^9WS0j+=Kp z|I9f?FmRDiJ|WVorMX~WymuGMyr0kc@S)~{!yKnZU_@Y*834g3pl5g88S>TFr$75F zZ7*z~@<0D**JtFTB7=vE47pci%vhAdJg_lzMdrsHcUkwP$geg#JXhq}4l1We>I&fQ z_J9H`vr6qgiyD?#v_Lep4jF13Uf}6LP321nc5e$qNbLX20U>hw(Qd0m;o1u(s{Si0Mqxe!} zIf=_L)9on>HbpG)%HU-yG!&ACP;SfAjy3!%fL^!&9^2w}a$KxQ6P^i_M~+b_IeN?p z4vhZ`oDfT2kOKY{z)nqVzBmHd?P1;@59_%JyT9dev19go5dY(`^iz@Jf2hpdgZ{!N z{uDr`##n5jcXs2si|Z6yw=zNk*!-x=!)kEgpvd9hRW8k+cubZoH51U=xfhT4Qvkhh zy?QFM)4O`D%JHCea53Zp{LZD9s2q=uPHf+V8lWr|EG|xm**!l0Je57-PXX-ESmwbi zGnOZUma!a;%i>y&3mu+Y?yf3Bsf518{#{yRfxkm;?+R!EughGX&>JuRT;;M@@`+}E zgr`J}8>#HlwR5qP+__70it=E+VIRHgI~C_69`LS!8*rnRk8lIYRAq;us3jk(%Q89xP#1NGWG+LWwn!B@yOjH-W0$iF6RD(GVcFEOO6aofDSPwiBEzEUN5d z$STnz-W0&x-(sDbS6KD&jS!NXTl~HN%jns0YKiiowfw~wzJQOeu{Q-AJ8lHvrmS8# zj=%9+5T5Mwd3jTRU6}^;MTR7T;lYeI3Ps7Peoc#&6M(?@_by-eh}r`1Z5fyGt+&1M2tB$1Sr z!h2VkS(<&tEeoE!<27n}iJ}5qVk3s>i%+(`=l93Ipw5uL$9kbX>I*&!%m=z$uX%=j zjV2z7Qg#mn-$x(!>qBRY*{fg>u`1PGfJ4^ zPJFdEDY7&{ku>r_jSLoA3C{{PZHBU-G@d!I^kpNac{B4Rz|Y2xJN<7af&YUM_VP#J zhU4Y+@!+%0rsph-M7v7gFk-m3l$HvA=@mUI@Ih)PLsek1`KZEJ;U#ijTK)WHy!w-^v|fpO7cKhy3l1 zG`UoybxT+oJH&YG80Yuw)sv&Or~%ymp-+N48$3f{7&I{`Es0VAKJBwo@H-XYoJ6D@ zTd06i=h$h*LMq^G3{y|06|CncWkjYFGC02+yOdtMg0=6W1)#!>K8qhKDW?hSxySx* zFs6Wvsgr3AeK1yWUdiF&3$Xita}rYk!G$7N0{?sTd5qYHl?-n3ix{1;R&!K^b(^0h zKz7K295rn0e+pT4IVIW|1rHY)<{9ZGVd1E|T}1u^O&p}FhqI9N00000NkvXXu0mjf Dhm3>z diff --git a/Wino.Mail/Assets/Thumbnails/apple.com.png b/Wino.Mail/Assets/Thumbnails/apple.com.png deleted file mode 100644 index 18621ea727e2a235a2e3ad877aa62b2ef2e2c73c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2113 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 diff --git a/Wino.Mail/Assets/Thumbnails/steampowered.com.png b/Wino.Mail/Assets/Thumbnails/steampowered.com.png deleted file mode 100644 index a5d6087fb1db94ea15a318b2254493ead0908347..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3396 zcmV-K4ZHG*P)@MtO zc6Mgo{()}cM0OWs3GL_aoIPjH?7Wxfd4Knr1@6jSxhwylh4S?R$xp2008F5Oi2&jN zL;;8Z5JG|C0f2xA01NCaj_=KS-SHnsz@r(N8c@^%0BHb{oKDH#Damk3 zGGtkXtPl`I0qL*_il)KP6c~nqO2vRvF<@Dh6~F-iUl4-a{H)jQ{hEN}jFk}-h2;RA z7938!;1D5-P8ZOgY>wkt@M;#knll0Th!FCp{H(RL{fdC(jFmcy!fzFYJZH5F8oMBZ z5YoFRMN!c39Q-xBK-1I*gpfbvXRS4T?*t@gtXv3?|FhVHaGTxHCyvWcqlVUxZv^ms ze%9L0zZU{#Wn^k;iu$wQa6aBS!*qQM!>9$%;6NU ztJPv3BRItGHH`6U4uM+DIRc~A}*R+J-Sc|D`j4w}~V^v6+JPwJI;t&%Zi2=bu0D$He zGb*aCqNwBy_7|Uq=#<=QLa;9294JgI+_KKnTLfeq&6dY~Y4bCu#N&mh7a%ND=k~rP zg9(4yyaR=Yzjf~ug0wk>Th=YUV*+NTzqZg~6+ZKox2GuzE1pe7>YORv8n^AM!+7h% zZC!?6Ud=)4$1f`UblvAYCSb;r*L0F3SDM?bVZJ)~SALm>)Y<Pq*jenKOBu*NO#q$#} zdPF1u;QYm_$lh6mqLMN=1q;@{`V8hI{qUy7m@x0>tGjfJej2r%=hzYZbJo{(O9KeO za$kW5fTYReT;Q8-7_nwU7Fa$2%YQZ>89zztbZ>lYG~#2Uk+pq4HhjDjYc^ydVeDvx zh3XL&szcJ`aoAII;<$gjvC9Fx3W<{* z_zw7vlJS!y{G;L$ib~3mom+?(o>>S0m_9MCO9B8u5S>py^7zZE4&=OP_lf{Q$dB!U zsPiQOG0~B(@5Lu8Kv6Un&rkF?Zt?s?d~@s!icePHg=gBY-7=M~#4d<>j$;=9$nlB* zrzEBMI@`OI+sWwl)pAMqvl*@+2IshAEa zEDb;gydeG7!L*OLdCiNSlhY|bGJVks^*5ir<#0;?A($YFPJds4A3A6NUU@F9)3wNm zFjQZuM|oup9(o|g?fCM_8rPUx(robD7!pONR?V^F0UUNqfF#Kie5L#<6@zsvo`k<& z=ioABd@QQ3)FbQ5LOd8Z3KT_kdHP$vECc{d9Utquey;MeXUzd1$?`w zfaGZpfdYls-`k4Ar!J!C=sGB}v%O$qSg6!~&;^GebJ-#!PkYcc?yV1UAQKvt7j-Qm zi13&S3l34V+5|Rl-!bGC96@b^;f|XW@2~kKCX5^9)&0#aW_-Tu5Dt}|#kIQTcE>q* z0H%zO#iEpHokHH$9ferGX&Y3WcL^&^)0UIF-VJa|fF#QbL(|kvmBI0{N_@Ge7>7@k zL+(L8;K`rPz^|U1+v_H^bw?rAf3OuQwYD3F0qNB4_gJ?kKy_velj37ADLw{G1`~D` zmS9K0Q8eA~?s-J)efQwGC3AWecw=)5-h3|$`-;oJ^4e~VyGW@ID+{QUqBFbvZppluc#G(6`jmdYz@aNy7h z6dWuCq19fieP*za>pl3 z2toGte7wCW2cjstvObz&+j|78+O0Enn+-Z_wP*K#JjAUD7>&jTZwVM29)e{VDZQrN z=2iE1NM8UAuXa1`^w~Uf)IjzN6unzQ7MKG2}9bvB+N^mil6}RJg=H#u`p#iXqv&=joJ9i zr#tcd(uJMwF|?TR^4j-t{2v#=@_{|JZEr;sETtYbAY|z1;GtutU8fmF)4_HiiFT-H z1<8pMv1r~5#Ep*jYQt8$fT^i3Ax!Uwy<6UNT|aiJ94lUbAFVbS46BB(MUf?2UFESM zO_$1yo;1K{G1ip?h7NTl?lhwUrwITd1be>u7I}q7FmiYVwtuu9G);R|zc(ALoz(v4 zFTDkp*Y*i`0I(VxN@Phid31v2#!JLs7oOXZfR1Uciqk;FX>hrr73Nke^tyJ3RoBpr z!2<_;XW_hp!vG-|7#`Be+p`+qt9&N{%uTt5Yv+kqEip?=U9K!S*D)$Ku*?xp0AI)2zVxa0Zx?HVAVUHAoGnsw@=RTV0kTKM-IbNsWUr` z>q}{={pWUBayH(!KIm#tKxpK+sDwEsG|hBrq(l~x{qC!X8FA08?koPftq^;PPN4CI z5#gbF%uYimuX>G;D%UJwBZ-8RBJ^`;NvZLnI-ifVEm&>|E0Iv;Z2iz04dBCvP z40jH0P%w`z8&@G%7wGvUOsz=&)f&{d2z_P(%~#5^4A(1`+DwhkZV>`{!Mna67qh@iKn>`*%^_Vh7Er`UJcr3WloEyva6m(}gZeWOoyA zngAvsG;;Z{xWson#8}7<3ua9pkCd5{asQ|&1P1t{watpN7pk#)UorBFO4~ENJ^~K_ zSI!i@Xf@Vv5G*Znj|cz&RuiBfI(l-pZeY}-p5ndibU<1kYRb6 z<^3NXIdQ>f?)*W25J?ma7mx2;BsuI~I~`V~R|vRC0>^80bE6aHZf006s6T?2ouaww zMBY--)|TgVSiO3ur&~rzt2m8D8~mfFgdc6{Z<-*9f}#4v?&lmf^KMDBd;MB{x4jb0 zuo_OIoqq2_b2j!DpCDLG)io!-TIO`v4$4kPxBgSmBezU3DvqXAal_&gSL+5w&Hg^Y zn;NS3UOij1N>P+EiX?XXRkj|-C5obH0FnBLkqd{!On#}aO+pd{!}W^dcZ_uxKLe<% zM3H**65|~QW{RTq46BYAG-CX-A(5lg+|>@fmD?Mxo!?gf&(aTNNjy&!*?33$@11Q? z41j^0Mmr|#-f@dVhQ!QwS4#J+q;<9$u2=lMsiy2xQD{34pdN(W@dM1>PR&>VVKl== z=m$k7=?6zAX@ess(2UBjXKgImZfU7KZmhew&)86PK#`>D0Ga^e?5z#^DmSG7=m6+x zMiuI>A9TOJF8qE?U}&`3FK8&s`-iF8yP)cud9*`v3aw7T+T^gAui0B~R9Q`p}*Z| diff --git a/Wino.Mail/Assets/Thumbnails/uber.com.png b/Wino.Mail/Assets/Thumbnails/uber.com.png deleted file mode 100644 index 5dbb4ea82e52d06d1f5641c3ee9ccd4107c3f986..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1392 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 diff --git a/Wino.Mail/Controls/ImagePreviewControl.cs b/Wino.Mail/Controls/ImagePreviewControl.cs index f3a0df2c..6037ecb9 100644 --- a/Wino.Mail/Controls/ImagePreviewControl.cs +++ b/Wino.Mail/Controls/ImagePreviewControl.cs @@ -5,13 +5,14 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Fernandezja.ColorHashSharp; +using Microsoft.Extensions.DependencyInjection; using Windows.UI; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Media.Imaging; using Windows.UI.Xaml.Shapes; -using Wino.Core.UWP.Services; +using Wino.Core.Domain.Interfaces; namespace Wino.Controls; @@ -21,12 +22,21 @@ public partial class ImagePreviewControl : Control private const string PART_InitialsTextBlock = "InitialsTextBlock"; private const string PART_KnownHostImage = "KnownHostImage"; private const string PART_Ellipse = "Ellipse"; + private const string PART_FaviconSquircle = "FaviconSquircle"; + private const string PART_FaviconImage = "FaviconImage"; #region Dependency Properties - 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 SenderContactPictureProperty = DependencyProperty.Register(nameof(SenderContactPicture), typeof(string), typeof(ImagePreviewControl), new PropertyMetadata(string.Empty, new PropertyChangedCallback(OnAddressInformationChanged))); + public static readonly DependencyProperty FromNameProperty = DependencyProperty.Register(nameof(FromName), typeof(string), typeof(ImagePreviewControl), new PropertyMetadata(string.Empty, OnInformationChanged)); + public static readonly DependencyProperty FromAddressProperty = DependencyProperty.Register(nameof(FromAddress), typeof(string), typeof(ImagePreviewControl), new PropertyMetadata(string.Empty, OnInformationChanged)); + public static readonly DependencyProperty SenderContactPictureProperty = DependencyProperty.Register(nameof(SenderContactPicture), typeof(string), typeof(ImagePreviewControl), new PropertyMetadata(string.Empty, new PropertyChangedCallback(OnInformationChanged))); + public static readonly DependencyProperty ThumbnailUpdatedEventProperty = DependencyProperty.Register(nameof(ThumbnailUpdatedEvent), typeof(bool), typeof(ImagePreviewControl), new PropertyMetadata(false, new PropertyChangedCallback(OnInformationChanged))); + + public bool ThumbnailUpdatedEvent + { + get { return (bool)GetValue(ThumbnailUpdatedEventProperty); } + set { SetValue(ThumbnailUpdatedEventProperty, value); } + } /// /// Gets or sets base64 string of the sender contact picture. @@ -55,6 +65,8 @@ public partial class ImagePreviewControl : Control private Grid InitialsGrid; private TextBlock InitialsTextblock; private Image KnownHostImage; + private Border FaviconSquircle; + private Image FaviconImage; private CancellationTokenSource contactPictureLoadingCancellationTokenSource; public ImagePreviewControl() @@ -70,11 +82,13 @@ public partial class ImagePreviewControl : Control InitialsTextblock = GetTemplateChild(PART_InitialsTextBlock) as TextBlock; KnownHostImage = GetTemplateChild(PART_KnownHostImage) as Image; Ellipse = GetTemplateChild(PART_Ellipse) as Ellipse; + FaviconSquircle = GetTemplateChild(PART_FaviconSquircle) as Border; + FaviconImage = GetTemplateChild(PART_FaviconImage) as Image; UpdateInformation(); } - private static void OnAddressInformationChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args) + private static void OnInformationChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args) { if (obj is ImagePreviewControl control) control.UpdateInformation(); @@ -82,7 +96,7 @@ public partial class ImagePreviewControl : Control private async void UpdateInformation() { - if (KnownHostImage == null || InitialsGrid == null || InitialsTextblock == null || (string.IsNullOrEmpty(FromName) && string.IsNullOrEmpty(FromAddress))) + if ((KnownHostImage == null && FaviconSquircle == null) || InitialsGrid == null || InitialsTextblock == null || (string.IsNullOrEmpty(FromName) && string.IsNullOrEmpty(FromAddress))) return; // Cancel active image loading if exists. @@ -91,81 +105,100 @@ public partial class ImagePreviewControl : Control contactPictureLoadingCancellationTokenSource.Cancel(); } - var host = ThumbnailService.GetHost(FromAddress); + string contactPicture = SenderContactPicture; - bool isKnownHost = false; + var isAvatarThumbnail = false; - if (!string.IsNullOrEmpty(host)) + if (string.IsNullOrEmpty(contactPicture) && !string.IsNullOrEmpty(FromAddress)) { - var tuple = ThumbnailService.CheckIsKnown(host); - - isKnownHost = tuple.Item1; - host = tuple.Item2; + contactPicture = await App.Current.ThumbnailService.GetThumbnailAsync(FromAddress); + isAvatarThumbnail = true; } - if (isKnownHost) + if (!string.IsNullOrEmpty(contactPicture)) { - // Unrealize others. - - KnownHostImage.Visibility = Visibility.Visible; - InitialsGrid.Visibility = Visibility.Collapsed; - - // Apply company logo. - KnownHostImage.Source = new BitmapImage(new Uri(ThumbnailService.GetKnownHostImage(host))); - } - else - { - KnownHostImage.Visibility = Visibility.Collapsed; - InitialsGrid.Visibility = Visibility.Visible; - - if (!string.IsNullOrEmpty(SenderContactPicture)) + if (isAvatarThumbnail && FaviconSquircle != null && FaviconImage != null) { - contactPictureLoadingCancellationTokenSource = new CancellationTokenSource(); + // Show favicon in squircle + FaviconSquircle.Visibility = Visibility.Visible; + InitialsGrid.Visibility = Visibility.Collapsed; + KnownHostImage.Visibility = Visibility.Collapsed; - try - { - var brush = await GetContactImageBrushAsync(); + var bitmapImage = await GetBitmapImageAsync(contactPicture); - if (!contactPictureLoadingCancellationTokenSource?.Token.IsCancellationRequested ?? false) - { - Ellipse.Fill = brush; - InitialsTextblock.Text = string.Empty; - } - } - catch (Exception) + if (bitmapImage != null) { - // Log exception. - Debugger.Break(); + FaviconImage.Source = bitmapImage; } } else { - var colorHash = new ColorHash(); - var rgb = colorHash.Rgb(FromAddress); + // Show normal avatar (tondo) + FaviconSquircle.Visibility = Visibility.Collapsed; + KnownHostImage.Visibility = Visibility.Collapsed; + InitialsGrid.Visibility = Visibility.Visible; + contactPictureLoadingCancellationTokenSource = new CancellationTokenSource(); + try + { + var brush = await GetContactImageBrushAsync(contactPicture); - Ellipse.Fill = new SolidColorBrush(Color.FromArgb(rgb.A, rgb.R, rgb.G, rgb.B)); - InitialsTextblock.Text = ExtractInitialsFromName(FromName); + if (brush != null) + { + if (!contactPictureLoadingCancellationTokenSource?.Token.IsCancellationRequested ?? false) + { + Ellipse.Fill = brush; + InitialsTextblock.Text = string.Empty; + } + } + } + catch (Exception) + { + Debugger.Break(); + } } } + else + { + FaviconSquircle.Visibility = Visibility.Collapsed; + KnownHostImage.Visibility = Visibility.Collapsed; + InitialsGrid.Visibility = Visibility.Visible; + + var colorHash = new ColorHash(); + var rgb = colorHash.Rgb(FromAddress); + + Ellipse.Fill = new SolidColorBrush(Color.FromArgb(rgb.A, rgb.R, rgb.G, rgb.B)); + InitialsTextblock.Text = ExtractInitialsFromName(FromName); + } } - private async Task GetContactImageBrushAsync() + private static async Task GetContactImageBrushAsync(string base64) { // Load the image from base64 string. - var bitmapImage = new BitmapImage(); + + var bitmapImage = await GetBitmapImageAsync(base64); - var imageArray = Convert.FromBase64String(SenderContactPicture); - var imageStream = new MemoryStream(imageArray); - var randomAccessImageStream = imageStream.AsRandomAccessStream(); - - randomAccessImageStream.Seek(0); - - - await bitmapImage.SetSourceAsync(randomAccessImageStream); + if (bitmapImage == null) return null; return new ImageBrush() { ImageSource = bitmapImage }; } + private static async Task GetBitmapImageAsync(string base64) + { + try + { + var bitmapImage = new BitmapImage(); + var imageArray = Convert.FromBase64String(base64); + var imageStream = new MemoryStream(imageArray); + var randomAccessImageStream = imageStream.AsRandomAccessStream(); + randomAccessImageStream.Seek(0); + await bitmapImage.SetSourceAsync(randomAccessImageStream); + return bitmapImage; + } + catch (Exception) { } + + return null; + } + 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 ff0c6ceb..92d3332a 100644 --- a/Wino.Mail/Controls/MailItemDisplayInformationControl.xaml +++ b/Wino.Mail/Controls/MailItemDisplayInformationControl.xaml @@ -76,6 +76,7 @@ FromAddress="{x:Bind MailItem.FromAddress, Mode=OneWay}" FromName="{x:Bind MailItem.FromName, Mode=OneWay}" SenderContactPicture="{x:Bind MailItem.SenderContact.Base64ContactPicture}" + ThumbnailUpdatedEvent="{x:Bind IsThumbnailUpdated, Mode=OneWay}" Visibility="{x:Bind IsAvatarVisible, Mode=OneWay}" /> + + + + + + Stretch="UniformToFill" + Visibility="Collapsed" /> diff --git a/Wino.Mail/Views/MailListPage.xaml b/Wino.Mail/Views/MailListPage.xaml index f9053698..8befa449 100644 --- a/Wino.Mail/Views/MailListPage.xaml +++ b/Wino.Mail/Views/MailListPage.xaml @@ -113,6 +113,7 @@ IsAvatarVisible="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowSenderPicturesEnabled, Mode=OneWay}" IsCustomFocused="{x:Bind IsCustomFocused, Mode=OneWay}" IsHoverActionsEnabled="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsHoverActionsEnabled, Mode=OneWay}" + IsThumbnailUpdated="{x:Bind ThumbnailUpdatedEvent, Mode=OneWay}" LeftHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.LeftHoverAction, Mode=OneWay}" MailItem="{x:Bind MailCopy, Mode=OneWay}" Prefer24HourTimeFormat="{Binding ElementName=root, Path=ViewModel.PreferencesService.Prefer24HourTimeFormat, Mode=OneWay}" @@ -136,6 +137,7 @@ IsAvatarVisible="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowSenderPicturesEnabled, Mode=OneWay}" IsCustomFocused="{x:Bind IsCustomFocused, Mode=OneWay}" IsHoverActionsEnabled="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsHoverActionsEnabled, Mode=OneWay}" + IsThumbnailUpdated="{x:Bind ThumbnailUpdatedEvent, Mode=OneWay}" LeftHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.LeftHoverAction, Mode=OneWay}" MailItem="{x:Bind MailCopy, Mode=OneWay}" Prefer24HourTimeFormat="{Binding ElementName=root, Path=ViewModel.PreferencesService.Prefer24HourTimeFormat, Mode=OneWay}" diff --git a/Wino.Mail/Views/MailRenderingPage.xaml b/Wino.Mail/Views/MailRenderingPage.xaml index a5fd3e83..c6a8a93a 100644 --- a/Wino.Mail/Views/MailRenderingPage.xaml +++ b/Wino.Mail/Views/MailRenderingPage.xaml @@ -46,7 +46,8 @@ Height="36" FromAddress="{x:Bind Address}" FromName="{x:Bind Name}" - SenderContactPicture="{x:Bind Base64ContactPicture}" /> + SenderContactPicture="{x:Bind Base64ContactPicture}" + ThumbnailUpdatedEvent="{x:Bind ThumbnailUpdatedEvent, Mode=OneWay}" /> diff --git a/Wino.Mail/Views/Settings/MessageListPage.xaml b/Wino.Mail/Views/Settings/MessageListPage.xaml index 91a492d6..0e67e487 100644 --- a/Wino.Mail/Views/Settings/MessageListPage.xaml +++ b/Wino.Mail/Views/Settings/MessageListPage.xaml @@ -68,14 +68,30 @@ - - - + + - - + - + + + + + + + + + + + + + + +