2024-04-18 01:44:37 +02:00
|
|
|
|
using System;
|
2024-08-24 01:57:36 +02:00
|
|
|
|
using System.Diagnostics;
|
2024-08-23 01:07:00 +02:00
|
|
|
|
using System.IO;
|
2024-04-18 01:44:37 +02:00
|
|
|
|
using System.Text.RegularExpressions;
|
2024-08-24 01:57:36 +02:00
|
|
|
|
using System.Threading;
|
2024-08-23 01:07:00 +02:00
|
|
|
|
using System.Threading.Tasks;
|
2024-04-18 01:44:37 +02:00
|
|
|
|
using Fernandezja.ColorHashSharp;
|
2025-06-21 01:40:25 +02:00
|
|
|
|
using Microsoft.Extensions.DependencyInjection;
|
2024-04-18 01:44:37 +02:00
|
|
|
|
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;
|
2025-06-21 01:40:25 +02:00
|
|
|
|
using Wino.Core.Domain.Interfaces;
|
2024-04-18 01:44:37 +02:00
|
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
|
namespace Wino.Controls;
|
|
|
|
|
|
|
|
|
|
|
|
public partial class ImagePreviewControl : Control
|
2024-04-18 01:44:37 +02:00
|
|
|
|
{
|
2025-02-16 11:54:23 +01:00
|
|
|
|
private const string PART_EllipseInitialsGrid = "EllipseInitialsGrid";
|
|
|
|
|
|
private const string PART_InitialsTextBlock = "InitialsTextBlock";
|
|
|
|
|
|
private const string PART_KnownHostImage = "KnownHostImage";
|
|
|
|
|
|
private const string PART_Ellipse = "Ellipse";
|
2025-06-21 01:40:25 +02:00
|
|
|
|
private const string PART_FaviconSquircle = "FaviconSquircle";
|
|
|
|
|
|
private const string PART_FaviconImage = "FaviconImage";
|
2024-04-18 01:44:37 +02:00
|
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
|
#region Dependency Properties
|
2024-04-18 01:44:37 +02:00
|
|
|
|
|
2025-06-21 01:40:25 +02:00
|
|
|
|
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); }
|
|
|
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Gets or sets base64 string of the sender contact picture.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public string SenderContactPicture
|
|
|
|
|
|
{
|
|
|
|
|
|
get { return (string)GetValue(SenderContactPictureProperty); }
|
|
|
|
|
|
set { SetValue(SenderContactPictureProperty, value); }
|
|
|
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
|
public string FromName
|
|
|
|
|
|
{
|
|
|
|
|
|
get { return (string)GetValue(FromNameProperty); }
|
|
|
|
|
|
set { SetValue(FromNameProperty, value); }
|
|
|
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
|
public string FromAddress
|
|
|
|
|
|
{
|
|
|
|
|
|
get { return (string)GetValue(FromAddressProperty); }
|
|
|
|
|
|
set { SetValue(FromAddressProperty, value); }
|
|
|
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
|
#endregion
|
2024-04-18 01:44:37 +02:00
|
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
|
private Ellipse Ellipse;
|
|
|
|
|
|
private Grid InitialsGrid;
|
|
|
|
|
|
private TextBlock InitialsTextblock;
|
|
|
|
|
|
private Image KnownHostImage;
|
2025-06-21 01:40:25 +02:00
|
|
|
|
private Border FaviconSquircle;
|
|
|
|
|
|
private Image FaviconImage;
|
2025-02-16 11:54:23 +01:00
|
|
|
|
private CancellationTokenSource contactPictureLoadingCancellationTokenSource;
|
2024-04-18 01:44:37 +02:00
|
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
|
public ImagePreviewControl()
|
|
|
|
|
|
{
|
|
|
|
|
|
DefaultStyleKey = nameof(ImagePreviewControl);
|
|
|
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
|
protected override void OnApplyTemplate()
|
|
|
|
|
|
{
|
|
|
|
|
|
base.OnApplyTemplate();
|
2025-02-16 11:35:43 +01:00
|
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
|
InitialsGrid = GetTemplateChild(PART_EllipseInitialsGrid) as Grid;
|
|
|
|
|
|
InitialsTextblock = GetTemplateChild(PART_InitialsTextBlock) as TextBlock;
|
|
|
|
|
|
KnownHostImage = GetTemplateChild(PART_KnownHostImage) as Image;
|
|
|
|
|
|
Ellipse = GetTemplateChild(PART_Ellipse) as Ellipse;
|
2025-06-21 01:40:25 +02:00
|
|
|
|
FaviconSquircle = GetTemplateChild(PART_FaviconSquircle) as Border;
|
|
|
|
|
|
FaviconImage = GetTemplateChild(PART_FaviconImage) as Image;
|
2025-02-16 11:43:30 +01:00
|
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
|
UpdateInformation();
|
|
|
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
|
2025-06-21 01:40:25 +02:00
|
|
|
|
private static void OnInformationChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
|
2025-02-16 11:54:23 +01:00
|
|
|
|
{
|
|
|
|
|
|
if (obj is ImagePreviewControl control)
|
|
|
|
|
|
control.UpdateInformation();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async void UpdateInformation()
|
|
|
|
|
|
{
|
2025-06-21 01:40:25 +02:00
|
|
|
|
if ((KnownHostImage == null && FaviconSquircle == null) || InitialsGrid == null || InitialsTextblock == null || (string.IsNullOrEmpty(FromName) && string.IsNullOrEmpty(FromAddress)))
|
2025-02-16 11:54:23 +01:00
|
|
|
|
return;
|
2024-04-18 01:44:37 +02:00
|
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
|
// Cancel active image loading if exists.
|
|
|
|
|
|
if (!contactPictureLoadingCancellationTokenSource?.IsCancellationRequested ?? false)
|
2025-02-16 11:43:30 +01:00
|
|
|
|
{
|
2025-02-16 11:54:23 +01:00
|
|
|
|
contactPictureLoadingCancellationTokenSource.Cancel();
|
|
|
|
|
|
}
|
2024-08-24 01:57:36 +02:00
|
|
|
|
|
2025-06-21 01:40:25 +02:00
|
|
|
|
string contactPicture = SenderContactPicture;
|
2024-04-18 01:44:37 +02:00
|
|
|
|
|
2025-06-21 01:40:25 +02:00
|
|
|
|
var isAvatarThumbnail = false;
|
2024-08-24 01:57:36 +02:00
|
|
|
|
|
2025-06-21 01:40:25 +02:00
|
|
|
|
if (string.IsNullOrEmpty(contactPicture) && !string.IsNullOrEmpty(FromAddress))
|
2025-02-16 11:54:23 +01:00
|
|
|
|
{
|
2025-06-21 01:40:25 +02:00
|
|
|
|
contactPicture = await App.Current.ThumbnailService.GetThumbnailAsync(FromAddress);
|
|
|
|
|
|
isAvatarThumbnail = true;
|
2025-02-16 11:54:23 +01:00
|
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
|
2025-06-21 01:40:25 +02:00
|
|
|
|
if (!string.IsNullOrEmpty(contactPicture))
|
2025-02-16 11:54:23 +01:00
|
|
|
|
{
|
2025-06-21 01:40:25 +02:00
|
|
|
|
if (isAvatarThumbnail && FaviconSquircle != null && FaviconImage != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Show favicon in squircle
|
|
|
|
|
|
FaviconSquircle.Visibility = Visibility.Visible;
|
|
|
|
|
|
InitialsGrid.Visibility = Visibility.Collapsed;
|
|
|
|
|
|
KnownHostImage.Visibility = Visibility.Collapsed;
|
2024-04-18 01:44:37 +02:00
|
|
|
|
|
2025-06-21 01:40:25 +02:00
|
|
|
|
var bitmapImage = await GetBitmapImageAsync(contactPicture);
|
2025-02-16 11:43:30 +01:00
|
|
|
|
|
2025-06-21 01:40:25 +02:00
|
|
|
|
if (bitmapImage != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
FaviconImage.Source = bitmapImage;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
2024-04-18 01:44:37 +02:00
|
|
|
|
{
|
2025-06-21 01:40:25 +02:00
|
|
|
|
// Show normal avatar (tondo)
|
|
|
|
|
|
FaviconSquircle.Visibility = Visibility.Collapsed;
|
|
|
|
|
|
KnownHostImage.Visibility = Visibility.Collapsed;
|
|
|
|
|
|
InitialsGrid.Visibility = Visibility.Visible;
|
2025-02-16 11:54:23 +01:00
|
|
|
|
contactPictureLoadingCancellationTokenSource = new CancellationTokenSource();
|
|
|
|
|
|
try
|
2024-08-24 01:57:36 +02:00
|
|
|
|
{
|
2025-06-21 01:40:25 +02:00
|
|
|
|
var brush = await GetContactImageBrushAsync(contactPicture);
|
2024-08-24 01:57:36 +02:00
|
|
|
|
|
2025-06-21 01:40:25 +02:00
|
|
|
|
if (brush != null)
|
2024-08-24 01:57:36 +02:00
|
|
|
|
{
|
2025-06-21 01:40:25 +02:00
|
|
|
|
if (!contactPictureLoadingCancellationTokenSource?.Token.IsCancellationRequested ?? false)
|
|
|
|
|
|
{
|
|
|
|
|
|
Ellipse.Fill = brush;
|
|
|
|
|
|
InitialsTextblock.Text = string.Empty;
|
|
|
|
|
|
}
|
2024-08-24 01:57:36 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-02-16 11:54:23 +01:00
|
|
|
|
catch (Exception)
|
2024-08-23 01:07:00 +02:00
|
|
|
|
{
|
2025-02-16 11:54:23 +01:00
|
|
|
|
Debugger.Break();
|
2025-02-16 11:43:30 +01:00
|
|
|
|
}
|
2025-02-16 11:35:43 +01:00
|
|
|
|
}
|
2025-06-21 01:40:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
FaviconSquircle.Visibility = Visibility.Collapsed;
|
|
|
|
|
|
KnownHostImage.Visibility = Visibility.Collapsed;
|
|
|
|
|
|
InitialsGrid.Visibility = Visibility.Visible;
|
2025-02-16 11:54:23 +01:00
|
|
|
|
|
2025-06-21 01:40:25 +02:00
|
|
|
|
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);
|
2024-08-23 01:07:00 +02:00
|
|
|
|
}
|
2025-02-16 11:54:23 +01:00
|
|
|
|
}
|
2024-08-23 01:07:00 +02:00
|
|
|
|
|
2025-06-21 01:40:25 +02:00
|
|
|
|
private static async Task<ImageBrush> GetContactImageBrushAsync(string base64)
|
2025-02-16 11:54:23 +01:00
|
|
|
|
{
|
|
|
|
|
|
// Load the image from base64 string.
|
2025-06-21 01:40:25 +02:00
|
|
|
|
|
|
|
|
|
|
var bitmapImage = await GetBitmapImageAsync(base64);
|
2024-04-18 01:44:37 +02:00
|
|
|
|
|
2025-06-21 01:40:25 +02:00
|
|
|
|
if (bitmapImage == null) return null;
|
2024-04-18 01:44:37 +02:00
|
|
|
|
|
2025-06-21 01:40:25 +02:00
|
|
|
|
return new ImageBrush() { ImageSource = bitmapImage };
|
|
|
|
|
|
}
|
2024-08-23 01:07:00 +02:00
|
|
|
|
|
2025-06-21 01:40:25 +02:00
|
|
|
|
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) { }
|
2024-08-23 01:07:00 +02:00
|
|
|
|
|
2025-06-21 01:40:25 +02:00
|
|
|
|
return null;
|
2025-02-16 11:54:23 +01:00
|
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
|
public string ExtractInitialsFromName(string name)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Change from name to from address in case of name doesn't exists.
|
|
|
|
|
|
if (string.IsNullOrEmpty(name))
|
2024-04-18 01:44:37 +02:00
|
|
|
|
{
|
2025-02-16 11:54:23 +01:00
|
|
|
|
name = FromAddress;
|
|
|
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
|
// first remove all: punctuation, separator chars, control chars, and numbers (unicode style regexes)
|
|
|
|
|
|
string initials = Regex.Replace(name, @"[\p{P}\p{S}\p{C}\p{N}]+", "");
|
2024-04-18 01:44:37 +02:00
|
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
|
// Replacing all possible whitespace/separator characters (unicode style), with a single, regular ascii space.
|
|
|
|
|
|
initials = Regex.Replace(initials, @"\p{Z}+", " ");
|
2024-04-18 01:44:37 +02:00
|
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
|
// Remove all Sr, Jr, I, II, III, IV, V, VI, VII, VIII, IX at the end of names
|
|
|
|
|
|
initials = Regex.Replace(initials.Trim(), @"\s+(?:[JS]R|I{1,3}|I[VX]|VI{0,3})$", "", RegexOptions.IgnoreCase);
|
2024-04-18 01:44:37 +02:00
|
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
|
// Extract up to 2 initials from the remaining cleaned name.
|
|
|
|
|
|
initials = Regex.Replace(initials, @"^(\p{L})[^\s]*(?:\s+(?:\p{L}+\s+(?=\p{L}))?(?:(\p{L})\p{L}*)?)?$", "$1$2").Trim();
|
2024-04-18 01:44:37 +02:00
|
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
|
if (initials.Length > 2)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Worst case scenario, everything failed, just grab the first two letters of what we have left.
|
|
|
|
|
|
initials = initials.Substring(0, 2);
|
2025-02-16 11:43:30 +01:00
|
|
|
|
}
|
2025-02-16 11:54:23 +01:00
|
|
|
|
|
|
|
|
|
|
return initials.ToUpperInvariant();
|
2024-04-18 01:44:37 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|