diff --git a/AGENTS.md b/AGENTS.md index b32a6ef6..4b451cce 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -150,6 +150,8 @@ private string searchQuery = string.Empty; - For dependency properties in WinUI code, always prefer `[GeneratedDependencyProperty]` from CommunityToolkit over manual `DependencyProperty.Register(...)` declarations. - When a `[RelayCommand]` needs enable/disable logic, prefer the command's `CanExecute` over binding `Button.IsEnabled` in XAML; use `[NotifyCanExecuteChangedFor]` on dependent properties and call `NotifyCanExecuteChanged()` explicitly when non-generated state affects the command. - In ViewModels, update all UI-bound properties/collections via `ExecuteUIThread(...)` (especially after awaited calls and any use of `ConfigureAwait(false)`). +- `ConfigureAwait(false)` continues execution on a background thread. Any UI-bound property change, `INotifyPropertyChanged` notification, collection mutation, or similar UI-facing state update after that point must be marshaled back with `ExecuteUIThread(...)` or the appropriate dispatcher call, otherwise the app can crash. +- Messenger messages are raised from a background thread by default, while UI control event handlers such as `Button.Click` start on the UI thread. Be deliberate when combining dispatcher usage with `ConfigureAwait(false)` so post-await UI updates always return to the UI thread. - ViewModels should only handle UI interaction/state and delegate business logic to services; account-management work belongs in `WinoAccountProfileService`, and preferences import/export/apply logic belongs in `PreferencesService`. - In `EventDetailsPageViewModel.LoadAttendeesAsync`, never mutate `CurrentEvent.Attendees` outside `ExecuteUIThread(...)`. - Never create pure C# controls or controls that heavily manipulate UI structure from `.cs` files. Define controls in XAML and keep UI composition in XAML. diff --git a/Wino.Core.Domain/Constants.cs b/Wino.Core.Domain/Constants.cs index e57f7696..dd1af841 100644 --- a/Wino.Core.Domain/Constants.cs +++ b/Wino.Core.Domain/Constants.cs @@ -24,6 +24,7 @@ public static class Constants public const string ToastModeKey = nameof(ToastModeKey); public const string ToastModeMail = nameof(ToastModeMail); public const string ToastModeCalendar = nameof(ToastModeCalendar); + public const string ToastDismissActionKey = nameof(ToastDismissActionKey); public const string ToastStoreUpdateActionKey = nameof(ToastStoreUpdateActionKey); public const string ToastStoreUpdateActionInstall = nameof(ToastStoreUpdateActionInstall); public const string ClientLogFile = "Client_.log"; diff --git a/Wino.Core.Domain/Enums/WinoPage.cs b/Wino.Core.Domain/Enums/WinoPage.cs index 5a23c97c..8f8e79bc 100644 --- a/Wino.Core.Domain/Enums/WinoPage.cs +++ b/Wino.Core.Domain/Enums/WinoPage.cs @@ -19,6 +19,7 @@ public enum WinoPage AboutPage, PersonalizationPage, MessageListPage, + MailNotificationSettingsPage, MailListPage, ReadComposePanePage, AppPreferencesPage, diff --git a/Wino.Core.Domain/Interfaces/IPreferencesService.cs b/Wino.Core.Domain/Interfaces/IPreferencesService.cs index c9f58ba3..cde1ca06 100644 --- a/Wino.Core.Domain/Interfaces/IPreferencesService.cs +++ b/Wino.Core.Domain/Interfaces/IPreferencesService.cs @@ -192,6 +192,16 @@ public interface IPreferencesService : INotifyPropertyChanged /// Guid? StartupEntityId { get; set; } + /// + /// Setting: First action button displayed on mail toast notifications. + /// + MailOperation FirstMailNotificationAction { get; set; } + + /// + /// Setting: Second action button displayed on mail toast notifications. + /// + MailOperation SecondMailNotificationAction { get; set; } + /// diff --git a/Wino.Core.Domain/Models/Settings/SettingsNavigationItemInfo.cs b/Wino.Core.Domain/Models/Settings/SettingsNavigationItemInfo.cs index 4a3581e4..274ce538 100644 --- a/Wino.Core.Domain/Models/Settings/SettingsNavigationItemInfo.cs +++ b/Wino.Core.Domain/Models/Settings/SettingsNavigationItemInfo.cs @@ -68,6 +68,11 @@ public static class SettingsNavigationInfoProvider Translator.SettingsMessageList_Description, "\uE8C4", searchKeywords: Translator.SettingsSearch_MessageList_Keywords), + new(WinoPage.MailNotificationSettingsPage, + Translator.SettingsMailNotifications_Title, + Translator.SettingsMailNotifications_Description, + "\uE7F4", + searchKeywords: Translator.SettingsSearch_MailNotifications_Keywords), new(WinoPage.ReadComposePanePage, Translator.SettingsReadComposePane_Title, Translator.SettingsReadComposePane_Description, @@ -149,6 +154,7 @@ public static class SettingsNavigationInfoProvider WinoPage.PersonalizationPage => Translator.SettingsPersonalization_Title, WinoPage.AboutPage => Translator.SettingsAbout_Title, WinoPage.MessageListPage => Translator.SettingsMessageList_Title, + WinoPage.MailNotificationSettingsPage => Translator.SettingsMailNotifications_Title, WinoPage.ReadComposePanePage => Translator.SettingsReadComposePane_Title, WinoPage.AppPreferencesPage => Translator.SettingsAppPreferences_Title, WinoPage.CalendarSettingsPage => Translator.CalendarSettings_Preferences_Title, diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json index b1fd54b5..fe103b57 100644 --- a/Wino.Core.Domain/Translations/en_US/resources.json +++ b/Wino.Core.Domain/Translations/en_US/resources.json @@ -84,6 +84,7 @@ "Buttons_Delete": "Delete", "Buttons_Deny": "Deny", "Buttons_Discard": "Discard", + "Buttons_Dismiss": "Dismiss", "Buttons_Edit": "Edit", "Buttons_EnableImageRendering": "Enable", "Buttons_Multiselect": "Select Multiple", @@ -910,6 +911,14 @@ "SettingsMarkAsRead_WhenSelected": "When selected", "SettingsMessageList_Description": "Change how your messages should be organized in mail list.", "SettingsMessageList_Title": "Message List", + "SettingsMailNotifications_Title": "Notifications", + "SettingsMailNotifications_Description": "Notification settings and preferences for mails.", + "SettingsMailNotifications_Actions_Title": "App notification actions.", + "SettingsMailNotifications_Actions_Description": "Customize the button behaviors on the notifications as you like.", + "SettingsMailNotifications_FirstAction_Title": "First notification action", + "SettingsMailNotifications_FirstAction_Description": "Choose the first button shown on mail notifications.", + "SettingsMailNotifications_SecondAction_Title": "Second notification action", + "SettingsMailNotifications_SecondAction_Description": "Choose the second button shown on mail notifications.", "SettingsNoAccountSetupMessage": "You didn't setup any accounts yet.", "SettingsNotifications_Description": "Turn on or off notifications for this account.", "SettingsNotifications_Title": "Notifications", @@ -946,6 +955,7 @@ "SettingsSearch_About_Keywords": "about;version;website;privacy;github;donate;store;support", "SettingsSearch_KeyboardShortcuts_Keywords": "shortcut;shortcuts;hotkey;hotkeys;keyboard;keys", "SettingsSearch_MessageList_Keywords": "message;messages;list;threading;threads;avatar;preview;sender", + "SettingsSearch_MailNotifications_Keywords": "mail;notification;notifications;toast;action;actions;reply;reply all;forward;archive;delete;junk;read", "SettingsSearch_ReadComposePane_Keywords": "reader;compose;composer;font;fonts;external content;display;reading", "SettingsSearch_SignatureAndEncryption_Keywords": "signature;signatures;encryption;certificate;certificates;s mime;smime;security", "SettingsSearch_Storage_Keywords": "storage;cache;caching;mime;disk;space;cleanup;clean up;local data", diff --git a/Wino.Mail.ViewModels/MailNotificationSettingsPageViewModel.cs b/Wino.Mail.ViewModels/MailNotificationSettingsPageViewModel.cs new file mode 100644 index 00000000..19032544 --- /dev/null +++ b/Wino.Mail.ViewModels/MailNotificationSettingsPageViewModel.cs @@ -0,0 +1,135 @@ +using System.Collections.ObjectModel; +using System.Linq; +using CommunityToolkit.Mvvm.ComponentModel; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain; + +namespace Wino.Mail.ViewModels; + +public partial class MailNotificationSettingsPageViewModel : MailBaseViewModel +{ + private static readonly MailOperation[] SupportedMailNotificationActions = + [ + MailOperation.MarkAsRead, + MailOperation.SoftDelete, + MailOperation.MoveToJunk, + MailOperation.Archive, + MailOperation.Reply, + MailOperation.ReplyAll, + MailOperation.Forward + ]; + + private readonly IPreferencesService _preferencesService; + private bool _isUpdatingSelection; + private bool _isLoaded; + + public ObservableCollection AvailableNotificationActions { get; } = []; + + [ObservableProperty] + public partial MailNotificationActionOption SelectedFirstAction { get; set; } + + [ObservableProperty] + public partial MailNotificationActionOption SelectedSecondAction { get; set; } + + public MailNotificationSettingsPageViewModel(IPreferencesService preferencesService) + { + _preferencesService = preferencesService; + + foreach (var action in SupportedMailNotificationActions) + { + AvailableNotificationActions.Add(new MailNotificationActionOption(action, GetOperationDisplayText(action))); + } + + InitializeSelections(); + _isLoaded = true; + } + + partial void OnSelectedFirstActionChanged(MailNotificationActionOption value) + { + if (!_isLoaded || _isUpdatingSelection || value == null) + return; + + EnsureDistinctSelections(changedSelection: value, isFirstSelection: true); + _preferencesService.FirstMailNotificationAction = value.Operation; + } + + partial void OnSelectedSecondActionChanged(MailNotificationActionOption value) + { + if (!_isLoaded || _isUpdatingSelection || value == null) + return; + + EnsureDistinctSelections(changedSelection: value, isFirstSelection: false); + _preferencesService.SecondMailNotificationAction = value.Operation; + } + + private void InitializeSelections() + { + var firstAction = ResolveSupportedAction(_preferencesService.FirstMailNotificationAction, MailOperation.MarkAsRead); + var secondAction = ResolveSupportedAction(_preferencesService.SecondMailNotificationAction, MailOperation.SoftDelete); + + if (secondAction == firstAction) + { + secondAction = GetFallbackDistinctAction(firstAction); + } + + SelectedFirstAction = GetOption(firstAction); + SelectedSecondAction = GetOption(secondAction); + + _preferencesService.FirstMailNotificationAction = firstAction; + _preferencesService.SecondMailNotificationAction = secondAction; + } + + private void EnsureDistinctSelections(MailNotificationActionOption changedSelection, bool isFirstSelection) + { + var otherSelection = isFirstSelection ? SelectedSecondAction : SelectedFirstAction; + if (otherSelection?.Operation != changedSelection.Operation) + return; + + _isUpdatingSelection = true; + + var fallbackAction = GetFallbackDistinctAction(changedSelection.Operation); + var fallbackOption = GetOption(fallbackAction); + + if (isFirstSelection) + { + SelectedSecondAction = fallbackOption; + _preferencesService.SecondMailNotificationAction = fallbackAction; + } + else + { + SelectedFirstAction = fallbackOption; + _preferencesService.FirstMailNotificationAction = fallbackAction; + } + + _isUpdatingSelection = false; + } + + private MailNotificationActionOption GetOption(MailOperation action) + => AvailableNotificationActions.First(option => option.Operation == action); + + private static MailOperation ResolveSupportedAction(MailOperation action, MailOperation fallbackAction) + => SupportedMailNotificationActions.Contains(action) ? action : fallbackAction; + + private static MailOperation GetFallbackDistinctAction(MailOperation excludedAction) + => SupportedMailNotificationActions.First(action => action != excludedAction); + + private static string GetOperationDisplayText(MailOperation action) + => action switch + { + MailOperation.MarkAsRead => Translator.MailOperation_MarkAsRead, + MailOperation.SoftDelete => Translator.MailOperation_Delete, + MailOperation.MoveToJunk => Translator.MailOperation_MarkAsJunk, + MailOperation.Archive => Translator.MailOperation_Archive, + MailOperation.Reply => Translator.MailOperation_Reply, + MailOperation.ReplyAll => Translator.MailOperation_ReplyAll, + MailOperation.Forward => Translator.MailOperation_Forward, + _ => action.ToString() + }; +} + +public sealed class MailNotificationActionOption(MailOperation operation, string displayText) +{ + public MailOperation Operation { get; } = operation; + public string DisplayText { get; } = displayText; +} diff --git a/Wino.Mail.WinUI/Activation/ToastActivationResolver.cs b/Wino.Mail.WinUI/Activation/ToastActivationResolver.cs index bbbf829d..e3cb7426 100644 --- a/Wino.Mail.WinUI/Activation/ToastActivationResolver.cs +++ b/Wino.Mail.WinUI/Activation/ToastActivationResolver.cs @@ -41,9 +41,14 @@ internal static class ToastActivationResolver return calendarAction == Constants.ToastCalendarNavigateAction; } + if (toastArguments.TryGetValue(Constants.ToastDismissActionKey, out string _)) + { + return false; + } + if (toastArguments.TryGetValue(Constants.ToastActionKey, out MailOperation mailAction)) { - return mailAction == MailOperation.Navigate; + return mailAction is MailOperation.Navigate or MailOperation.Reply or MailOperation.ReplyAll or MailOperation.Forward; } return true; @@ -52,5 +57,6 @@ internal static class ToastActivationResolver private static bool ContainsKnownToastKey(NotificationArguments toastArguments) => toastArguments.TryGetValue(Constants.ToastStoreUpdateActionKey, out string _) || toastArguments.TryGetValue(Constants.ToastCalendarActionKey, out string _) || + toastArguments.TryGetValue(Constants.ToastDismissActionKey, out string _) || toastArguments.TryGetValue(Constants.ToastActionKey, out string _); } diff --git a/Wino.Mail.WinUI/App.xaml.cs b/Wino.Mail.WinUI/App.xaml.cs index b3b7189a..893990b5 100644 --- a/Wino.Mail.WinUI/App.xaml.cs +++ b/Wino.Mail.WinUI/App.xaml.cs @@ -358,6 +358,7 @@ public partial class App : WinoApplication, services.AddTransient(typeof(AccountDetailsPageViewModel)); services.AddTransient(typeof(SignatureManagementPageViewModel)); services.AddTransient(typeof(MessageListPageViewModel)); + services.AddTransient(typeof(MailNotificationSettingsPageViewModel)); services.AddTransient(typeof(ReadComposePanePageViewModel)); services.AddTransient(typeof(MergedAccountDetailsPageViewModel)); services.AddTransient(typeof(AppPreferencesPageViewModel)); @@ -599,6 +600,12 @@ public partial class App : WinoApplication, return; } + if (toastArguments.TryGetValue(Constants.ToastDismissActionKey, out string _)) + { + LogActivation("Handling notification dismiss action."); + return; + } + // Check calendar reminder toast activation first. if (toastArguments.TryGetValue(Constants.ToastCalendarActionKey, out string calendarAction) && toastArguments.TryGetValue(Constants.ToastCalendarItemIdKey, out string calendarItemIdString) && @@ -632,6 +639,10 @@ public partial class App : WinoApplication, // User clicked notification - create window if needed and navigate. await HandleToastNavigationAsync(mailItemUniqueId); } + else if (IsComposeToastAction(action)) + { + await HandleToastComposeActionAsync(action, mailItemUniqueId); + } else { // User clicked action button (Mark as Read, Delete, etc.) @@ -985,6 +996,87 @@ public partial class App : WinoApplication, } } + private async Task HandleToastComposeActionAsync(MailOperation action, Guid mailItemUniqueId) + { + LogActivation($"Handling compose toast action: {action} for mail {mailItemUniqueId}"); + + var mailService = Services.GetRequiredService(); + var folderService = Services.GetRequiredService(); + var mimeFileService = Services.GetRequiredService(); + var navigationService = Services.GetRequiredService(); + var requestDelegator = Services.GetRequiredService(); + var mailShellViewModel = Services.GetRequiredService(); + + var mailItem = await mailService.GetSingleMailItemAsync(mailItemUniqueId); + if (mailItem == null) + { + LogActivation($"Compose toast mail item was not found for {mailItemUniqueId}."); + return; + } + + var account = await mailService.GetMailAccountByUniqueIdAsync(mailItemUniqueId) ?? mailItem.AssignedAccount; + if (account == null) + { + LogActivation($"Compose toast account was not found for {mailItemUniqueId}."); + return; + } + + var draftFolder = await folderService.GetSpecialFolderByAccountIdAsync(account.Id, SpecialFolderType.Draft); + if (draftFolder == null) + { + LogActivation($"Compose toast draft folder is missing for account {account.Id}."); + return; + } + + var mimeInformation = await mimeFileService.GetMimeMessageInformationAsync(mailItem.FileId, account.Id); + if (mimeInformation?.MimeMessage == null) + { + LogActivation($"Compose toast MIME payload was not found for mail {mailItemUniqueId}."); + return; + } + + await EnsureShellWindowAsync(WinoApplicationMode.Mail, activateWindow: true); + navigationService.ChangeApplicationMode(WinoApplicationMode.Mail); + + if (mailShellViewModel.MenuItems.TryGetAccountMenuItem(account.Id, out IAccountMenuItem accountMenuItem)) + { + await mailShellViewModel.ChangeLoadedAccountAsync(accountMenuItem, navigateInbox: false); + } + + if (mailShellViewModel.MenuItems.TryGetSpecialFolderMenuItem(account.Id, SpecialFolderType.Draft, out var draftFolderMenuItem)) + { + await mailShellViewModel.NavigateFolderAsync(draftFolderMenuItem); + } + + var draftOptions = new DraftCreationOptions + { + Reason = action switch + { + MailOperation.Reply => DraftCreationReason.Reply, + MailOperation.ReplyAll => DraftCreationReason.ReplyAll, + MailOperation.Forward => DraftCreationReason.Forward, + _ => DraftCreationReason.Empty + }, + ReferencedMessage = new ReferencedMessage + { + MimeMessage = mimeInformation.MimeMessage, + MailCopy = mailItem + } + }; + + var (draftMailCopy, draftBase64MimeMessage) = await mailService.CreateDraftAsync(account.Id, draftOptions); + var draftPreparationRequest = new DraftPreparationRequest(account, draftMailCopy, draftBase64MimeMessage, draftOptions.Reason, mailItem); + + await requestDelegator.ExecuteAsync(draftPreparationRequest); + navigationService.Navigate(WinoPage.ComposePage, + new MailItemViewModel(draftMailCopy), + NavigationReferenceFrame.RenderingFrame, + NavigationTransitionType.DrillIn); + } + + private static bool IsComposeToastAction(MailOperation action) + => action is MailOperation.Reply or MailOperation.ReplyAll or MailOperation.Forward; + /// /// Creates the main window and activates it. /// diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-100/mail-forward.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-100/mail-forward.png new file mode 100644 index 00000000..38c3e00a Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-100/mail-forward.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-100/mail-junk.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-100/mail-junk.png new file mode 100644 index 00000000..098634b9 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-100/mail-junk.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-100/mail-reply.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-100/mail-reply.png new file mode 100644 index 00000000..6366658c Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-100/mail-reply.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-100/mail-replyall.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-100/mail-replyall.png new file mode 100644 index 00000000..aa9644ad Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-100/mail-replyall.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-125/mail-forward.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-125/mail-forward.png new file mode 100644 index 00000000..7b5f2532 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-125/mail-forward.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-125/mail-junk.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-125/mail-junk.png new file mode 100644 index 00000000..7532e7c6 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-125/mail-junk.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-125/mail-reply.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-125/mail-reply.png new file mode 100644 index 00000000..c96bb76b Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-125/mail-reply.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-125/mail-replyall.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-125/mail-replyall.png new file mode 100644 index 00000000..3029bcaa Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-125/mail-replyall.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-150/mail-forward.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-150/mail-forward.png new file mode 100644 index 00000000..7431c7c8 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-150/mail-forward.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-150/mail-junk.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-150/mail-junk.png new file mode 100644 index 00000000..f1156562 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-150/mail-junk.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-150/mail-reply.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-150/mail-reply.png new file mode 100644 index 00000000..50fb6bf7 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-150/mail-reply.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-150/mail-replyall.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-150/mail-replyall.png new file mode 100644 index 00000000..5875c1c3 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-150/mail-replyall.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-200/mail-forward.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-200/mail-forward.png new file mode 100644 index 00000000..ced855f6 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-200/mail-forward.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-200/mail-junk.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-200/mail-junk.png new file mode 100644 index 00000000..ca265c3f Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-200/mail-junk.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-200/mail-reply.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-200/mail-reply.png new file mode 100644 index 00000000..972a9bf1 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-200/mail-reply.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-200/mail-replyall.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-200/mail-replyall.png new file mode 100644 index 00000000..2a94da2b Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-200/mail-replyall.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-400/mail-forward.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-400/mail-forward.png new file mode 100644 index 00000000..81125184 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-400/mail-forward.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-400/mail-junk.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-400/mail-junk.png new file mode 100644 index 00000000..ab1ec9eb Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-400/mail-junk.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-400/mail-reply.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-400/mail-reply.png new file mode 100644 index 00000000..22b30d41 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-400/mail-reply.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-400/mail-replyall.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-400/mail-replyall.png new file mode 100644 index 00000000..86b55939 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-dark/scale-400/mail-replyall.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-100/mail-forward.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-100/mail-forward.png new file mode 100644 index 00000000..48d584f3 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-100/mail-forward.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-100/mail-junk.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-100/mail-junk.png new file mode 100644 index 00000000..6ec71bc7 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-100/mail-junk.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-100/mail-reply.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-100/mail-reply.png new file mode 100644 index 00000000..3212d6eb Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-100/mail-reply.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-100/mail-replyall.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-100/mail-replyall.png new file mode 100644 index 00000000..4eec5a28 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-100/mail-replyall.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-125/mail-forward.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-125/mail-forward.png new file mode 100644 index 00000000..9b6f9bc0 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-125/mail-forward.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-125/mail-junk.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-125/mail-junk.png new file mode 100644 index 00000000..6bdb71c4 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-125/mail-junk.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-125/mail-reply.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-125/mail-reply.png new file mode 100644 index 00000000..8babc787 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-125/mail-reply.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-125/mail-replyall.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-125/mail-replyall.png new file mode 100644 index 00000000..b93039b4 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-125/mail-replyall.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-150/mail-forward.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-150/mail-forward.png new file mode 100644 index 00000000..a10134b0 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-150/mail-forward.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-150/mail-junk.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-150/mail-junk.png new file mode 100644 index 00000000..90f992af Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-150/mail-junk.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-150/mail-reply.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-150/mail-reply.png new file mode 100644 index 00000000..f8ff82d1 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-150/mail-reply.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-150/mail-replyall.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-150/mail-replyall.png new file mode 100644 index 00000000..e73efc66 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-150/mail-replyall.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-200/mail-forward.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-200/mail-forward.png new file mode 100644 index 00000000..0abf2501 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-200/mail-forward.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-200/mail-junk.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-200/mail-junk.png new file mode 100644 index 00000000..7f32d4bc Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-200/mail-junk.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-200/mail-reply.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-200/mail-reply.png new file mode 100644 index 00000000..502d0055 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-200/mail-reply.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-200/mail-replyall.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-200/mail-replyall.png new file mode 100644 index 00000000..9ed846f8 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-200/mail-replyall.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-400/mail-forward.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-400/mail-forward.png new file mode 100644 index 00000000..5279390a Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-400/mail-forward.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-400/mail-junk.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-400/mail-junk.png new file mode 100644 index 00000000..e0c791b4 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-400/mail-junk.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-400/mail-reply.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-400/mail-reply.png new file mode 100644 index 00000000..fad78de3 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-400/mail-reply.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-400/mail-replyall.png b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-400/mail-replyall.png new file mode 100644 index 00000000..2887def3 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/theme-light/scale-400/mail-replyall.png differ diff --git a/Wino.Mail.WinUI/Services/NavigationService.cs b/Wino.Mail.WinUI/Services/NavigationService.cs index cdf45cca..233df6e3 100644 --- a/Wino.Mail.WinUI/Services/NavigationService.cs +++ b/Wino.Mail.WinUI/Services/NavigationService.cs @@ -79,6 +79,7 @@ public class NavigationService : NavigationServiceBase, INavigationService WinoPage.AboutPage, WinoPage.PersonalizationPage, WinoPage.MessageListPage, + WinoPage.MailNotificationSettingsPage, WinoPage.ReadComposePanePage, WinoPage.AppPreferencesPage, WinoPage.AliasManagementPage, @@ -142,6 +143,7 @@ public class NavigationService : NavigationServiceBase, INavigationService WinoPage.AboutPage => typeof(AboutPage), WinoPage.PersonalizationPage => typeof(PersonalizationPage), WinoPage.MessageListPage => typeof(MessageListPage), + WinoPage.MailNotificationSettingsPage => typeof(MailNotificationSettingsPage), WinoPage.ReadComposePanePage => typeof(ReadComposePanePage), WinoPage.MailRenderingPage => typeof(MailRenderingPage), WinoPage.ComposePage => typeof(ComposePage), diff --git a/Wino.Mail.WinUI/Services/NotificationBuilder.cs b/Wino.Mail.WinUI/Services/NotificationBuilder.cs index 3a134f50..63813f09 100644 --- a/Wino.Mail.WinUI/Services/NotificationBuilder.cs +++ b/Wino.Mail.WinUI/Services/NotificationBuilder.cs @@ -17,6 +17,7 @@ using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Extensions; using Wino.Core.Domain.Interfaces; +using Wino.Helpers; using Wino.Mail.WinUI.Activation; using Wino.Messaging.UI; @@ -26,6 +27,16 @@ public class NotificationBuilder : INotificationBuilder { private const string NotificationIconRootUri = "ms-appx:///Assets/NotificationIcons/"; private static int _calendarTaskbarBadgeCount; + private static readonly MailOperation[] SupportedMailNotificationActions = + [ + MailOperation.MarkAsRead, + MailOperation.SoftDelete, + MailOperation.MoveToJunk, + MailOperation.Archive, + MailOperation.Reply, + MailOperation.ReplyAll, + MailOperation.Forward + ]; private readonly IAccountService _accountService; private readonly IFolderService _folderService; @@ -76,6 +87,7 @@ public class NotificationBuilder : INotificationBuilder builder.AddText(Translator.Notifications_MultipleNotificationsTitle); builder.AddText(string.Format(Translator.Notifications_MultipleNotificationsMessage, mailCount)); builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail); + builder.AddButton(CreateDismissButton()); builder.SetAudioUri(new Uri("ms-winsoundevent:Notification.Mail")); ShowNotification(builder); @@ -167,6 +179,7 @@ public class NotificationBuilder : INotificationBuilder builder.AddButton(new AppNotificationButton(Translator.Buttons_FixAccount) .AddArgument(Constants.ToastMailAccountIdKey, account.Id.ToString()) .AddArgument(Constants.ToastModeKey, Constants.ToastModeMail)); + builder.AddButton(CreateDismissButton()); ShowNotification(builder); } @@ -177,6 +190,7 @@ public class NotificationBuilder : INotificationBuilder builder.AddText(Translator.Exception_WebView2RuntimeMissing_Title); builder.AddText(Translator.Exception_WebView2RuntimeMissing_Message); builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail); + builder.AddButton(CreateDismissButton()); ShowNotification(builder); } @@ -188,6 +202,7 @@ public class NotificationBuilder : INotificationBuilder builder.AddText(Translator.Notifications_StoreUpdateAvailableMessage); builder.AddArgument(Constants.ToastStoreUpdateActionKey, Constants.ToastStoreUpdateActionInstall); builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail); + builder.AddButton(CreateDismissButton()); ShowNotification(builder, "store-update-available"); } @@ -255,6 +270,8 @@ public class NotificationBuilder : INotificationBuilder .AddArgument(Constants.ToastModeKey, Constants.ToastModeCalendar)); } + builder.AddButton(CreateDismissButton()); + var tag = $"calendar-reminder-{calendarItem.Id:N}-{reminderDurationInSeconds}"; ShowNotification(builder, tag); @@ -288,9 +305,11 @@ public class NotificationBuilder : INotificationBuilder builder.AddArgument(Constants.ToastMailUniqueIdKey, mailItem.UniqueId.ToString()); builder.AddArgument(Constants.ToastActionKey, MailOperation.Navigate.ToString()); builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail); - builder.AddButton(GetMarkAsReadButton(mailItem.UniqueId)); - builder.AddButton(GetDeleteButton(mailItem.UniqueId)); - builder.AddButton(GetArchiveButton(mailItem.UniqueId)); + + var (firstAction, secondAction) = GetConfiguredMailNotificationActions(); + builder.AddButton(CreateMailNotificationActionButton(firstAction, mailItem.UniqueId)); + builder.AddButton(CreateMailNotificationActionButton(secondAction, mailItem.UniqueId)); + builder.AddButton(CreateDismissButton()); builder.SetAudioUri(new Uri("ms-winsoundevent:Notification.Mail")); ShowNotification(builder, mailItem.UniqueId.ToString()); @@ -347,26 +366,55 @@ public class NotificationBuilder : INotificationBuilder return string.Format(Translator.CalendarReminder_StartedMinutesAgo, minutesAgo); } - private AppNotificationButton GetArchiveButton(Guid mailUniqueId) - => new AppNotificationButton(Translator.MailOperation_Archive) - .SetIcon(GetNotificationIconUri("mail-archive")) + private (MailOperation FirstAction, MailOperation SecondAction) GetConfiguredMailNotificationActions() + { + var firstAction = ResolveMailNotificationAction(_preferencesService.FirstMailNotificationAction, MailOperation.MarkAsRead); + var secondAction = ResolveMailNotificationAction(_preferencesService.SecondMailNotificationAction, MailOperation.SoftDelete); + + if (secondAction == firstAction) + { + secondAction = SupportedMailNotificationActions.First(action => action != firstAction); + } + + return (firstAction, secondAction); + } + + private static MailOperation ResolveMailNotificationAction(MailOperation configuredAction, MailOperation fallbackAction) + => SupportedMailNotificationActions.Contains(configuredAction) ? configuredAction : fallbackAction; + + private AppNotificationButton CreateMailNotificationActionButton(MailOperation action, Guid mailUniqueId) + { + var button = new AppNotificationButton(XamlHelpers.GetOperationString(action)) .AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString()) - .AddArgument(Constants.ToastActionKey, MailOperation.Archive.ToString()) + .AddArgument(Constants.ToastActionKey, action.ToString()) .AddArgument(Constants.ToastModeKey, Constants.ToastModeMail); - private AppNotificationButton GetDeleteButton(Guid mailUniqueId) - => new AppNotificationButton(Translator.MailOperation_Delete) - .SetIcon(GetNotificationIconUri("mail-delete")) - .AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString()) - .AddArgument(Constants.ToastActionKey, MailOperation.SoftDelete.ToString()) - .AddArgument(Constants.ToastModeKey, Constants.ToastModeMail); + var iconUri = GetMailActionIconUri(action); + if (iconUri != null) + { + button.SetIcon(iconUri); + } - private AppNotificationButton GetMarkAsReadButton(Guid mailUniqueId) - => new AppNotificationButton(Translator.MailOperation_MarkAsRead) - .SetIcon(GetNotificationIconUri("mail-markread")) - .AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString()) - .AddArgument(Constants.ToastActionKey, MailOperation.MarkAsRead.ToString()) - .AddArgument(Constants.ToastModeKey, Constants.ToastModeMail); + return button; + } + + private static Uri? GetMailActionIconUri(MailOperation action) + => action switch + { + MailOperation.Archive => GetNotificationIconUri("mail-archive"), + MailOperation.SoftDelete => GetNotificationIconUri("mail-delete"), + MailOperation.MarkAsRead => GetNotificationIconUri("mail-markread"), + MailOperation.MoveToJunk => GetNotificationIconUri("mail-junk"), + MailOperation.Reply => GetNotificationIconUri("mail-reply"), + MailOperation.ReplyAll => GetNotificationIconUri("mail-replyall"), + MailOperation.Forward => GetNotificationIconUri("mail-forward"), + _ => null + }; + + private static AppNotificationButton CreateDismissButton() + => new AppNotificationButton(Translator.Buttons_Dismiss) + .SetIcon(GetNotificationIconUri("dismiss")) + .AddArgument(Constants.ToastDismissActionKey, bool.TrueString); private static AppNotificationBuilder CreateBuilder(AppNotificationScenario scenario = AppNotificationScenario.Default) => new AppNotificationBuilder().SetScenario(scenario); diff --git a/Wino.Mail.WinUI/Services/PreferencesService.cs b/Wino.Mail.WinUI/Services/PreferencesService.cs index cc1ed9bf..d7d3feb3 100644 --- a/Wino.Mail.WinUI/Services/PreferencesService.cs +++ b/Wino.Mail.WinUI/Services/PreferencesService.cs @@ -237,6 +237,18 @@ public class PreferencesService(IConfigurationService configurationService) : Ob set => SaveProperty(propertyName: nameof(StartupEntityId), value); } + public MailOperation FirstMailNotificationAction + { + get => _configurationService.Get(nameof(FirstMailNotificationAction), MailOperation.MarkAsRead); + set => SetPropertyAndSave(nameof(FirstMailNotificationAction), value); + } + + public MailOperation SecondMailNotificationAction + { + get => _configurationService.Get(nameof(SecondMailNotificationAction), MailOperation.SoftDelete); + set => SetPropertyAndSave(nameof(SecondMailNotificationAction), value); + } + public AppLanguage CurrentLanguage { get => _configurationService.Get(nameof(CurrentLanguage), TranslationService.DefaultAppLanguage); diff --git a/Wino.Mail.WinUI/Views/Abstract/MailNotificationSettingsPageAbstract.cs b/Wino.Mail.WinUI/Views/Abstract/MailNotificationSettingsPageAbstract.cs new file mode 100644 index 00000000..132714e3 --- /dev/null +++ b/Wino.Mail.WinUI/Views/Abstract/MailNotificationSettingsPageAbstract.cs @@ -0,0 +1,5 @@ +using Wino.Mail.ViewModels; + +namespace Wino.Views.Abstract; + +public abstract class MailNotificationSettingsPageAbstract : SettingsPageBase { } diff --git a/Wino.Mail.WinUI/Views/Settings/MailNotificationSettingsPage.xaml b/Wino.Mail.WinUI/Views/Settings/MailNotificationSettingsPage.xaml new file mode 100644 index 00000000..034d5582 --- /dev/null +++ b/Wino.Mail.WinUI/Views/Settings/MailNotificationSettingsPage.xaml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wino.Mail.WinUI/Views/Settings/MailNotificationSettingsPage.xaml.cs b/Wino.Mail.WinUI/Views/Settings/MailNotificationSettingsPage.xaml.cs new file mode 100644 index 00000000..bb160fe5 --- /dev/null +++ b/Wino.Mail.WinUI/Views/Settings/MailNotificationSettingsPage.xaml.cs @@ -0,0 +1,11 @@ +using Wino.Views.Abstract; + +namespace Wino.Views.Settings; + +public sealed partial class MailNotificationSettingsPage : MailNotificationSettingsPageAbstract +{ + public MailNotificationSettingsPage() + { + InitializeComponent(); + } +}