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>
@@ -19,6 +19,7 @@
|
||||
<PackageVersion Include="CommunityToolkit.Uwp.Extensions" Version="8.2.250402" />
|
||||
<PackageVersion Include="CommunityToolkit.Uwp.Controls.Primitives" Version="8.2.250129-preview2" />
|
||||
<PackageVersion Include="EmailValidation" Version="1.3.0" />
|
||||
<PackageVersion Include="gravatar-dotnet" Version="0.1.3" />
|
||||
<PackageVersion Include="HtmlAgilityPack" Version="1.12.0" />
|
||||
<PackageVersion Include="Ical.Net" Version="4.3.1" />
|
||||
<PackageVersion Include="IsExternalInit" Version="1.0.3" />
|
||||
|
||||
14
Wino.Core.Domain/Entities/Shared/Thumbnail.cs
Normal 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; }
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// When any of the preferences are changed.
|
||||
@@ -193,6 +194,16 @@ public interface IPreferencesService
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
#region Calendar
|
||||
|
||||
20
Wino.Core.Domain/Interfaces/IThumbnailService.cs
Normal 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);
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -25,7 +25,7 @@ public static class CoreUWPContainerSetup
|
||||
services.AddSingleton<IPreferencesService, PreferencesService>();
|
||||
services.AddSingleton<IThemeService, ThemeService>();
|
||||
services.AddSingleton<IStatePersistanceService, StatePersistenceService>();
|
||||
|
||||
services.AddSingleton<IThumbnailService, ThumbnailService>();
|
||||
services.AddSingleton<IDialogServiceBase, DialogServiceBase>();
|
||||
services.AddTransient<IConfigurationService, ConfigurationService>();
|
||||
services.AddTransient<IFileService, FileService>();
|
||||
|
||||
@@ -45,7 +45,6 @@ public class NativeAppService : INativeAppService
|
||||
return _mimeMessagesFolder;
|
||||
}
|
||||
|
||||
|
||||
public async Task<string> GetEditorBundlePathAsync()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_editorBundlePath))
|
||||
|
||||
@@ -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<IMailItem> 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.
|
||||
|
||||
@@ -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<string> 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<Guid?>(nameof(StartupEntityId), null);
|
||||
|
||||
@@ -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<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 (string.IsNullOrEmpty(address))
|
||||
return string.Empty;
|
||||
|
||||
if (address.Contains('@'))
|
||||
if (!_isInitialized)
|
||||
{
|
||||
var splitted = address.Split('@');
|
||||
var thumbnailsList = await _databaseService.Connection.Table<Thumbnail>().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<string, (string graviton, string favicon)>(
|
||||
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<bool, string> CheckIsKnown(string host)
|
||||
public async Task ClearCache()
|
||||
{
|
||||
// Check known hosts.
|
||||
// Apply company logo if available.
|
||||
_cache?.Clear();
|
||||
_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
|
||||
{
|
||||
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<bool, string>(false, host);
|
||||
}
|
||||
|
||||
return new Tuple<bool, string>(IsKnown(host), host);
|
||||
catch { }
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string GetKnownHostImage(string host)
|
||||
=> $"ms-appx:///Assets/Thumbnails/{host}.png";
|
||||
private static async Task<string> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
<Content Include="BackgroundImages\Snowflake.jpg" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="gravatar-dotnet" />
|
||||
<PackageReference Include="Microsoft.Identity.Client" />
|
||||
<PackageReference Include="Microsoft.UI.Xaml" />
|
||||
<PackageReference Include="CommunityToolkit.Common" />
|
||||
|
||||
@@ -40,6 +40,7 @@ public abstract class WinoApplication : Application, IRecipient<LanguageChanged>
|
||||
protected IWinoServerConnectionManager<AppServiceConnection> 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<LanguageChanged>
|
||||
DatabaseService = Services.GetService<IDatabaseService>();
|
||||
TranslationService = Services.GetService<ITranslationService>();
|
||||
UnderlyingThemeService = Services.GetService<IUnderlyingThemeService>();
|
||||
ThumbnailService = Services.GetService<IThumbnailService>();
|
||||
|
||||
// Make sure the paths are setup on app start.
|
||||
AppConfiguration.ApplicationDataFolderPath = ApplicationData.Current.LocalFolder.Path;
|
||||
|
||||
@@ -18,7 +18,6 @@ public partial class AppPreferencesPageViewModel : MailBaseViewModel
|
||||
[ObservableProperty]
|
||||
private List<string> _appTerminationBehavior;
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
public partial List<string> SearchModes { get; set; }
|
||||
|
||||
|
||||
@@ -129,11 +129,11 @@ public class WinoMailCollection
|
||||
private async Task HandleExistingThreadAsync(ObservableGroup<object, IMailItem> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fins the item container that updated mail copy belongs to and updates it.
|
||||
/// </summary>
|
||||
|
||||
@@ -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 <Address>.
|
||||
/// </summary>
|
||||
public string DisplayName => Address == Name || string.IsNullOrWhiteSpace(Name) ? Address.ToLowerInvariant() : $"{Name} <{Address.ToLowerInvariant()}>";
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool ThumbnailUpdatedEvent { get; set; }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -41,7 +41,8 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
IRecipient<AccountSynchronizationCompleted>,
|
||||
IRecipient<NewMailSynchronizationRequested>,
|
||||
IRecipient<AccountSynchronizerStateChanged>,
|
||||
IRecipient<AccountCacheResetMessage>
|
||||
IRecipient<AccountCacheResetMessage>,
|
||||
IRecipient<ThumbnailAdded>
|
||||
{
|
||||
private bool isChangingFolder = false;
|
||||
|
||||
@@ -1140,4 +1141,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(ThumbnailAdded message) => MailCollection.UpdateThumbnails(message.Email);
|
||||
}
|
||||
|
||||
@@ -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<NewMailItemRenderingRequestedEvent>,
|
||||
IRecipient<ThumbnailAdded>,
|
||||
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<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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<MailMarkAsOption>(), PreferencesService.MarkAsPreference);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ClearAvatarsCacheAsync()
|
||||
{
|
||||
await _thumbnailService.ClearCache();
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 574 B |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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}" />
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
4
Wino.Messages/UI/ThumbnailAdded.cs
Normal file
@@ -0,0 +1,4 @@
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Messaging.UI;
|
||||
public record ThumbnailAdded(string Email): IUIMessage;
|
||||
@@ -79,6 +79,7 @@ public partial class App : Application
|
||||
services.AddTransient<INotificationBuilder, NotificationBuilder>();
|
||||
services.AddTransient<IUnderlyingThemeService, UnderlyingThemeService>();
|
||||
services.AddSingleton<IApplicationConfiguration, ApplicationConfiguration>();
|
||||
services.AddSingleton<IThumbnailService, ThumbnailService>();
|
||||
|
||||
// Register server message handler factory.
|
||||
var serverMessageHandlerFactory = new ServerMessageHandlerFactory();
|
||||
|
||||
@@ -34,11 +34,12 @@
|
||||
</Resource>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
<PackageReference Include="H.NotifyIcon.Wpf" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Notifications" />
|
||||
<PackageReference Include="Microsoft.Identity.Client" />
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
<PackageReference Include="gravatar-dotnet" />
|
||||
<PackageReference Include="H.NotifyIcon.Wpf" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Notifications" />
|
||||
<PackageReference Include="Microsoft.Identity.Client" />
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Wino.Authentication\Wino.Authentication.csproj" />
|
||||
|
||||
@@ -57,7 +57,8 @@ public class DatabaseService : IDatabaseService
|
||||
typeof(AccountCalendar),
|
||||
typeof(CalendarEventAttendee),
|
||||
typeof(CalendarItem),
|
||||
typeof(Reminder)
|
||||
typeof(Reminder),
|
||||
typeof(Thumbnail)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||