feat: Enhanced sender avatars with gravatar and favicons integration (#685)

* 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 <aleh.khantsevich@gmail.com>
Co-authored-by: Burak Kaan Köse <bkaankose@outlook.com>
This commit is contained in:
Maicol Battistini
2025-06-21 01:40:25 +02:00
committed by GitHub
parent a8cb332232
commit 256fd1cce2
38 changed files with 489 additions and 172 deletions

View File

@@ -19,6 +19,7 @@
<PackageVersion Include="CommunityToolkit.Uwp.Extensions" Version="8.2.250402" /> <PackageVersion Include="CommunityToolkit.Uwp.Extensions" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.Uwp.Controls.Primitives" Version="8.2.250129-preview2" /> <PackageVersion Include="CommunityToolkit.Uwp.Controls.Primitives" Version="8.2.250129-preview2" />
<PackageVersion Include="EmailValidation" Version="1.3.0" /> <PackageVersion Include="EmailValidation" Version="1.3.0" />
<PackageVersion Include="gravatar-dotnet" Version="0.1.3" />
<PackageVersion Include="HtmlAgilityPack" Version="1.12.0" /> <PackageVersion Include="HtmlAgilityPack" Version="1.12.0" />
<PackageVersion Include="Ical.Net" Version="4.3.1" /> <PackageVersion Include="Ical.Net" Version="4.3.1" />
<PackageVersion Include="IsExternalInit" Version="1.0.3" /> <PackageVersion Include="IsExternalInit" Version="1.0.3" />

View File

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

View File

@@ -1,11 +1,12 @@
using System; using System;
using System.ComponentModel;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Calendar; using Wino.Core.Domain.Models.Calendar;
using Wino.Core.Domain.Models.Reader; using Wino.Core.Domain.Models.Reader;
namespace Wino.Core.Domain.Interfaces; namespace Wino.Core.Domain.Interfaces;
public interface IPreferencesService public interface IPreferencesService: INotifyPropertyChanged
{ {
/// <summary> /// <summary>
/// When any of the preferences are changed. /// When any of the preferences are changed.
@@ -193,6 +194,16 @@ public interface IPreferencesService
/// </summary> /// </summary>
bool IsShowActionLabelsEnabled { get; set; } bool IsShowActionLabelsEnabled { get; set; }
/// <summary>
/// Setting: Enable/disable Gravatar for sender avatars.
/// </summary>
bool IsGravatarEnabled { get; set; }
/// <summary>
/// Setting: Enable/disable Favicon for sender avatars.
/// </summary>
bool IsFaviconEnabled { get; set; }
#endregion #endregion
#region Calendar #region Calendar

View File

@@ -0,0 +1,20 @@
using System.Threading.Tasks;
namespace Wino.Core.Domain.Interfaces;
public interface IThumbnailService
{
/// <summary>
/// Clears the thumbnail cache.
/// </summary>
Task ClearCache();
/// <summary>
/// Gets thumbnail
/// </summary>
/// <param name="email">Address for thumbnail</param>
/// <param name="awaitLoad">Force to wait for thumbnail loading.
/// Should be used in non-UI threads or where delay is acceptable
/// </param>
ValueTask<string> GetThumbnailAsync(string email, bool awaitLoad = false);
}

View File

@@ -623,6 +623,11 @@
"SettingsShowPreviewText_Title": "Show Preview Text", "SettingsShowPreviewText_Title": "Show Preview Text",
"SettingsShowSenderPictures_Description": "Hide/show the thumbnail sender pictures.", "SettingsShowSenderPictures_Description": "Hide/show the thumbnail sender pictures.",
"SettingsShowSenderPictures_Title": "Show Sender Avatars", "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_Button": "Add signature",
"SettingsSignature_AddCustomSignature_Title": "Add custom signature", "SettingsSignature_AddCustomSignature_Title": "Add custom signature",
"SettingsSignature_DeleteSignature_Title": "Delete signature", "SettingsSignature_DeleteSignature_Title": "Delete signature",

View File

@@ -25,7 +25,7 @@ public static class CoreUWPContainerSetup
services.AddSingleton<IPreferencesService, PreferencesService>(); services.AddSingleton<IPreferencesService, PreferencesService>();
services.AddSingleton<IThemeService, ThemeService>(); services.AddSingleton<IThemeService, ThemeService>();
services.AddSingleton<IStatePersistanceService, StatePersistenceService>(); services.AddSingleton<IStatePersistanceService, StatePersistenceService>();
services.AddSingleton<IThumbnailService, ThumbnailService>();
services.AddSingleton<IDialogServiceBase, DialogServiceBase>(); services.AddSingleton<IDialogServiceBase, DialogServiceBase>();
services.AddTransient<IConfigurationService, ConfigurationService>(); services.AddTransient<IConfigurationService, ConfigurationService>();
services.AddTransient<IFileService, FileService>(); services.AddTransient<IFileService, FileService>();

View File

@@ -45,7 +45,6 @@ public class NativeAppService : INativeAppService
return _mimeMessagesFolder; return _mimeMessagesFolder;
} }
public async Task<string> GetEditorBundlePathAsync() public async Task<string> GetEditorBundlePathAsync()
{ {
if (string.IsNullOrEmpty(_editorBundlePath)) if (string.IsNullOrEmpty(_editorBundlePath))

View File

@@ -11,7 +11,7 @@ using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Services; using System.IO;
namespace Wino.Core.UWP.Services; namespace Wino.Core.UWP.Services;
@@ -23,16 +23,19 @@ public class NotificationBuilder : INotificationBuilder
private readonly IAccountService _accountService; private readonly IAccountService _accountService;
private readonly IFolderService _folderService; private readonly IFolderService _folderService;
private readonly IMailService _mailService; private readonly IMailService _mailService;
private readonly IThumbnailService _thumbnailService;
public NotificationBuilder(IUnderlyingThemeService underlyingThemeService, public NotificationBuilder(IUnderlyingThemeService underlyingThemeService,
IAccountService accountService, IAccountService accountService,
IFolderService folderService, IFolderService folderService,
IMailService mailService) IMailService mailService,
IThumbnailService thumbnailService)
{ {
_underlyingThemeService = underlyingThemeService; _underlyingThemeService = underlyingThemeService;
_accountService = accountService; _accountService = accountService;
_folderService = folderService; _folderService = folderService;
_mailService = mailService; _mailService = mailService;
_thumbnailService = thumbnailService;
} }
public async Task CreateNotificationsAsync(Guid inboxFolderId, IEnumerable<IMailItem> downloadedMailItems) public async Task CreateNotificationsAsync(Guid inboxFolderId, IEnumerable<IMailItem> downloadedMailItems)
@@ -83,24 +86,16 @@ public class NotificationBuilder : INotificationBuilder
var builder = new ToastContentBuilder(); var builder = new ToastContentBuilder();
builder.SetToastScenario(ToastScenario.Default); builder.SetToastScenario(ToastScenario.Default);
var host = ThumbnailService.GetHost(mailItem.FromAddress); var avatarThumbnail = await _thumbnailService.GetThumbnailAsync(mailItem.FromAddress, awaitLoad: true);
if (!string.IsNullOrEmpty(avatarThumbnail))
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
{ {
// TODO: https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts?tabs=toolkit var tempFile = await Windows.Storage.ApplicationData.Current.TemporaryFolder.CreateFileAsync($"{Guid.NewGuid()}.png", Windows.Storage.CreationCollisionOption.ReplaceExisting);
// Follow official guides for icons/theme. await using (var stream = await tempFile.OpenStreamForWriteAsync())
{
bool isOSDarkTheme = _underlyingThemeService.IsUnderlyingThemeDark(); var bytes = Convert.FromBase64String(avatarThumbnail);
string profileLogoName = isOSDarkTheme ? "profile-dark.png" : "profile-light.png"; await stream.WriteAsync(bytes);
}
builder.AddAppLogoOverride(new System.Uri($"ms-appx:///Assets/NotificationIcons/{profileLogoName}"), hintCrop: ToastGenericAppLogoCrop.Circle); builder.AddAppLogoOverride(new Uri($"ms-appdata:///temp/{tempFile.Name}"), hintCrop: ToastGenericAppLogoCrop.Default);
} }
// Override system notification timetamp with received date of the mail. // Override system notification timetamp with received date of the mail.

View File

@@ -13,17 +13,12 @@ using Wino.Services;
namespace Wino.Core.UWP.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<string> PreferenceChanged; public event EventHandler<string> PreferenceChanged;
public PreferencesService(IConfigurationService configurationService)
{
_configurationService = configurationService;
}
protected override void OnPropertyChanged(PropertyChangedEventArgs e) protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{ {
base.OnPropertyChanged(e); base.OnPropertyChanged(e);
@@ -181,6 +176,18 @@ public class PreferencesService : ObservableObject, IPreferencesService
set => SetPropertyAndSave(nameof(IsMailkitProtocolLoggerEnabled), value); 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 public Guid? StartupEntityId
{ {
get => _configurationService.Get<Guid?>(nameof(StartupEntityId), null); get => _configurationService.Get<Guid?>(nameof(StartupEntityId), null);

View File

@@ -1,63 +1,198 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Net.Mail; 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; 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<string, (string graviton, string favicon)> _cache;
private readonly ConcurrentDictionary<string, Task> _requests = [];
private static readonly List<string> _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<string> 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 (!_isInitialized)
{
if (string.IsNullOrEmpty(address))
return string.Empty;
if (address.Contains('@'))
{ {
var splitted = address.Split('@'); var thumbnailsList = await _databaseService.Connection.Table<Thumbnail>().ToListAsync();
if (splitted.Length >= 2 && !string.IsNullOrEmpty(splitted[1])) _cache = new ConcurrentDictionary<string, (string graviton, string favicon)>(
{ thumbnailsList.ToDictionary(x => x.Domain, x => (x.Gravatar, x.Favicon)));
try _isInitialized = true;
{
return new MailAddress(address).Host;
}
catch (Exception)
{
// TODO: Exceptions are ignored for now.
}
}
} }
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<bool, string> CheckIsKnown(string host) public async Task ClearCache()
{ {
// Check known hosts. _cache?.Clear();
// Apply company logo if available. _requests.Clear();
await _databaseService.Connection.DeleteAllAsync<Thumbnail>();
}
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<string> GetGravatarBase64(string email)
{
try try
{ {
var last = host.Split('.'); var gravatarUrl = GravatarHelper.GetAvatarUrl(
email,
if (last.Length > 2) size: 128,
host = $"{last[last.Length - 2]}.{last[last.Length - 1]}"; 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) catch { }
{ return null;
return new Tuple<bool, string>(false, host);
}
return new Tuple<bool, string>(IsKnown(host), host);
} }
public static string GetKnownHostImage(string host) private static async Task<string> GetFaviconBase64(string email)
=> $"ms-appx:///Assets/Thumbnails/{host}.png"; {
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;
}
} }

View File

@@ -85,6 +85,7 @@
<Content Include="BackgroundImages\Snowflake.jpg" /> <Content Include="BackgroundImages\Snowflake.jpg" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="gravatar-dotnet" />
<PackageReference Include="Microsoft.Identity.Client" /> <PackageReference Include="Microsoft.Identity.Client" />
<PackageReference Include="Microsoft.UI.Xaml" /> <PackageReference Include="Microsoft.UI.Xaml" />
<PackageReference Include="CommunityToolkit.Common" /> <PackageReference Include="CommunityToolkit.Common" />

View File

@@ -40,6 +40,7 @@ public abstract class WinoApplication : Application, IRecipient<LanguageChanged>
protected IWinoServerConnectionManager<AppServiceConnection> AppServiceConnectionManager { get; } protected IWinoServerConnectionManager<AppServiceConnection> AppServiceConnectionManager { get; }
public IThemeService ThemeService { get; } public IThemeService ThemeService { get; }
public IUnderlyingThemeService UnderlyingThemeService { get; } public IUnderlyingThemeService UnderlyingThemeService { get; }
public IThumbnailService ThumbnailService { get; }
protected IDatabaseService DatabaseService { get; } protected IDatabaseService DatabaseService { get; }
protected ITranslationService TranslationService { get; } protected ITranslationService TranslationService { get; }
@@ -64,6 +65,7 @@ public abstract class WinoApplication : Application, IRecipient<LanguageChanged>
DatabaseService = Services.GetService<IDatabaseService>(); DatabaseService = Services.GetService<IDatabaseService>();
TranslationService = Services.GetService<ITranslationService>(); TranslationService = Services.GetService<ITranslationService>();
UnderlyingThemeService = Services.GetService<IUnderlyingThemeService>(); UnderlyingThemeService = Services.GetService<IUnderlyingThemeService>();
ThumbnailService = Services.GetService<IThumbnailService>();
// Make sure the paths are setup on app start. // Make sure the paths are setup on app start.
AppConfiguration.ApplicationDataFolderPath = ApplicationData.Current.LocalFolder.Path; AppConfiguration.ApplicationDataFolderPath = ApplicationData.Current.LocalFolder.Path;

View File

@@ -18,7 +18,6 @@ public partial class AppPreferencesPageViewModel : MailBaseViewModel
[ObservableProperty] [ObservableProperty]
private List<string> _appTerminationBehavior; private List<string> _appTerminationBehavior;
[ObservableProperty] [ObservableProperty]
public partial List<string> SearchModes { get; set; } public partial List<string> SearchModes { get; set; }

View File

@@ -129,11 +129,11 @@ public class WinoMailCollection
private async Task HandleExistingThreadAsync(ObservableGroup<object, IMailItem> group, ThreadMailItemViewModel threadViewModel, MailCopy addedItem) private async Task HandleExistingThreadAsync(ObservableGroup<object, IMailItem> group, ThreadMailItemViewModel threadViewModel, MailCopy addedItem)
{ {
var existingGroupKey = GetGroupingKey(threadViewModel); var existingGroupKey = GetGroupingKey(threadViewModel);
await ExecuteUIThread(() => { threadViewModel.AddMailItemViewModel(addedItem); }); await ExecuteUIThread(() => { threadViewModel.AddMailItemViewModel(addedItem); });
var newGroupKey = GetGroupingKey(threadViewModel); var newGroupKey = GetGroupingKey(threadViewModel);
if (!existingGroupKey.Equals(newGroupKey)) if (!existingGroupKey.Equals(newGroupKey))
{ {
await MoveThreadToNewGroupAsync(group, threadViewModel, newGroupKey); await MoveThreadToNewGroupAsync(group, threadViewModel, newGroupKey);
@@ -294,6 +294,25 @@ public class WinoMailCollection
return null; 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;
}
}
}
});
}
/// <summary> /// <summary>
/// Fins the item container that updated mail copy belongs to and updates it. /// Fins the item container that updated mail copy belongs to and updates it.
/// </summary> /// </summary>

View File

@@ -1,10 +1,17 @@
using Wino.Core.Domain; using System;
using CommunityToolkit.Mvvm.ComponentModel;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Entities.Shared;
namespace Wino.Mail.ViewModels.Data; 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) public AccountContactViewModel(AccountContact contact)
{ {
Address = contact.Address; Address = contact.Address;
@@ -39,4 +46,7 @@ public class AccountContactViewModel : AccountContact
/// Display name of the contact in a format: Name <Address>. /// Display name of the contact in a format: Name <Address>.
/// </summary> /// </summary>
public string DisplayName => Address == Name || string.IsNullOrWhiteSpace(Name) ? Address.ToLowerInvariant() : $"{Name} <{Address.ToLowerInvariant()}>"; public string DisplayName => Address == Name || string.IsNullOrWhiteSpace(Name) ? Address.ToLowerInvariant() : $"{Name} <{Address.ToLowerInvariant()}>";
[ObservableProperty]
public partial bool ThumbnailUpdatedEvent { get; set; }
} }

View File

@@ -13,7 +13,7 @@ namespace Wino.Mail.ViewModels.Data;
public partial class MailItemViewModel(MailCopy mailCopy) : ObservableObject, IMailItem public partial class MailItemViewModel(MailCopy mailCopy) : ObservableObject, IMailItem
{ {
[ObservableProperty] [ObservableProperty]
private MailCopy mailCopy = mailCopy; public partial MailCopy MailCopy { get; set; } = mailCopy;
public Guid UniqueId => ((IMailItem)MailCopy).UniqueId; public Guid UniqueId => ((IMailItem)MailCopy).UniqueId;
public string ThreadId => ((IMailItem)MailCopy).ThreadId; public string ThreadId => ((IMailItem)MailCopy).ThreadId;
@@ -23,10 +23,13 @@ public partial class MailItemViewModel(MailCopy mailCopy) : ObservableObject, IM
public string InReplyTo => ((IMailItem)MailCopy).InReplyTo; public string InReplyTo => ((IMailItem)MailCopy).InReplyTo;
[ObservableProperty] [ObservableProperty]
private bool isCustomFocused; public partial bool ThumbnailUpdatedEvent { get; set; } = false;
[ObservableProperty] [ObservableProperty]
private bool isSelected; public partial bool IsCustomFocused { get; set; }
[ObservableProperty]
public partial bool IsSelected { get; set; }
public bool IsFlagged public bool IsFlagged
{ {

View File

@@ -41,7 +41,8 @@ public partial class MailListPageViewModel : MailBaseViewModel,
IRecipient<AccountSynchronizationCompleted>, IRecipient<AccountSynchronizationCompleted>,
IRecipient<NewMailSynchronizationRequested>, IRecipient<NewMailSynchronizationRequested>,
IRecipient<AccountSynchronizerStateChanged>, IRecipient<AccountSynchronizerStateChanged>,
IRecipient<AccountCacheResetMessage> IRecipient<AccountCacheResetMessage>,
IRecipient<ThumbnailAdded>
{ {
private bool isChangingFolder = false; private bool isChangingFolder = false;
@@ -1140,4 +1141,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
}); });
} }
} }
public void Receive(ThumbnailAdded message) => MailCollection.UpdateThumbnails(message.Email);
} }

View File

@@ -25,12 +25,14 @@ using Wino.Mail.ViewModels.Data;
using Wino.Mail.ViewModels.Messages; using Wino.Mail.ViewModels.Messages;
using Wino.Messaging.Client.Mails; using Wino.Messaging.Client.Mails;
using Wino.Messaging.Server; using Wino.Messaging.Server;
using Wino.Messaging.UI;
using IMailService = Wino.Core.Domain.Interfaces.IMailService; using IMailService = Wino.Core.Domain.Interfaces.IMailService;
namespace Wino.Mail.ViewModels; namespace Wino.Mail.ViewModels;
public partial class MailRenderingPageViewModel : MailBaseViewModel, public partial class MailRenderingPageViewModel : MailBaseViewModel,
IRecipient<NewMailItemRenderingRequestedEvent>, IRecipient<NewMailItemRenderingRequestedEvent>,
IRecipient<ThumbnailAdded>,
ITransferProgress // For listening IMAP message download progress. ITransferProgress // For listening IMAP message download progress.
{ {
private readonly IMailDialogService _dialogService; private readonly IMailDialogService _dialogService;
@@ -788,4 +790,27 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
Log.Error(ex, "Failed to render mail."); 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<AccountContactViewModel> 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;
}
}
});
}
} }

View File

@@ -1,17 +1,19 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Input;
using Wino.Core.Domain; using Wino.Core.Domain;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
namespace Wino.Mail.ViewModels; namespace Wino.Mail.ViewModels;
public class MessageListPageViewModel : MailBaseViewModel public partial class MessageListPageViewModel : MailBaseViewModel
{ {
public IPreferencesService PreferencesService { get; } public IPreferencesService PreferencesService { get; }
private readonly IThumbnailService _thumbnailService;
private int selectedMarkAsOptionIndex; private int selectedMarkAsOptionIndex;
public int SelectedMarkAsOptionIndex public int SelectedMarkAsOptionIndex
{ {
get => selectedMarkAsOptionIndex; get => selectedMarkAsOptionIndex;
@@ -46,9 +48,7 @@ public class MessageListPageViewModel : MailBaseViewModel
]; ];
#region Properties #region Properties
private int leftHoverActionIndex; private int leftHoverActionIndex;
public int LeftHoverActionIndex public int LeftHoverActionIndex
{ {
get => leftHoverActionIndex; get => leftHoverActionIndex;
@@ -61,9 +61,7 @@ public class MessageListPageViewModel : MailBaseViewModel
} }
} }
private int centerHoverActionIndex; private int centerHoverActionIndex;
public int CenterHoverActionIndex public int CenterHoverActionIndex
{ {
get => centerHoverActionIndex; get => centerHoverActionIndex;
@@ -77,7 +75,6 @@ public class MessageListPageViewModel : MailBaseViewModel
} }
private int rightHoverActionIndex; private int rightHoverActionIndex;
public int RightHoverActionIndex public int RightHoverActionIndex
{ {
get => rightHoverActionIndex; get => rightHoverActionIndex;
@@ -89,18 +86,21 @@ public class MessageListPageViewModel : MailBaseViewModel
} }
} }
} }
#endregion #endregion
public MessageListPageViewModel(IMailDialogService dialogService, public MessageListPageViewModel(IPreferencesService preferencesService, IThumbnailService thumbnailService)
IPreferencesService preferencesService)
{ {
PreferencesService = preferencesService; PreferencesService = preferencesService;
_thumbnailService = thumbnailService;
leftHoverActionIndex = availableHoverActions.IndexOf(PreferencesService.LeftHoverAction); leftHoverActionIndex = availableHoverActions.IndexOf(PreferencesService.LeftHoverAction);
centerHoverActionIndex = availableHoverActions.IndexOf(PreferencesService.CenterHoverAction); centerHoverActionIndex = availableHoverActions.IndexOf(PreferencesService.CenterHoverAction);
rightHoverActionIndex = availableHoverActions.IndexOf(PreferencesService.RightHoverAction); rightHoverActionIndex = availableHoverActions.IndexOf(PreferencesService.RightHoverAction);
SelectedMarkAsOptionIndex = Array.IndexOf(Enum.GetValues<MailMarkAsOption>(), PreferencesService.MarkAsPreference); SelectedMarkAsOptionIndex = Array.IndexOf(Enum.GetValues<MailMarkAsOption>(), PreferencesService.MarkAsPreference);
} }
[RelayCommand]
private async Task ClearAvatarsCacheAsync()
{
await _thumbnailService.ClearCache();
}
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 574 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -5,13 +5,14 @@ using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Fernandezja.ColorHashSharp; using Fernandezja.ColorHashSharp;
using Microsoft.Extensions.DependencyInjection;
using Windows.UI; using Windows.UI;
using Windows.UI.Xaml; using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Imaging; using Windows.UI.Xaml.Media.Imaging;
using Windows.UI.Xaml.Shapes; using Windows.UI.Xaml.Shapes;
using Wino.Core.UWP.Services; using Wino.Core.Domain.Interfaces;
namespace Wino.Controls; namespace Wino.Controls;
@@ -21,12 +22,21 @@ public partial class ImagePreviewControl : Control
private const string PART_InitialsTextBlock = "InitialsTextBlock"; private const string PART_InitialsTextBlock = "InitialsTextBlock";
private const string PART_KnownHostImage = "KnownHostImage"; private const string PART_KnownHostImage = "KnownHostImage";
private const string PART_Ellipse = "Ellipse"; private const string PART_Ellipse = "Ellipse";
private const string PART_FaviconSquircle = "FaviconSquircle";
private const string PART_FaviconImage = "FaviconImage";
#region Dependency Properties #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 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, OnAddressInformationChanged)); 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(OnAddressInformationChanged))); 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); }
}
/// <summary> /// <summary>
/// Gets or sets base64 string of the sender contact picture. /// Gets or sets base64 string of the sender contact picture.
@@ -55,6 +65,8 @@ public partial class ImagePreviewControl : Control
private Grid InitialsGrid; private Grid InitialsGrid;
private TextBlock InitialsTextblock; private TextBlock InitialsTextblock;
private Image KnownHostImage; private Image KnownHostImage;
private Border FaviconSquircle;
private Image FaviconImage;
private CancellationTokenSource contactPictureLoadingCancellationTokenSource; private CancellationTokenSource contactPictureLoadingCancellationTokenSource;
public ImagePreviewControl() public ImagePreviewControl()
@@ -70,11 +82,13 @@ public partial class ImagePreviewControl : Control
InitialsTextblock = GetTemplateChild(PART_InitialsTextBlock) as TextBlock; InitialsTextblock = GetTemplateChild(PART_InitialsTextBlock) as TextBlock;
KnownHostImage = GetTemplateChild(PART_KnownHostImage) as Image; KnownHostImage = GetTemplateChild(PART_KnownHostImage) as Image;
Ellipse = GetTemplateChild(PART_Ellipse) as Ellipse; Ellipse = GetTemplateChild(PART_Ellipse) as Ellipse;
FaviconSquircle = GetTemplateChild(PART_FaviconSquircle) as Border;
FaviconImage = GetTemplateChild(PART_FaviconImage) as Image;
UpdateInformation(); UpdateInformation();
} }
private static void OnAddressInformationChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args) private static void OnInformationChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{ {
if (obj is ImagePreviewControl control) if (obj is ImagePreviewControl control)
control.UpdateInformation(); control.UpdateInformation();
@@ -82,7 +96,7 @@ public partial class ImagePreviewControl : Control
private async void UpdateInformation() 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; return;
// Cancel active image loading if exists. // Cancel active image loading if exists.
@@ -91,81 +105,100 @@ public partial class ImagePreviewControl : Control
contactPictureLoadingCancellationTokenSource.Cancel(); 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); contactPicture = await App.Current.ThumbnailService.GetThumbnailAsync(FromAddress);
isAvatarThumbnail = true;
isKnownHost = tuple.Item1;
host = tuple.Item2;
} }
if (isKnownHost) if (!string.IsNullOrEmpty(contactPicture))
{ {
// Unrealize others. if (isAvatarThumbnail && FaviconSquircle != null && FaviconImage != null)
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))
{ {
contactPictureLoadingCancellationTokenSource = new CancellationTokenSource(); // Show favicon in squircle
FaviconSquircle.Visibility = Visibility.Visible;
InitialsGrid.Visibility = Visibility.Collapsed;
KnownHostImage.Visibility = Visibility.Collapsed;
try var bitmapImage = await GetBitmapImageAsync(contactPicture);
{
var brush = await GetContactImageBrushAsync();
if (!contactPictureLoadingCancellationTokenSource?.Token.IsCancellationRequested ?? false) if (bitmapImage != null)
{
Ellipse.Fill = brush;
InitialsTextblock.Text = string.Empty;
}
}
catch (Exception)
{ {
// Log exception. FaviconImage.Source = bitmapImage;
Debugger.Break();
} }
} }
else else
{ {
var colorHash = new ColorHash(); // Show normal avatar (tondo)
var rgb = colorHash.Rgb(FromAddress); 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)); if (brush != null)
InitialsTextblock.Text = ExtractInitialsFromName(FromName); {
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<ImageBrush> GetContactImageBrushAsync() private static async Task<ImageBrush> GetContactImageBrushAsync(string base64)
{ {
// Load the image from base64 string. // Load the image from base64 string.
var bitmapImage = new BitmapImage();
var bitmapImage = await GetBitmapImageAsync(base64);
var imageArray = Convert.FromBase64String(SenderContactPicture); if (bitmapImage == null) return null;
var imageStream = new MemoryStream(imageArray);
var randomAccessImageStream = imageStream.AsRandomAccessStream();
randomAccessImageStream.Seek(0);
await bitmapImage.SetSourceAsync(randomAccessImageStream);
return new ImageBrush() { ImageSource = bitmapImage }; return new ImageBrush() { ImageSource = bitmapImage };
} }
private static async Task<BitmapImage> 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) public string ExtractInitialsFromName(string name)
{ {
// Change from name to from address in case of name doesn't exists. // Change from name to from address in case of name doesn't exists.

View File

@@ -76,6 +76,7 @@
FromAddress="{x:Bind MailItem.FromAddress, Mode=OneWay}" FromAddress="{x:Bind MailItem.FromAddress, Mode=OneWay}"
FromName="{x:Bind MailItem.FromName, Mode=OneWay}" FromName="{x:Bind MailItem.FromName, Mode=OneWay}"
SenderContactPicture="{x:Bind MailItem.SenderContact.Base64ContactPicture}" SenderContactPicture="{x:Bind MailItem.SenderContact.Base64ContactPicture}"
ThumbnailUpdatedEvent="{x:Bind IsThumbnailUpdated, Mode=OneWay}"
Visibility="{x:Bind IsAvatarVisible, Mode=OneWay}" /> Visibility="{x:Bind IsAvatarVisible, Mode=OneWay}" />
<Grid <Grid

View File

@@ -1,6 +1,7 @@
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Windows.Input; using System.Windows.Input;
using CommunityToolkit.WinUI;
using Windows.UI.Xaml; using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Controls;
using Wino.Core.Domain; using Wino.Core.Domain;
@@ -33,6 +34,13 @@ public sealed partial class MailItemDisplayInformationControl : UserControl
public static readonly DependencyProperty Prefer24HourTimeFormatProperty = DependencyProperty.Register(nameof(Prefer24HourTimeFormat), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(false)); public static readonly DependencyProperty Prefer24HourTimeFormatProperty = DependencyProperty.Register(nameof(Prefer24HourTimeFormat), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(false));
public static readonly DependencyProperty IsThreadExpanderVisibleProperty = DependencyProperty.Register(nameof(IsThreadExpanderVisible), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(false)); public static readonly DependencyProperty IsThreadExpanderVisibleProperty = DependencyProperty.Register(nameof(IsThreadExpanderVisible), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(false));
public static readonly DependencyProperty IsThreadExpandedProperty = DependencyProperty.Register(nameof(IsThreadExpanded), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(false)); public static readonly DependencyProperty IsThreadExpandedProperty = DependencyProperty.Register(nameof(IsThreadExpanded), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(false));
public static readonly DependencyProperty IsThumbnailUpdatedProperty = DependencyProperty.Register(nameof(IsThumbnailUpdated), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(false));
public bool IsThumbnailUpdated
{
get { return (bool)GetValue(IsThumbnailUpdatedProperty); }
set { SetValue(IsThumbnailUpdatedProperty, value); }
}
public bool IsThreadExpanded public bool IsThreadExpanded
{ {

View File

@@ -28,13 +28,27 @@
Foreground="White" /> Foreground="White" />
</Grid> </Grid>
<!-- Squircle for favicon -->
<Border
x:Name="FaviconSquircle"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="Transparent"
CornerRadius="6"
Visibility="Collapsed">
<Image x:Name="FaviconImage" Stretch="Fill" />
</Border>
<Image <Image
x:Name="KnownHostImage" x:Name="KnownHostImage"
Width="{TemplateBinding Width}" Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}" Height="{TemplateBinding Height}"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
Stretch="UniformToFill" /> Stretch="UniformToFill"
Visibility="Collapsed" />
</Grid> </Grid>
</ControlTemplate> </ControlTemplate>
</Setter.Value> </Setter.Value>

View File

@@ -113,6 +113,7 @@
IsAvatarVisible="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowSenderPicturesEnabled, Mode=OneWay}" IsAvatarVisible="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowSenderPicturesEnabled, Mode=OneWay}"
IsCustomFocused="{x:Bind IsCustomFocused, Mode=OneWay}" IsCustomFocused="{x:Bind IsCustomFocused, Mode=OneWay}"
IsHoverActionsEnabled="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsHoverActionsEnabled, 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}" LeftHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.LeftHoverAction, Mode=OneWay}"
MailItem="{x:Bind MailCopy, Mode=OneWay}" MailItem="{x:Bind MailCopy, Mode=OneWay}"
Prefer24HourTimeFormat="{Binding ElementName=root, Path=ViewModel.PreferencesService.Prefer24HourTimeFormat, 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}" IsAvatarVisible="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowSenderPicturesEnabled, Mode=OneWay}"
IsCustomFocused="{x:Bind IsCustomFocused, Mode=OneWay}" IsCustomFocused="{x:Bind IsCustomFocused, Mode=OneWay}"
IsHoverActionsEnabled="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsHoverActionsEnabled, 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}" LeftHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.LeftHoverAction, Mode=OneWay}"
MailItem="{x:Bind MailCopy, Mode=OneWay}" MailItem="{x:Bind MailCopy, Mode=OneWay}"
Prefer24HourTimeFormat="{Binding ElementName=root, Path=ViewModel.PreferencesService.Prefer24HourTimeFormat, Mode=OneWay}" Prefer24HourTimeFormat="{Binding ElementName=root, Path=ViewModel.PreferencesService.Prefer24HourTimeFormat, Mode=OneWay}"

View File

@@ -46,7 +46,8 @@
Height="36" Height="36"
FromAddress="{x:Bind Address}" FromAddress="{x:Bind Address}"
FromName="{x:Bind Name}" FromName="{x:Bind Name}"
SenderContactPicture="{x:Bind Base64ContactPicture}" /> SenderContactPicture="{x:Bind Base64ContactPicture}"
ThumbnailUpdatedEvent="{x:Bind ThumbnailUpdatedEvent, Mode=OneWay}" />
<TextBlock Grid.Column="1" Text="{x:Bind Name}" /> <TextBlock Grid.Column="1" Text="{x:Bind Name}" />

File diff suppressed because one or more lines are too long

View File

@@ -33,13 +33,6 @@
<None Remove="Assets\NotificationIcons\profile-dark.png" /> <None Remove="Assets\NotificationIcons\profile-dark.png" />
<None Remove="Assets\NotificationIcons\profile-light.png" /> <None Remove="Assets\NotificationIcons\profile-light.png" />
<None Remove="Assets\ReleaseNotes\1102.md" /> <None Remove="Assets\ReleaseNotes\1102.md" />
<None Remove="Assets\Thumbnails\airbnb.com.png" />
<None Remove="Assets\Thumbnails\apple.com.png" />
<None Remove="Assets\Thumbnails\google.com.png" />
<None Remove="Assets\Thumbnails\microsoft.com.png" />
<None Remove="Assets\Thumbnails\steampowered.com.png" />
<None Remove="Assets\Thumbnails\uber.com.png" />
<None Remove="Assets\Thumbnails\youtube.com.png" />
<None Remove="JS\editor.html" /> <None Remove="JS\editor.html" />
<None Remove="JS\editor.js" /> <None Remove="JS\editor.js" />
<None Remove="JS\global.css" /> <None Remove="JS\global.css" />
@@ -59,13 +52,6 @@
<Content Include="Assets\NotificationIcons\profile-dark.png" /> <Content Include="Assets\NotificationIcons\profile-dark.png" />
<Content Include="Assets\NotificationIcons\profile-light.png" /> <Content Include="Assets\NotificationIcons\profile-light.png" />
<Content Include="Assets\ReleaseNotes\1102.md" /> <Content Include="Assets\ReleaseNotes\1102.md" />
<Content Include="Assets\Thumbnails\airbnb.com.png" />
<Content Include="Assets\Thumbnails\apple.com.png" />
<Content Include="Assets\Thumbnails\google.com.png" />
<Content Include="Assets\Thumbnails\microsoft.com.png" />
<Content Include="Assets\Thumbnails\steampowered.com.png" />
<Content Include="Assets\Thumbnails\uber.com.png" />
<Content Include="Assets\Thumbnails\youtube.com.png" />
<Content Include="JS\editor.html" /> <Content Include="JS\editor.html" />
<Content Include="JS\editor.js" /> <Content Include="JS\editor.js" />
<Content Include="JS\global.css" /> <Content Include="JS\global.css" />

View File

@@ -0,0 +1,4 @@
using Wino.Core.Domain.Interfaces;
namespace Wino.Messaging.UI;
public record ThumbnailAdded(string Email): IUIMessage;

View File

@@ -79,6 +79,7 @@ public partial class App : Application
services.AddTransient<INotificationBuilder, NotificationBuilder>(); services.AddTransient<INotificationBuilder, NotificationBuilder>();
services.AddTransient<IUnderlyingThemeService, UnderlyingThemeService>(); services.AddTransient<IUnderlyingThemeService, UnderlyingThemeService>();
services.AddSingleton<IApplicationConfiguration, ApplicationConfiguration>(); services.AddSingleton<IApplicationConfiguration, ApplicationConfiguration>();
services.AddSingleton<IThumbnailService, ThumbnailService>();
// Register server message handler factory. // Register server message handler factory.
var serverMessageHandlerFactory = new ServerMessageHandlerFactory(); var serverMessageHandlerFactory = new ServerMessageHandlerFactory();

View File

@@ -34,11 +34,12 @@
</Resource> </Resource>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" /> <PackageReference Include="CommunityToolkit.Mvvm" />
<PackageReference Include="H.NotifyIcon.Wpf" /> <PackageReference Include="gravatar-dotnet" />
<PackageReference Include="CommunityToolkit.WinUI.Notifications" /> <PackageReference Include="H.NotifyIcon.Wpf" />
<PackageReference Include="Microsoft.Identity.Client" /> <PackageReference Include="CommunityToolkit.WinUI.Notifications" />
<PackageReference Include="System.Text.Encoding.CodePages" /> <PackageReference Include="Microsoft.Identity.Client" />
<PackageReference Include="System.Text.Encoding.CodePages" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Wino.Authentication\Wino.Authentication.csproj" /> <ProjectReference Include="..\Wino.Authentication\Wino.Authentication.csproj" />

View File

@@ -57,7 +57,8 @@ public class DatabaseService : IDatabaseService
typeof(AccountCalendar), typeof(AccountCalendar),
typeof(CalendarEventAttendee), typeof(CalendarEventAttendee),
typeof(CalendarItem), typeof(CalendarItem),
typeof(Reminder) typeof(Reminder),
typeof(Thumbnail)
); );
} }
} }