Add configurable mail notification actions

This commit is contained in:
Burak Kaan Köse
2026-04-15 15:43:07 +02:00
parent 1a1d69be56
commit 4ca26cb131
55 changed files with 410 additions and 20 deletions
+2
View File
@@ -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. - 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. - 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)`). - 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`. - 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(...)`. - 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. - 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.
+1
View File
@@ -24,6 +24,7 @@ public static class Constants
public const string ToastModeKey = nameof(ToastModeKey); public const string ToastModeKey = nameof(ToastModeKey);
public const string ToastModeMail = nameof(ToastModeMail); public const string ToastModeMail = nameof(ToastModeMail);
public const string ToastModeCalendar = nameof(ToastModeCalendar); public const string ToastModeCalendar = nameof(ToastModeCalendar);
public const string ToastDismissActionKey = nameof(ToastDismissActionKey);
public const string ToastStoreUpdateActionKey = nameof(ToastStoreUpdateActionKey); public const string ToastStoreUpdateActionKey = nameof(ToastStoreUpdateActionKey);
public const string ToastStoreUpdateActionInstall = nameof(ToastStoreUpdateActionInstall); public const string ToastStoreUpdateActionInstall = nameof(ToastStoreUpdateActionInstall);
public const string ClientLogFile = "Client_.log"; public const string ClientLogFile = "Client_.log";
+1
View File
@@ -19,6 +19,7 @@ public enum WinoPage
AboutPage, AboutPage,
PersonalizationPage, PersonalizationPage,
MessageListPage, MessageListPage,
MailNotificationSettingsPage,
MailListPage, MailListPage,
ReadComposePanePage, ReadComposePanePage,
AppPreferencesPage, AppPreferencesPage,
@@ -192,6 +192,16 @@ public interface IPreferencesService : INotifyPropertyChanged
/// </summary> /// </summary>
Guid? StartupEntityId { get; set; } Guid? StartupEntityId { get; set; }
/// <summary>
/// Setting: First action button displayed on mail toast notifications.
/// </summary>
MailOperation FirstMailNotificationAction { get; set; }
/// <summary>
/// Setting: Second action button displayed on mail toast notifications.
/// </summary>
MailOperation SecondMailNotificationAction { get; set; }
/// <summary> /// <summary>
@@ -68,6 +68,11 @@ public static class SettingsNavigationInfoProvider
Translator.SettingsMessageList_Description, Translator.SettingsMessageList_Description,
"\uE8C4", "\uE8C4",
searchKeywords: Translator.SettingsSearch_MessageList_Keywords), searchKeywords: Translator.SettingsSearch_MessageList_Keywords),
new(WinoPage.MailNotificationSettingsPage,
Translator.SettingsMailNotifications_Title,
Translator.SettingsMailNotifications_Description,
"\uE7F4",
searchKeywords: Translator.SettingsSearch_MailNotifications_Keywords),
new(WinoPage.ReadComposePanePage, new(WinoPage.ReadComposePanePage,
Translator.SettingsReadComposePane_Title, Translator.SettingsReadComposePane_Title,
Translator.SettingsReadComposePane_Description, Translator.SettingsReadComposePane_Description,
@@ -149,6 +154,7 @@ public static class SettingsNavigationInfoProvider
WinoPage.PersonalizationPage => Translator.SettingsPersonalization_Title, WinoPage.PersonalizationPage => Translator.SettingsPersonalization_Title,
WinoPage.AboutPage => Translator.SettingsAbout_Title, WinoPage.AboutPage => Translator.SettingsAbout_Title,
WinoPage.MessageListPage => Translator.SettingsMessageList_Title, WinoPage.MessageListPage => Translator.SettingsMessageList_Title,
WinoPage.MailNotificationSettingsPage => Translator.SettingsMailNotifications_Title,
WinoPage.ReadComposePanePage => Translator.SettingsReadComposePane_Title, WinoPage.ReadComposePanePage => Translator.SettingsReadComposePane_Title,
WinoPage.AppPreferencesPage => Translator.SettingsAppPreferences_Title, WinoPage.AppPreferencesPage => Translator.SettingsAppPreferences_Title,
WinoPage.CalendarSettingsPage => Translator.CalendarSettings_Preferences_Title, WinoPage.CalendarSettingsPage => Translator.CalendarSettings_Preferences_Title,
@@ -84,6 +84,7 @@
"Buttons_Delete": "Delete", "Buttons_Delete": "Delete",
"Buttons_Deny": "Deny", "Buttons_Deny": "Deny",
"Buttons_Discard": "Discard", "Buttons_Discard": "Discard",
"Buttons_Dismiss": "Dismiss",
"Buttons_Edit": "Edit", "Buttons_Edit": "Edit",
"Buttons_EnableImageRendering": "Enable", "Buttons_EnableImageRendering": "Enable",
"Buttons_Multiselect": "Select Multiple", "Buttons_Multiselect": "Select Multiple",
@@ -910,6 +911,14 @@
"SettingsMarkAsRead_WhenSelected": "When selected", "SettingsMarkAsRead_WhenSelected": "When selected",
"SettingsMessageList_Description": "Change how your messages should be organized in mail list.", "SettingsMessageList_Description": "Change how your messages should be organized in mail list.",
"SettingsMessageList_Title": "Message 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.", "SettingsNoAccountSetupMessage": "You didn't setup any accounts yet.",
"SettingsNotifications_Description": "Turn on or off notifications for this account.", "SettingsNotifications_Description": "Turn on or off notifications for this account.",
"SettingsNotifications_Title": "Notifications", "SettingsNotifications_Title": "Notifications",
@@ -946,6 +955,7 @@
"SettingsSearch_About_Keywords": "about;version;website;privacy;github;donate;store;support", "SettingsSearch_About_Keywords": "about;version;website;privacy;github;donate;store;support",
"SettingsSearch_KeyboardShortcuts_Keywords": "shortcut;shortcuts;hotkey;hotkeys;keyboard;keys", "SettingsSearch_KeyboardShortcuts_Keywords": "shortcut;shortcuts;hotkey;hotkeys;keyboard;keys",
"SettingsSearch_MessageList_Keywords": "message;messages;list;threading;threads;avatar;preview;sender", "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_ReadComposePane_Keywords": "reader;compose;composer;font;fonts;external content;display;reading",
"SettingsSearch_SignatureAndEncryption_Keywords": "signature;signatures;encryption;certificate;certificates;s mime;smime;security", "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", "SettingsSearch_Storage_Keywords": "storage;cache;caching;mime;disk;space;cleanup;clean up;local data",
@@ -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<MailNotificationActionOption> 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;
}
@@ -41,9 +41,14 @@ internal static class ToastActivationResolver
return calendarAction == Constants.ToastCalendarNavigateAction; return calendarAction == Constants.ToastCalendarNavigateAction;
} }
if (toastArguments.TryGetValue(Constants.ToastDismissActionKey, out string _))
{
return false;
}
if (toastArguments.TryGetValue(Constants.ToastActionKey, out MailOperation mailAction)) 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; return true;
@@ -52,5 +57,6 @@ internal static class ToastActivationResolver
private static bool ContainsKnownToastKey(NotificationArguments toastArguments) private static bool ContainsKnownToastKey(NotificationArguments toastArguments)
=> toastArguments.TryGetValue(Constants.ToastStoreUpdateActionKey, out string _) || => toastArguments.TryGetValue(Constants.ToastStoreUpdateActionKey, out string _) ||
toastArguments.TryGetValue(Constants.ToastCalendarActionKey, out string _) || toastArguments.TryGetValue(Constants.ToastCalendarActionKey, out string _) ||
toastArguments.TryGetValue(Constants.ToastDismissActionKey, out string _) ||
toastArguments.TryGetValue(Constants.ToastActionKey, out string _); toastArguments.TryGetValue(Constants.ToastActionKey, out string _);
} }
+92
View File
@@ -358,6 +358,7 @@ public partial class App : WinoApplication,
services.AddTransient(typeof(AccountDetailsPageViewModel)); services.AddTransient(typeof(AccountDetailsPageViewModel));
services.AddTransient(typeof(SignatureManagementPageViewModel)); services.AddTransient(typeof(SignatureManagementPageViewModel));
services.AddTransient(typeof(MessageListPageViewModel)); services.AddTransient(typeof(MessageListPageViewModel));
services.AddTransient(typeof(MailNotificationSettingsPageViewModel));
services.AddTransient(typeof(ReadComposePanePageViewModel)); services.AddTransient(typeof(ReadComposePanePageViewModel));
services.AddTransient(typeof(MergedAccountDetailsPageViewModel)); services.AddTransient(typeof(MergedAccountDetailsPageViewModel));
services.AddTransient(typeof(AppPreferencesPageViewModel)); services.AddTransient(typeof(AppPreferencesPageViewModel));
@@ -599,6 +600,12 @@ public partial class App : WinoApplication,
return; return;
} }
if (toastArguments.TryGetValue(Constants.ToastDismissActionKey, out string _))
{
LogActivation("Handling notification dismiss action.");
return;
}
// Check calendar reminder toast activation first. // Check calendar reminder toast activation first.
if (toastArguments.TryGetValue(Constants.ToastCalendarActionKey, out string calendarAction) && if (toastArguments.TryGetValue(Constants.ToastCalendarActionKey, out string calendarAction) &&
toastArguments.TryGetValue(Constants.ToastCalendarItemIdKey, out string calendarItemIdString) && 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. // User clicked notification - create window if needed and navigate.
await HandleToastNavigationAsync(mailItemUniqueId); await HandleToastNavigationAsync(mailItemUniqueId);
} }
else if (IsComposeToastAction(action))
{
await HandleToastComposeActionAsync(action, mailItemUniqueId);
}
else else
{ {
// User clicked action button (Mark as Read, Delete, etc.) // 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<IMailService>();
var folderService = Services.GetRequiredService<IFolderService>();
var mimeFileService = Services.GetRequiredService<IMimeFileService>();
var navigationService = Services.GetRequiredService<INavigationService>();
var requestDelegator = Services.GetRequiredService<IWinoRequestDelegator>();
var mailShellViewModel = Services.GetRequiredService<MailAppShellViewModel>();
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;
/// <summary> /// <summary>
/// Creates the main window and activates it. /// Creates the main window and activates it.
/// </summary> /// </summary>
Binary file not shown.

After

Width:  |  Height:  |  Size: 426 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 608 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 776 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 759 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 767 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 691 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 697 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

@@ -79,6 +79,7 @@ public class NavigationService : NavigationServiceBase, INavigationService
WinoPage.AboutPage, WinoPage.AboutPage,
WinoPage.PersonalizationPage, WinoPage.PersonalizationPage,
WinoPage.MessageListPage, WinoPage.MessageListPage,
WinoPage.MailNotificationSettingsPage,
WinoPage.ReadComposePanePage, WinoPage.ReadComposePanePage,
WinoPage.AppPreferencesPage, WinoPage.AppPreferencesPage,
WinoPage.AliasManagementPage, WinoPage.AliasManagementPage,
@@ -142,6 +143,7 @@ public class NavigationService : NavigationServiceBase, INavigationService
WinoPage.AboutPage => typeof(AboutPage), WinoPage.AboutPage => typeof(AboutPage),
WinoPage.PersonalizationPage => typeof(PersonalizationPage), WinoPage.PersonalizationPage => typeof(PersonalizationPage),
WinoPage.MessageListPage => typeof(MessageListPage), WinoPage.MessageListPage => typeof(MessageListPage),
WinoPage.MailNotificationSettingsPage => typeof(MailNotificationSettingsPage),
WinoPage.ReadComposePanePage => typeof(ReadComposePanePage), WinoPage.ReadComposePanePage => typeof(ReadComposePanePage),
WinoPage.MailRenderingPage => typeof(MailRenderingPage), WinoPage.MailRenderingPage => typeof(MailRenderingPage),
WinoPage.ComposePage => typeof(ComposePage), WinoPage.ComposePage => typeof(ComposePage),
+67 -19
View File
@@ -17,6 +17,7 @@ using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Extensions; using Wino.Core.Domain.Extensions;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Helpers;
using Wino.Mail.WinUI.Activation; using Wino.Mail.WinUI.Activation;
using Wino.Messaging.UI; using Wino.Messaging.UI;
@@ -26,6 +27,16 @@ public class NotificationBuilder : INotificationBuilder
{ {
private const string NotificationIconRootUri = "ms-appx:///Assets/NotificationIcons/"; private const string NotificationIconRootUri = "ms-appx:///Assets/NotificationIcons/";
private static int _calendarTaskbarBadgeCount; 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 IAccountService _accountService;
private readonly IFolderService _folderService; private readonly IFolderService _folderService;
@@ -76,6 +87,7 @@ public class NotificationBuilder : INotificationBuilder
builder.AddText(Translator.Notifications_MultipleNotificationsTitle); builder.AddText(Translator.Notifications_MultipleNotificationsTitle);
builder.AddText(string.Format(Translator.Notifications_MultipleNotificationsMessage, mailCount)); builder.AddText(string.Format(Translator.Notifications_MultipleNotificationsMessage, mailCount));
builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail); builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
builder.AddButton(CreateDismissButton());
builder.SetAudioUri(new Uri("ms-winsoundevent:Notification.Mail")); builder.SetAudioUri(new Uri("ms-winsoundevent:Notification.Mail"));
ShowNotification(builder); ShowNotification(builder);
@@ -167,6 +179,7 @@ public class NotificationBuilder : INotificationBuilder
builder.AddButton(new AppNotificationButton(Translator.Buttons_FixAccount) builder.AddButton(new AppNotificationButton(Translator.Buttons_FixAccount)
.AddArgument(Constants.ToastMailAccountIdKey, account.Id.ToString()) .AddArgument(Constants.ToastMailAccountIdKey, account.Id.ToString())
.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail)); .AddArgument(Constants.ToastModeKey, Constants.ToastModeMail));
builder.AddButton(CreateDismissButton());
ShowNotification(builder); ShowNotification(builder);
} }
@@ -177,6 +190,7 @@ public class NotificationBuilder : INotificationBuilder
builder.AddText(Translator.Exception_WebView2RuntimeMissing_Title); builder.AddText(Translator.Exception_WebView2RuntimeMissing_Title);
builder.AddText(Translator.Exception_WebView2RuntimeMissing_Message); builder.AddText(Translator.Exception_WebView2RuntimeMissing_Message);
builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail); builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
builder.AddButton(CreateDismissButton());
ShowNotification(builder); ShowNotification(builder);
} }
@@ -188,6 +202,7 @@ public class NotificationBuilder : INotificationBuilder
builder.AddText(Translator.Notifications_StoreUpdateAvailableMessage); builder.AddText(Translator.Notifications_StoreUpdateAvailableMessage);
builder.AddArgument(Constants.ToastStoreUpdateActionKey, Constants.ToastStoreUpdateActionInstall); builder.AddArgument(Constants.ToastStoreUpdateActionKey, Constants.ToastStoreUpdateActionInstall);
builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail); builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
builder.AddButton(CreateDismissButton());
ShowNotification(builder, "store-update-available"); ShowNotification(builder, "store-update-available");
} }
@@ -255,6 +270,8 @@ public class NotificationBuilder : INotificationBuilder
.AddArgument(Constants.ToastModeKey, Constants.ToastModeCalendar)); .AddArgument(Constants.ToastModeKey, Constants.ToastModeCalendar));
} }
builder.AddButton(CreateDismissButton());
var tag = $"calendar-reminder-{calendarItem.Id:N}-{reminderDurationInSeconds}"; var tag = $"calendar-reminder-{calendarItem.Id:N}-{reminderDurationInSeconds}";
ShowNotification(builder, tag); ShowNotification(builder, tag);
@@ -288,9 +305,11 @@ public class NotificationBuilder : INotificationBuilder
builder.AddArgument(Constants.ToastMailUniqueIdKey, mailItem.UniqueId.ToString()); builder.AddArgument(Constants.ToastMailUniqueIdKey, mailItem.UniqueId.ToString());
builder.AddArgument(Constants.ToastActionKey, MailOperation.Navigate.ToString()); builder.AddArgument(Constants.ToastActionKey, MailOperation.Navigate.ToString());
builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail); builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
builder.AddButton(GetMarkAsReadButton(mailItem.UniqueId));
builder.AddButton(GetDeleteButton(mailItem.UniqueId)); var (firstAction, secondAction) = GetConfiguredMailNotificationActions();
builder.AddButton(GetArchiveButton(mailItem.UniqueId)); builder.AddButton(CreateMailNotificationActionButton(firstAction, mailItem.UniqueId));
builder.AddButton(CreateMailNotificationActionButton(secondAction, mailItem.UniqueId));
builder.AddButton(CreateDismissButton());
builder.SetAudioUri(new Uri("ms-winsoundevent:Notification.Mail")); builder.SetAudioUri(new Uri("ms-winsoundevent:Notification.Mail"));
ShowNotification(builder, mailItem.UniqueId.ToString()); ShowNotification(builder, mailItem.UniqueId.ToString());
@@ -347,26 +366,55 @@ public class NotificationBuilder : INotificationBuilder
return string.Format(Translator.CalendarReminder_StartedMinutesAgo, minutesAgo); return string.Format(Translator.CalendarReminder_StartedMinutesAgo, minutesAgo);
} }
private AppNotificationButton GetArchiveButton(Guid mailUniqueId) private (MailOperation FirstAction, MailOperation SecondAction) GetConfiguredMailNotificationActions()
=> new AppNotificationButton(Translator.MailOperation_Archive) {
.SetIcon(GetNotificationIconUri("mail-archive")) 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.ToastMailUniqueIdKey, mailUniqueId.ToString())
.AddArgument(Constants.ToastActionKey, MailOperation.Archive.ToString()) .AddArgument(Constants.ToastActionKey, action.ToString())
.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail); .AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
private AppNotificationButton GetDeleteButton(Guid mailUniqueId) var iconUri = GetMailActionIconUri(action);
=> new AppNotificationButton(Translator.MailOperation_Delete) if (iconUri != null)
.SetIcon(GetNotificationIconUri("mail-delete")) {
.AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString()) button.SetIcon(iconUri);
.AddArgument(Constants.ToastActionKey, MailOperation.SoftDelete.ToString()) }
.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
private AppNotificationButton GetMarkAsReadButton(Guid mailUniqueId) return button;
=> new AppNotificationButton(Translator.MailOperation_MarkAsRead) }
.SetIcon(GetNotificationIconUri("mail-markread"))
.AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString()) private static Uri? GetMailActionIconUri(MailOperation action)
.AddArgument(Constants.ToastActionKey, MailOperation.MarkAsRead.ToString()) => action switch
.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail); {
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) private static AppNotificationBuilder CreateBuilder(AppNotificationScenario scenario = AppNotificationScenario.Default)
=> new AppNotificationBuilder().SetScenario(scenario); => new AppNotificationBuilder().SetScenario(scenario);
@@ -237,6 +237,18 @@ public class PreferencesService(IConfigurationService configurationService) : Ob
set => SaveProperty(propertyName: nameof(StartupEntityId), value); 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 public AppLanguage CurrentLanguage
{ {
get => _configurationService.Get(nameof(CurrentLanguage), TranslationService.DefaultAppLanguage); get => _configurationService.Get(nameof(CurrentLanguage), TranslationService.DefaultAppLanguage);
@@ -0,0 +1,5 @@
using Wino.Mail.ViewModels;
namespace Wino.Views.Abstract;
public abstract class MailNotificationSettingsPageAbstract : SettingsPageBase<MailNotificationSettingsPageViewModel> { }
@@ -0,0 +1,49 @@
<abstract:MailNotificationSettingsPageAbstract
x:Class="Wino.Views.Settings.MailNotificationSettingsPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:abstract="using:Wino.Views.Abstract"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:domain="using:Wino.Core.Domain"
xmlns:mailViewModels="using:Wino.Mail.ViewModels"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<ScrollViewer>
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<controls:SettingsExpander
Description="{x:Bind domain:Translator.SettingsMailNotifications_Actions_Description}"
Header="{x:Bind domain:Translator.SettingsMailNotifications_Actions_Title}"
IsExpanded="True">
<controls:SettingsExpander.HeaderIcon>
<PathIcon Data="F1 M 6.347656 16.25 L 3.701172 16.25 C 3.375651 16.25 3.064779 16.18327 2.768555 16.049805 C 2.472331 15.916342 2.211914 15.737305 1.987305 15.512695 C 1.762695 15.288086 1.583659 15.02767 1.450195 14.731445 C 1.316732 14.435222 1.25 14.12435 1.25 13.798828 L 1.25 3.701172 C 1.25 3.375652 1.316732 3.064779 1.450195 2.768555 C 1.583659 2.472332 1.762695 2.211914 1.987305 1.987305 C 2.211914 1.762695 2.472331 1.58366 2.768555 1.450195 C 3.064779 1.316732 3.375651 1.25 3.701172 1.25 L 16.298828 1.25 C 16.624348 1.25 16.935221 1.316732 17.231445 1.450195 C 17.527668 1.58366 17.788086 1.762695 18.012695 1.987305 C 18.237305 2.211914 18.41634 2.472332 18.549805 2.768555 C 18.683268 3.064779 18.75 3.375652 18.75 3.701172 L 18.75 13.798828 C 18.75 14.13737 18.681641 14.454753 18.544922 14.750977 C 18.408203 15.047201 18.22591 15.30599 17.998047 15.527344 C 17.770182 15.748698 17.504883 15.924479 17.202148 16.054688 C 16.899414 16.184896 16.582031 16.25 16.25 16.25 L 13.652344 16.25 L 10.46875 19.794922 C 10.345052 19.931641 10.188802 20 10 20 C 9.811197 20 9.654947 19.931641 9.53125 19.794922 Z M 15.625 7.5 C 15.79427 7.5 15.940754 7.438151 16.064453 7.314453 C 16.18815 7.190756 16.25 7.044271 16.25 6.875 C 16.25 6.705729 16.18815 6.559245 16.064453 6.435547 C 15.940754 6.31185 15.79427 6.25 15.625 6.25 L 4.375 6.25 C 4.205729 6.25 4.059245 6.31185 3.935547 6.435547 C 3.811849 6.559245 3.75 6.705729 3.75 6.875 C 3.75 7.044271 3.811849 7.190756 3.935547 7.314453 C 4.059245 7.438151 4.205729 7.5 4.375 7.5 Z M 15.625 11.25 C 15.79427 11.25 15.940754 11.188151 16.064453 11.064453 C 16.18815 10.940756 16.25 10.794271 16.25 10.625 C 16.25 10.455729 16.18815 10.309245 16.064453 10.185547 C 15.940754 10.06185 15.79427 10 15.625 10 L 4.375 10 C 4.205729 10 4.059245 10.06185 3.935547 10.185547 C 3.811849 10.309245 3.75 10.455729 3.75 10.625 C 3.75 10.794271 3.811849 10.940756 3.935547 11.064453 C 4.059245 11.188151 4.205729 11.25 4.375 11.25 Z " />
</controls:SettingsExpander.HeaderIcon>
<controls:SettingsExpander.Items>
<controls:SettingsCard Description="{x:Bind domain:Translator.SettingsMailNotifications_FirstAction_Description}" Header="{x:Bind domain:Translator.SettingsMailNotifications_FirstAction_Title}">
<ComboBox ItemsSource="{x:Bind ViewModel.AvailableNotificationActions, Mode=OneWay}" SelectedItem="{x:Bind ViewModel.SelectedFirstAction, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="mailViewModels:MailNotificationActionOption">
<TextBlock Text="{x:Bind DisplayText}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</controls:SettingsCard>
<controls:SettingsCard Description="{x:Bind domain:Translator.SettingsMailNotifications_SecondAction_Description}" Header="{x:Bind domain:Translator.SettingsMailNotifications_SecondAction_Title}">
<ComboBox ItemsSource="{x:Bind ViewModel.AvailableNotificationActions, Mode=OneWay}" SelectedItem="{x:Bind ViewModel.SelectedSecondAction, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="mailViewModels:MailNotificationActionOption">
<TextBlock Text="{x:Bind DisplayText}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
</StackPanel>
</ScrollViewer>
</abstract:MailNotificationSettingsPageAbstract>
@@ -0,0 +1,11 @@
using Wino.Views.Abstract;
namespace Wino.Views.Settings;
public sealed partial class MailNotificationSettingsPage : MailNotificationSettingsPageAbstract
{
public MailNotificationSettingsPage()
{
InitializeComponent();
}
}