using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using CommunityToolkit.Mvvm.Messaging; using Microsoft.Toolkit.Uwp.Notifications; using Serilog; using Windows.ApplicationModel; using Windows.Data.Xml.Dom; using Windows.UI.Notifications; using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain; using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Messaging.UI; namespace Wino.Mail.WinUI.Services; public class NotificationBuilder : INotificationBuilder { private const string MailApplicationId = "App"; private readonly IAccountService _accountService; private readonly IFolderService _folderService; private readonly IMailService _mailService; private readonly IThumbnailService _thumbnailService; public NotificationBuilder(IAccountService accountService, IFolderService folderService, IMailService mailService, IThumbnailService thumbnailService) { _accountService = accountService; _folderService = folderService; _mailService = mailService; _thumbnailService = thumbnailService; WeakReferenceMessenger.Default.Register(this, (r, msg) => { RemoveNotification(msg.UniqueId); }); } public async Task CreateNotificationsAsync(IEnumerable downloadedMailItems) { try { // Filter mails to only include Inbox folder items var inboxMailItems = new List(); foreach (var item in downloadedMailItems) { var mailItem = await _mailService.GetSingleMailItemAsync(item.UniqueId); //if (mailItem == null || mailItem.AssignedFolder == null) // continue; //// Only create notifications for Inbox folder mails //if (mailItem.AssignedFolder.SpecialFolderType != SpecialFolderType.Inbox) // continue; //// Skip folders with synchronization disabled //if (!mailItem.AssignedFolder.IsSynchronizationEnabled) // continue; //// Skip already read mails //if (mailItem.IsRead) //{ // RemoveNotification(mailItem.UniqueId); // continue; //} inboxMailItems.Add(mailItem); } var mailCount = inboxMailItems.Count; if (mailCount == 0) return; // If there are more than 3 mails, just display 1 general toast. if (mailCount > 3) { var builder = new ToastContentBuilder(); builder.SetToastScenario(ToastScenario.Default); builder.AddText(Translator.Notifications_MultipleNotificationsTitle); builder.AddText(string.Format(Translator.Notifications_MultipleNotificationsMessage, mailCount)); builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail); builder.AddButton(GetDismissButton()); builder.AddAudio(new ToastAudio() { Src = new Uri("ms-winsoundevent:Notification.Mail") }); ShowToast(builder, ToastTargetApp.Mail); } else { foreach (var mailItem in inboxMailItems) { await CreateSingleNotificationAsync(mailItem); } await UpdateTaskbarIconBadgeAsync(); } } catch (Exception ex) { Log.Error(ex, "Failed to create notifications."); } } private async Task CreateSingleNotificationAsync(MailCopy mailItem) { var builder = new ToastContentBuilder(); builder.SetToastScenario(ToastScenario.Default); var avatarThumbnail = await _thumbnailService.GetThumbnailAsync(mailItem.FromAddress, awaitLoad: true); if (!string.IsNullOrEmpty(avatarThumbnail)) { 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 timestamp with received date of the mail. builder.AddCustomTimeStamp(mailItem.CreationDate.ToLocalTime()); builder.AddText(mailItem.FromName); builder.AddText(mailItem.Subject); builder.AddText(mailItem.PreviewText); builder.AddArgument(Constants.ToastMailUniqueIdKey, mailItem.UniqueId.ToString()); builder.AddArgument(Constants.ToastActionKey, MailOperation.Navigate); builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail); builder.AddButton(GetMarkAsReadButton(mailItem.UniqueId)); builder.AddButton(GetDeleteButton(mailItem.UniqueId)); builder.AddButton(GetArchiveButton(mailItem.UniqueId)); builder.AddAudio(new ToastAudio() { Src = new Uri("ms-winsoundevent:Notification.Mail") }); // Use UniqueId as tag to allow removal ShowToast(builder, ToastTargetApp.Mail, mailItem.UniqueId.ToString()); } private ToastButton GetDismissButton() => new ToastButton() .SetDismissActivation() .SetImageUri(new Uri("ms-appx:///Assets/NotificationIcons/dismiss.png")); private static ToastButton GetArchiveButton(Guid mailUniqueId) => new ToastButton() .SetContent(Translator.MailOperation_Archive) .SetImageUri(new Uri("ms-appx:///Assets/NotificationIcons/archive.png")) .AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString()) .AddArgument(Constants.ToastActionKey, MailOperation.Archive) .AddArgument(Constants.ToastModeKey, Constants.ToastModeMail) .SetBackgroundActivation(); private ToastButton GetDeleteButton(Guid mailUniqueId) => new ToastButton() .SetContent(Translator.MailOperation_Delete) .SetImageUri(new Uri("ms-appx:///Assets/NotificationIcons/delete.png")) .AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString()) .AddArgument(Constants.ToastActionKey, MailOperation.SoftDelete) .AddArgument(Constants.ToastModeKey, Constants.ToastModeMail) .SetBackgroundActivation(); private static ToastButton GetMarkAsReadButton(Guid mailUniqueId) => new ToastButton() .SetContent(Translator.MailOperation_MarkAsRead) .SetImageUri(new System.Uri("ms-appx:///Assets/NotificationIcons/markread.png")) .AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString()) .AddArgument(Constants.ToastActionKey, MailOperation.MarkAsRead) .AddArgument(Constants.ToastModeKey, Constants.ToastModeMail) .SetBackgroundActivation(); public async Task UpdateTaskbarIconBadgeAsync() { int totalUnreadCount = 0; try { var badgeUpdater = BadgeUpdateManager.CreateBadgeUpdaterForApplication(MailApplicationId); var accounts = await _accountService.GetAccountsAsync(); foreach (var account in accounts) { if (!account.Preferences.IsTaskbarBadgeEnabled) continue; var accountInbox = await _folderService.GetSpecialFolderByAccountIdAsync(account.Id, SpecialFolderType.Inbox); if (accountInbox == null) continue; var inboxUnreadCount = await _folderService.GetFolderNotificationBadgeAsync(accountInbox.Id); totalUnreadCount += inboxUnreadCount; } if (totalUnreadCount > 0) { // Get the blank badge XML payload for a badge number XmlDocument badgeXml = BadgeUpdateManager.GetTemplateContent(BadgeTemplateType.BadgeNumber); // Set the value of the badge in the XML to our number XmlElement? badgeElement = badgeXml.SelectSingleNode("/badge") as XmlElement; if (badgeElement == null) { badgeUpdater.Clear(); return; } badgeElement.SetAttribute("value", totalUnreadCount.ToString()); // Create the badge notification BadgeNotification badge = new BadgeNotification(badgeXml); // And update the badge badgeUpdater.Update(badge); } else badgeUpdater.Clear(); } catch (Exception ex) { Log.Error(ex, "Error while updating taskbar badge."); } } public void RemoveNotification(Guid mailUniqueId) { try { ToastNotificationManager.History.Remove(mailUniqueId.ToString(), null, GetAppUserModelId(ToastTargetApp.Mail)); } catch (Exception ex) { Log.Error(ex, $"Failed to remove notification for mail {mailUniqueId}"); } } public void CreateAttentionRequiredNotification(MailAccount account) { var builder = new ToastContentBuilder(); builder.SetToastScenario(ToastScenario.Default); builder.AddText(Translator.Exception_AccountNeedsAttention_Title); builder.AddText(string.Format(Translator.Exception_AccountNeedsAttention_Message, account.Name)); builder.AddButton(GetDismissButton()); builder.AddArgument(Constants.ToastMailAccountIdKey, account.Id.ToString()); builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail); builder.AddButton(new ToastButton().SetContent(Translator.Buttons_FixAccount)); ShowToast(builder, ToastTargetApp.Mail); } public void CreateWebView2RuntimeMissingNotification() { var builder = new ToastContentBuilder(); builder.SetToastScenario(ToastScenario.Default); builder.AddText(Translator.Exception_WebView2RuntimeMissing_Title); builder.AddText(Translator.Exception_WebView2RuntimeMissing_Message); builder.AddButton(GetDismissButton()); builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail); ShowToast(builder, ToastTargetApp.Mail); } public Task CreateCalendarReminderNotificationAsync(CalendarItem calendarItem, long reminderDurationInSeconds) { if (calendarItem == null) return Task.CompletedTask; var builder = new ToastContentBuilder(); builder.SetToastScenario(ToastScenario.Reminder); var localStart = calendarItem.LocalStartDate; var reminderMinutes = (int)Math.Max(0, reminderDurationInSeconds / 60); var reminderContext = reminderMinutes > 0 ? $"Starts in {reminderMinutes} minute{(reminderMinutes == 1 ? string.Empty : "s")}" : "Starting now"; builder.AddText(calendarItem.Title); builder.AddText($"{reminderContext} - {localStart:g}"); if (!string.IsNullOrWhiteSpace(calendarItem.Location)) builder.AddText(calendarItem.Location); builder.AddArgument(Constants.ToastCalendarActionKey, Constants.ToastCalendarNavigateAction); builder.AddArgument(Constants.ToastCalendarItemIdKey, calendarItem.Id.ToString()); builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeCalendar); builder.AddButton(GetDismissButton()); builder.AddAudio(new ToastAudio() { Src = new Uri("ms-winsoundevent:Notification.Reminder") }); var tag = $"calendar-reminder-{calendarItem.Id:N}-{reminderDurationInSeconds}"; ShowToast(builder, ToastTargetApp.Calendar, tag); return Task.CompletedTask; } private static void ShowToast(ToastContentBuilder builder, ToastTargetApp targetApp, string? tag = null) { var toastNotification = new ToastNotification(builder.GetToastContent().GetXml()); if (!string.IsNullOrWhiteSpace(tag)) { toastNotification.Tag = tag; } var appUserModelId = GetAppUserModelId(targetApp); var notifier = ToastNotificationManager.CreateToastNotifier(appUserModelId); notifier.Show(toastNotification); } private static string GetAppUserModelId(ToastTargetApp targetApp) { _ = targetApp; return $"{Package.Current.Id.FamilyName}!{MailApplicationId}"; } private enum ToastTargetApp { Mail, Calendar } }