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

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.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); }
}
/// <summary>
/// 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<ImageBrush> GetContactImageBrushAsync()
private static async Task<ImageBrush> 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<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)
{
// 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}"
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}" />
<Grid

View File

@@ -1,6 +1,7 @@
using System.Linq;
using System.Numerics;
using System.Windows.Input;
using CommunityToolkit.WinUI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
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 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 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
{

View File

@@ -28,13 +28,27 @@
Foreground="White" />
</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
x:Name="KnownHostImage"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Stretch="UniformToFill" />
Stretch="UniformToFill"
Visibility="Collapsed" />
</Grid>
</ControlTemplate>
</Setter.Value>

View File

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

View File

@@ -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}" />
<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-light.png" />
<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.js" />
<None Remove="JS\global.css" />
@@ -59,13 +52,6 @@
<Content Include="Assets\NotificationIcons\profile-dark.png" />
<Content Include="Assets\NotificationIcons\profile-light.png" />
<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.js" />
<Content Include="JS\global.css" />