diff --git a/AGENTS.md b/AGENTS.md index a3d40d5f..bfef1ef7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -130,5 +130,6 @@ private string searchQuery = string.Empty; - Wrap async operations in try-catch - Log errors via IWinoLogger - In ViewModels, update all UI-bound properties/collections via `ExecuteUIThread(...)` (especially after awaited calls and any use of `ConfigureAwait(false)`). +- In `EventDetailsPageViewModel.LoadAttendeesAsync`, never mutate `CurrentEvent.Attendees` outside `ExecuteUIThread(...)`. diff --git a/Wino.Calendar.ViewModels/CalendarSettingsPageViewModel.cs b/Wino.Calendar.ViewModels/CalendarSettingsPageViewModel.cs index 801b67ed..d03f2ad4 100644 --- a/Wino.Calendar.ViewModels/CalendarSettingsPageViewModel.cs +++ b/Wino.Calendar.ViewModels/CalendarSettingsPageViewModel.cs @@ -50,6 +50,12 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel [ObservableProperty] public partial int SelectedDefaultReminderIndex { get; set; } + [ObservableProperty] + public partial List SnoozeOptions { get; set; } = []; + + [ObservableProperty] + public partial int SelectedDefaultSnoozeIndex { get; set; } + public IPreferencesService PreferencesService { get; } private readonly ICalendarService _calendarService; private readonly IAccountService _accountService; @@ -108,6 +114,15 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel SelectedDefaultReminderIndex = index >= 0 ? index + 1 : 0; } + var supportedSnoozeMinutes = CalendarReminderSnoozeOptions.GetSupportedSnoozeMinutes().ToArray(); + foreach (var snoozeMinutes in supportedSnoozeMinutes) + { + SnoozeOptions.Add(string.Format(Translator.CalendarReminder_SnoozeMinutesOption, snoozeMinutes)); + } + + var selectedSnoozeIndex = Array.IndexOf(supportedSnoozeMinutes, preferencesService.DefaultSnoozeDurationInMinutes); + SelectedDefaultSnoozeIndex = selectedSnoozeIndex >= 0 ? selectedSnoozeIndex : 0; + _isLoaded = true; // Load accounts with calendar support @@ -147,6 +162,7 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel partial void OnWorkingDayStartIndexChanged(int value) => SaveSettings(); partial void OnWorkingDayEndIndexChanged(int value) => SaveSettings(); partial void OnSelectedDefaultReminderIndexChanged(int value) => SaveSettings(); + partial void OnSelectedDefaultSnoozeIndexChanged(int value) => SaveSettings(); public void SaveSettings() { @@ -205,6 +221,13 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel PreferencesService.DefaultReminderDurationInSeconds = minutes * 60; } + var supportedSnoozeMinutes = CalendarReminderSnoozeOptions.GetSupportedSnoozeMinutes(); + if (supportedSnoozeMinutes.Count > 0) + { + var selectedIndex = Math.Clamp(SelectedDefaultSnoozeIndex, 0, supportedSnoozeMinutes.Count - 1); + PreferencesService.DefaultSnoozeDurationInMinutes = supportedSnoozeMinutes[selectedIndex]; + } + Messenger.Send(new CalendarSettingsUpdatedMessage()); } } diff --git a/Wino.Calendar.ViewModels/EventDetailsPageViewModel.cs b/Wino.Calendar.ViewModels/EventDetailsPageViewModel.cs index cac9288f..f2064610 100644 --- a/Wino.Calendar.ViewModels/EventDetailsPageViewModel.cs +++ b/Wino.Calendar.ViewModels/EventDetailsPageViewModel.cs @@ -31,6 +31,7 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel private readonly IWinoRequestDelegator _winoRequestDelegator; private readonly INavigationService _navigationService; private readonly IUnderlyingThemeService _underlyingThemeService; + private readonly INotificationBuilder _notificationBuilder; private readonly IContactService _contactService; public CalendarSettings CurrentSettings { get; } @@ -144,6 +145,7 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel IMailDialogService dialogService, IWinoRequestDelegator winoRequestDelegator, INavigationService navigationService, + INotificationBuilder notificationBuilder, IUnderlyingThemeService underlyingThemeService, IContactService contactService) { @@ -154,6 +156,7 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel _winoRequestDelegator = winoRequestDelegator; _navigationService = navigationService; _underlyingThemeService = underlyingThemeService; + _notificationBuilder = notificationBuilder; _contactService = contactService; CurrentSettings = _preferencesService.GetCurrentCalendarSettings(); @@ -259,8 +262,6 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel private async Task LoadAttendeesAsync(Guid calendarItemId, CalendarItem calendarItem) { - CurrentEvent.Attendees.Clear(); - var attendees = await _calendarService.GetAttendeesAsync(calendarItemId); // Resolve contacts for all attendees in a single batch DB query. @@ -285,10 +286,12 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel var organizer = attendees.FirstOrDefault(a => a.IsOrganizer); var nonOrganizerAttendees = attendees.Where(a => !a.IsOrganizer).ToList(); + var attendeesForUi = new List(); + // If the organizer is in the list, add them first if (organizer != null) { - CurrentEvent.Attendees.Add(organizer); + attendeesForUi.Add(organizer); } else if (!string.IsNullOrEmpty(calendarItem.OrganizerEmail)) { @@ -306,14 +309,27 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel if (contactLookup.TryGetValue(calendarItem.OrganizerEmail, out var organizerContact)) organizerAttendee.ResolvedContact = organizerContact; - CurrentEvent.Attendees.Add(organizerAttendee); + attendeesForUi.Add(organizerAttendee); } // Add all other attendees after the organizer foreach (var item in nonOrganizerAttendees) { - CurrentEvent.Attendees.Add(item); + attendeesForUi.Add(item); } + + await ExecuteUIThread(() => + { + if (CurrentEvent == null) + return; + + CurrentEvent.Attendees.Clear(); + + foreach (var attendee in attendeesForUi) + { + CurrentEvent.Attendees.Add(attendee); + } + }); } private async Task LoadAttachmentsAsync(Guid calendarItemId) @@ -491,6 +507,24 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel return _nativeAppService.LaunchUriAsync(new Uri(CurrentEvent.CalendarItem.HtmlLink)); } + [RelayCommand] + private Task CreateTestNotificationAsync() + { + if (CurrentEvent?.CalendarItem == null) + return Task.CompletedTask; + + var reminderDurationInSeconds = Reminders? + .Where(x => x.DurationInSeconds > 0) + .OrderByDescending(x => x.DurationInSeconds) + .Select(x => x.DurationInSeconds) + .FirstOrDefault() ?? 0; + + if (reminderDurationInSeconds <= 0) + reminderDurationInSeconds = Math.Max(_preferencesService.DefaultReminderDurationInSeconds, 30 * 60); + + return _notificationBuilder.CreateCalendarReminderNotificationAsync(CurrentEvent.CalendarItem, reminderDurationInSeconds); + } + [RelayCommand] private void ToggleRsvpPanel() { diff --git a/Wino.Core.Domain/CalendarReminderSnoozeOptions.cs b/Wino.Core.Domain/CalendarReminderSnoozeOptions.cs new file mode 100644 index 00000000..5786c42f --- /dev/null +++ b/Wino.Core.Domain/CalendarReminderSnoozeOptions.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Wino.Core.Domain; + +public static class CalendarReminderSnoozeOptions +{ + private static readonly int[] SupportedSnoozeMinutes = [5, 10, 15, 30]; + + public static IReadOnlyList GetSupportedSnoozeMinutes() + => SupportedSnoozeMinutes; + + public static IReadOnlyList GetAllowedSnoozeMinutes(long reminderDurationInSeconds, long defaultReminderDurationInSeconds) + { + var reminderMinutes = (int)Math.Max(0, reminderDurationInSeconds / 60); + + if (reminderMinutes <= 0) + return []; + + var maxSnoozeMinutes = reminderMinutes; + var defaultReminderMinutes = (int)Math.Max(0, defaultReminderDurationInSeconds / 60); + + if (defaultReminderMinutes > 0) + maxSnoozeMinutes = Math.Min(maxSnoozeMinutes, defaultReminderMinutes); + + return SupportedSnoozeMinutes.Where(minutes => minutes <= maxSnoozeMinutes).ToArray(); + } +} diff --git a/Wino.Core.Domain/Constants.cs b/Wino.Core.Domain/Constants.cs index 6c57207b..6165b628 100644 --- a/Wino.Core.Domain/Constants.cs +++ b/Wino.Core.Domain/Constants.cs @@ -16,6 +16,8 @@ public static class Constants public const string ToastCalendarItemIdKey = nameof(ToastCalendarItemIdKey); public const string ToastCalendarActionKey = nameof(ToastCalendarActionKey); public const string ToastCalendarNavigateAction = nameof(ToastCalendarNavigateAction); + public const string ToastCalendarSnoozeAction = nameof(ToastCalendarSnoozeAction); + public const string ToastCalendarSnoozeDurationInputId = nameof(ToastCalendarSnoozeDurationInputId); public const string ToastModeKey = nameof(ToastModeKey); public const string ToastModeMail = nameof(ToastModeMail); public const string ToastModeCalendar = nameof(ToastModeCalendar); diff --git a/Wino.Core.Domain/Entities/Calendar/CalendarItem.cs b/Wino.Core.Domain/Entities/Calendar/CalendarItem.cs index 86d6235a..a04576f9 100644 --- a/Wino.Core.Domain/Entities/Calendar/CalendarItem.cs +++ b/Wino.Core.Domain/Entities/Calendar/CalendarItem.cs @@ -138,6 +138,7 @@ public class CalendarItem : ICalendarItem // TODO public string CustomEventColorHex { get; set; } public string HtmlLink { get; set; } + public DateTime? SnoozedUntil { get; set; } public CalendarItemStatus Status { get; set; } public CalendarItemVisibility Visibility { get; set; } diff --git a/Wino.Core.Domain/Interfaces/ICalendarService.cs b/Wino.Core.Domain/Interfaces/ICalendarService.cs index 6e2ba3c1..d41f6dc0 100644 --- a/Wino.Core.Domain/Interfaces/ICalendarService.cs +++ b/Wino.Core.Domain/Interfaces/ICalendarService.cs @@ -42,6 +42,7 @@ public interface ICalendarService Task UpdateCalendarItemAsync(CalendarItem calendarItem, List attendees); Task> GetRemindersAsync(Guid calendarItemId); Task SaveRemindersAsync(Guid calendarItemId, List reminders); + Task SnoozeCalendarItemAsync(Guid calendarItemId, DateTime snoozedUntilLocal); /// /// Checks due reminder windows and returns reminder notifications that should trigger now. diff --git a/Wino.Core.Domain/Interfaces/IPreferencesService.cs b/Wino.Core.Domain/Interfaces/IPreferencesService.cs index a207136c..8763b686 100644 --- a/Wino.Core.Domain/Interfaces/IPreferencesService.cs +++ b/Wino.Core.Domain/Interfaces/IPreferencesService.cs @@ -222,6 +222,11 @@ public interface IPreferencesService : INotifyPropertyChanged /// long DefaultReminderDurationInSeconds { get; set; } + /// + /// Setting: Default snooze duration in minutes for calendar reminder notifications. + /// + int DefaultSnoozeDurationInMinutes { get; set; } + CalendarSettings GetCurrentCalendarSettings(); #endregion diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json index ba2f5ee1..36074c3a 100644 --- a/Wino.Core.Domain/Translations/en_US/resources.json +++ b/Wino.Core.Domain/Translations/en_US/resources.json @@ -133,6 +133,14 @@ "CalendarEventDetails_People": "People", "CalendarEventDetails_ReadOnlyEvent": "Read-only event", "CalendarEventDetails_Reminder": "Reminder", + "CalendarReminder_StartedHoursAgo": "Started {0} hours ago", + "CalendarReminder_StartedMinutesAgo": "Started {0} minutes ago", + "CalendarReminder_StartedNow": "Started just now", + "CalendarReminder_StartingNow": "Starting now", + "CalendarReminder_StartsInHours": "Starts in {0} hours", + "CalendarReminder_StartsInMinutes": "Starts in {0} minutes", + "CalendarReminder_SnoozeAction": "Snooze", + "CalendarReminder_SnoozeMinutesOption": "{0} minutes", "CalendarEventDetails_ShowAs": "Show as", "CalendarShowAs_Free": "Free", "CalendarShowAs_Tentative": "Tentative", @@ -646,6 +654,8 @@ "SettingsAvailableThemes_Title": "Available Themes", "SettingsCalendarSettings_Description": "Change first day of week, hour cell height and more...", "SettingsCalendarSettings_Title": "Calendar Settings", + "CalendarSettings_DefaultSnoozeDuration_Header": "Default snooze duration", + "CalendarSettings_DefaultSnoozeDuration_Description": "Set a default snooze duration for calendar reminder notifications.", "SettingsComposer_Title": "Composer", "SettingsComposerFont_Title": "Default Composer Font", "SettingsComposerFontFamily_Description": "Change the default font family and font size for composing mails.", diff --git a/Wino.Core.Tests/CalendarReminderSnoozeOptionsTests.cs b/Wino.Core.Tests/CalendarReminderSnoozeOptionsTests.cs new file mode 100644 index 00000000..561e3024 --- /dev/null +++ b/Wino.Core.Tests/CalendarReminderSnoozeOptionsTests.cs @@ -0,0 +1,48 @@ +using FluentAssertions; +using Wino.Core.Domain; +using Xunit; + +namespace Wino.Core.Tests; + +public class CalendarReminderSnoozeOptionsTests +{ + [Fact] + public void GetAllowedSnoozeMinutes_WhenDefaultIs15AndReminderIs15_Excludes30() + { + var options = CalendarReminderSnoozeOptions.GetAllowedSnoozeMinutes( + reminderDurationInSeconds: 15 * 60, + defaultReminderDurationInSeconds: 15 * 60); + + options.Should().Equal(5, 10, 15); + } + + [Fact] + public void GetAllowedSnoozeMinutes_WhenReminderIs5AndDefaultIs15_DoesNotPassEventStart() + { + var options = CalendarReminderSnoozeOptions.GetAllowedSnoozeMinutes( + reminderDurationInSeconds: 5 * 60, + defaultReminderDurationInSeconds: 15 * 60); + + options.Should().Equal(5); + } + + [Fact] + public void GetAllowedSnoozeMinutes_WhenDefaultReminderIsNone_UsesReminderDurationOnly() + { + var options = CalendarReminderSnoozeOptions.GetAllowedSnoozeMinutes( + reminderDurationInSeconds: 30 * 60, + defaultReminderDurationInSeconds: 0); + + options.Should().Equal(5, 10, 15, 30); + } + + [Fact] + public void GetAllowedSnoozeMinutes_WhenReminderIsUnderFiveMinutes_ReturnsNoOptions() + { + var options = CalendarReminderSnoozeOptions.GetAllowedSnoozeMinutes( + reminderDurationInSeconds: 60, + defaultReminderDurationInSeconds: 15 * 60); + + options.Should().BeEmpty(); + } +} diff --git a/Wino.Core.Tests/Services/CalendarReminderServiceTests.cs b/Wino.Core.Tests/Services/CalendarReminderServiceTests.cs index 00261663..23e9b62b 100644 --- a/Wino.Core.Tests/Services/CalendarReminderServiceTests.cs +++ b/Wino.Core.Tests/Services/CalendarReminderServiceTests.cs @@ -68,8 +68,8 @@ public class CalendarReminderServiceTests : IAsyncLifetime due.Should().HaveCount(1); due[0].CalendarItem.Id.Should().Be(calendarItem.Id); due[0].ReminderDurationInSeconds.Should().Be(5 * 60); - due[0].ReminderKey.Should().Be($"{calendarItem.Id:N}:{5 * 60}"); - sentReminderKeys.Should().Contain($"{calendarItem.Id:N}:{5 * 60}"); + due[0].ReminderKey.Should().StartWith($"{calendarItem.Id:N}:{5 * 60}:"); + sentReminderKeys.Should().ContainSingle(k => k.StartsWith($"{calendarItem.Id:N}:{5 * 60}:")); } [Fact] @@ -108,7 +108,7 @@ public class CalendarReminderServiceTests : IAsyncLifetime firstRun.Should().HaveCount(1); secondRun.Should().BeEmpty(); - sentReminderKeys.Should().Contain($"{calendarItem.Id:N}:{5 * 60}"); + sentReminderKeys.Should().ContainSingle(k => k.StartsWith($"{calendarItem.Id:N}:{5 * 60}:")); } [Fact] @@ -189,6 +189,35 @@ public class CalendarReminderServiceTests : IAsyncLifetime due.Should().BeEmpty(); } + + [Fact] + public async Task CheckAndNotifyAsync_WhenItemIsSnoozed_TriggersAtSnoozedTime() + { + var nowLocal = new DateTime(2026, 1, 1, 10, 0, 0); + var lastCheckLocal = nowLocal.AddSeconds(-30); + + var calendarItem = await CreateCalendarItemWithReminderAsync( + startDate: nowLocal.AddMinutes(5), + reminderDurationInSeconds: 5 * 60, + reminderType: CalendarItemReminderType.Popup); + + await _calendarService.SnoozeCalendarItemAsync(calendarItem.Id, nowLocal.AddMinutes(10)); + + HashSet sentReminderKeys = []; + + var dueAtOriginalTrigger = await _calendarService.CheckAndNotifyAsync(lastCheckLocal, nowLocal, sentReminderKeys); + dueAtOriginalTrigger.Should().BeEmpty(); + + var snoozeTriggerWindowStart = nowLocal.AddMinutes(10).AddSeconds(-30); + var snoozeTriggerWindowEnd = nowLocal.AddMinutes(10); + + var dueAtSnoozeTime = await _calendarService.CheckAndNotifyAsync(snoozeTriggerWindowStart, snoozeTriggerWindowEnd, sentReminderKeys); + + dueAtSnoozeTime.Should().HaveCount(1); + dueAtSnoozeTime[0].CalendarItem.Id.Should().Be(calendarItem.Id); + dueAtSnoozeTime[0].ReminderKey.Should().StartWith($"{calendarItem.Id:N}:{5 * 60}:"); + } + private async Task CreateCalendarItemWithReminderAsync( DateTime startDate, long reminderDurationInSeconds, diff --git a/Wino.Mail.WinUI/App.xaml.cs b/Wino.Mail.WinUI/App.xaml.cs index 161a5fcc..10a42c14 100644 --- a/Wino.Mail.WinUI/App.xaml.cs +++ b/Wino.Mail.WinUI/App.xaml.cs @@ -192,6 +192,8 @@ public partial class App : WinoApplication, private void AppNotificationInvoked(AppNotificationManager sender, AppNotificationActivatedEventArgs args) { + // AppNotification callbacks are not guaranteed to run on the UI thread. + // Marshal toast handling to the window dispatcher before touching window APIs. if (MainWindow?.DispatcherQueue?.TryEnqueue(() => _ = HandleToastActivationAsync(args)) == true) return; @@ -224,12 +226,20 @@ public partial class App : WinoApplication, // Check calendar reminder toast activation first. if (toastArguments.TryGetValue(Constants.ToastCalendarActionKey, out string calendarAction) && - calendarAction == Constants.ToastCalendarNavigateAction && toastArguments.TryGetValue(Constants.ToastCalendarItemIdKey, out string calendarItemIdString) && Guid.TryParse(calendarItemIdString, out Guid calendarItemId)) { - await HandleCalendarToastNavigationAsync(calendarItemId); - return; + if (calendarAction == Constants.ToastCalendarNavigateAction) + { + await HandleCalendarToastNavigationAsync(calendarItemId); + return; + } + + if (calendarAction == Constants.ToastCalendarSnoozeAction) + { + await HandleCalendarToastSnoozeAsync(toastArgs, calendarItemId); + return; + } } // Check if this is a navigation toast (user clicked the notification). @@ -275,6 +285,33 @@ public partial class App : WinoApplication, navigationService.Navigate(WinoPage.EventDetailsPage, target); } + private async Task HandleCalendarToastSnoozeAsync(AppNotificationActivatedEventArgs toastArgs, Guid calendarItemId) + { + if (!TryGetSnoozeDurationMinutes(toastArgs, out var snoozeDurationMinutes)) + return; + + var calendarService = Services.GetRequiredService(); + var snoozedUntilLocal = DateTime.Now.AddMinutes(snoozeDurationMinutes); + + await calendarService.SnoozeCalendarItemAsync(calendarItemId, snoozedUntilLocal).ConfigureAwait(false); + } + + private static bool TryGetSnoozeDurationMinutes(AppNotificationActivatedEventArgs toastArgs, out int snoozeDurationMinutes) + { + snoozeDurationMinutes = 0; + + if (toastArgs.UserInput == null || + !toastArgs.UserInput.TryGetValue(Constants.ToastCalendarSnoozeDurationInputId, out var selectedValue) || + selectedValue == null) + { + return false; + } + + var selectedText = selectedValue.ToString(); + + return int.TryParse(selectedText, out snoozeDurationMinutes) && snoozeDurationMinutes > 0; + } + /// /// Handles toast notification click for navigation. /// Creates window if not running, sets up navigation parameter. diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/archive.png b/Wino.Mail.WinUI/Assets/NotificationIcons/archive.png deleted file mode 100644 index 40dc3dd1..00000000 Binary files a/Wino.Mail.WinUI/Assets/NotificationIcons/archive.png and /dev/null differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-join.theme-dark.scale-100.png b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-join.theme-dark.scale-100.png new file mode 100644 index 00000000..bc6fc2bc Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-join.theme-dark.scale-100.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-join.theme-dark.scale-125.png b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-join.theme-dark.scale-125.png new file mode 100644 index 00000000..1c23956e Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-join.theme-dark.scale-125.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-join.theme-dark.scale-150.png b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-join.theme-dark.scale-150.png new file mode 100644 index 00000000..b47963af Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-join.theme-dark.scale-150.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-join.theme-dark.scale-200.png b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-join.theme-dark.scale-200.png new file mode 100644 index 00000000..445bef6a Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-join.theme-dark.scale-200.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-join.theme-dark.scale-400.png b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-join.theme-dark.scale-400.png new file mode 100644 index 00000000..fb17ae9a Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-join.theme-dark.scale-400.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-join.theme-light.scale-100.png b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-join.theme-light.scale-100.png new file mode 100644 index 00000000..4ba19db7 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-join.theme-light.scale-100.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-join.theme-light.scale-125.png b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-join.theme-light.scale-125.png new file mode 100644 index 00000000..01a4d0ee Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-join.theme-light.scale-125.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-join.theme-light.scale-150.png b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-join.theme-light.scale-150.png new file mode 100644 index 00000000..1a12fe40 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-join.theme-light.scale-150.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-join.theme-light.scale-200.png b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-join.theme-light.scale-200.png new file mode 100644 index 00000000..1905dfb0 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-join.theme-light.scale-200.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-join.theme-light.scale-400.png b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-join.theme-light.scale-400.png new file mode 100644 index 00000000..3be418e3 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-join.theme-light.scale-400.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-snooze.theme-dark.scale-100.png b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-snooze.theme-dark.scale-100.png new file mode 100644 index 00000000..618e1d16 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-snooze.theme-dark.scale-100.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-snooze.theme-dark.scale-125.png b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-snooze.theme-dark.scale-125.png new file mode 100644 index 00000000..a568d93d Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-snooze.theme-dark.scale-125.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-snooze.theme-dark.scale-150.png b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-snooze.theme-dark.scale-150.png new file mode 100644 index 00000000..f9968a2b Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-snooze.theme-dark.scale-150.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-snooze.theme-dark.scale-200.png b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-snooze.theme-dark.scale-200.png new file mode 100644 index 00000000..7357261a Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-snooze.theme-dark.scale-200.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-snooze.theme-dark.scale-400.png b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-snooze.theme-dark.scale-400.png new file mode 100644 index 00000000..0ad818e1 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-snooze.theme-dark.scale-400.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-snooze.theme-light.scale-100.png b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-snooze.theme-light.scale-100.png new file mode 100644 index 00000000..8e297571 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-snooze.theme-light.scale-100.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-snooze.theme-light.scale-125.png b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-snooze.theme-light.scale-125.png new file mode 100644 index 00000000..9ec09801 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-snooze.theme-light.scale-125.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-snooze.theme-light.scale-150.png b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-snooze.theme-light.scale-150.png new file mode 100644 index 00000000..daebf99b Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-snooze.theme-light.scale-150.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-snooze.theme-light.scale-200.png b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-snooze.theme-light.scale-200.png new file mode 100644 index 00000000..3498f142 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-snooze.theme-light.scale-200.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-snooze.theme-light.scale-400.png b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-snooze.theme-light.scale-400.png new file mode 100644 index 00000000..0bd4607d Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/calendar-snooze.theme-light.scale-400.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/delete.png b/Wino.Mail.WinUI/Assets/NotificationIcons/delete.png deleted file mode 100644 index bc46276e..00000000 Binary files a/Wino.Mail.WinUI/Assets/NotificationIcons/delete.png and /dev/null differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/dismiss.png b/Wino.Mail.WinUI/Assets/NotificationIcons/dismiss.png deleted file mode 100644 index 63086036..00000000 Binary files a/Wino.Mail.WinUI/Assets/NotificationIcons/dismiss.png and /dev/null differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/dismiss.theme-dark.scale-100.png b/Wino.Mail.WinUI/Assets/NotificationIcons/dismiss.theme-dark.scale-100.png new file mode 100644 index 00000000..e81df3fb Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/dismiss.theme-dark.scale-100.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/dismiss.theme-dark.scale-125.png b/Wino.Mail.WinUI/Assets/NotificationIcons/dismiss.theme-dark.scale-125.png new file mode 100644 index 00000000..679d63f3 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/dismiss.theme-dark.scale-125.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/dismiss.theme-dark.scale-150.png b/Wino.Mail.WinUI/Assets/NotificationIcons/dismiss.theme-dark.scale-150.png new file mode 100644 index 00000000..8ba86ea4 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/dismiss.theme-dark.scale-150.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/dismiss.theme-dark.scale-200.png b/Wino.Mail.WinUI/Assets/NotificationIcons/dismiss.theme-dark.scale-200.png new file mode 100644 index 00000000..735d6c69 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/dismiss.theme-dark.scale-200.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/dismiss.theme-dark.scale-400.png b/Wino.Mail.WinUI/Assets/NotificationIcons/dismiss.theme-dark.scale-400.png new file mode 100644 index 00000000..a6591fd2 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/dismiss.theme-dark.scale-400.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/dismiss.theme-light.scale-100.png b/Wino.Mail.WinUI/Assets/NotificationIcons/dismiss.theme-light.scale-100.png new file mode 100644 index 00000000..a635bbfa Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/dismiss.theme-light.scale-100.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/dismiss.theme-light.scale-125.png b/Wino.Mail.WinUI/Assets/NotificationIcons/dismiss.theme-light.scale-125.png new file mode 100644 index 00000000..5f2fa277 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/dismiss.theme-light.scale-125.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/dismiss.theme-light.scale-150.png b/Wino.Mail.WinUI/Assets/NotificationIcons/dismiss.theme-light.scale-150.png new file mode 100644 index 00000000..4cdcf55c Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/dismiss.theme-light.scale-150.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/dismiss.theme-light.scale-200.png b/Wino.Mail.WinUI/Assets/NotificationIcons/dismiss.theme-light.scale-200.png new file mode 100644 index 00000000..24b40c5c Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/dismiss.theme-light.scale-200.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/dismiss.theme-light.scale-400.png b/Wino.Mail.WinUI/Assets/NotificationIcons/dismiss.theme-light.scale-400.png new file mode 100644 index 00000000..c05ba123 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/dismiss.theme-light.scale-400.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/mail-archive.theme-dark.scale-100.png b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-archive.theme-dark.scale-100.png new file mode 100644 index 00000000..ef6b6b8f Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-archive.theme-dark.scale-100.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/mail-archive.theme-dark.scale-125.png b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-archive.theme-dark.scale-125.png new file mode 100644 index 00000000..0601580d Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-archive.theme-dark.scale-125.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/mail-archive.theme-dark.scale-150.png b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-archive.theme-dark.scale-150.png new file mode 100644 index 00000000..b7435c8a Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-archive.theme-dark.scale-150.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/mail-archive.theme-dark.scale-200.png b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-archive.theme-dark.scale-200.png new file mode 100644 index 00000000..785fbb5a Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-archive.theme-dark.scale-200.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/mail-archive.theme-dark.scale-400.png b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-archive.theme-dark.scale-400.png new file mode 100644 index 00000000..a4e573d9 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-archive.theme-dark.scale-400.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/mail-archive.theme-light.scale-100.png b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-archive.theme-light.scale-100.png new file mode 100644 index 00000000..de9ddc37 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-archive.theme-light.scale-100.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/mail-archive.theme-light.scale-125.png b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-archive.theme-light.scale-125.png new file mode 100644 index 00000000..922eb752 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-archive.theme-light.scale-125.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/mail-archive.theme-light.scale-150.png b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-archive.theme-light.scale-150.png new file mode 100644 index 00000000..edfc3805 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-archive.theme-light.scale-150.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/mail-archive.theme-light.scale-200.png b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-archive.theme-light.scale-200.png new file mode 100644 index 00000000..1369d7ce Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-archive.theme-light.scale-200.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/mail-archive.theme-light.scale-400.png b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-archive.theme-light.scale-400.png new file mode 100644 index 00000000..fc527f89 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-archive.theme-light.scale-400.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/mail-delete.theme-dark.scale-100.png b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-delete.theme-dark.scale-100.png new file mode 100644 index 00000000..e6ea8428 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-delete.theme-dark.scale-100.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/mail-delete.theme-dark.scale-125.png b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-delete.theme-dark.scale-125.png new file mode 100644 index 00000000..c74ce5f9 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-delete.theme-dark.scale-125.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/mail-delete.theme-dark.scale-150.png b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-delete.theme-dark.scale-150.png new file mode 100644 index 00000000..f970df4b Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-delete.theme-dark.scale-150.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/mail-delete.theme-dark.scale-200.png b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-delete.theme-dark.scale-200.png new file mode 100644 index 00000000..7ecaf105 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-delete.theme-dark.scale-200.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/mail-delete.theme-dark.scale-400.png b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-delete.theme-dark.scale-400.png new file mode 100644 index 00000000..23702da9 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-delete.theme-dark.scale-400.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/mail-delete.theme-light.scale-100.png b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-delete.theme-light.scale-100.png new file mode 100644 index 00000000..9f57ff6f Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-delete.theme-light.scale-100.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/mail-delete.theme-light.scale-125.png b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-delete.theme-light.scale-125.png new file mode 100644 index 00000000..8c1c5cf5 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-delete.theme-light.scale-125.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/mail-delete.theme-light.scale-150.png b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-delete.theme-light.scale-150.png new file mode 100644 index 00000000..1eb3b3bd Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-delete.theme-light.scale-150.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/mail-delete.theme-light.scale-200.png b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-delete.theme-light.scale-200.png new file mode 100644 index 00000000..0a3dc404 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-delete.theme-light.scale-200.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/mail-delete.theme-light.scale-400.png b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-delete.theme-light.scale-400.png new file mode 100644 index 00000000..7f66c6fa Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-delete.theme-light.scale-400.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/mail-markread.theme-dark.scale-100.png b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-markread.theme-dark.scale-100.png new file mode 100644 index 00000000..abf2884e Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-markread.theme-dark.scale-100.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/mail-markread.theme-dark.scale-125.png b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-markread.theme-dark.scale-125.png new file mode 100644 index 00000000..11cd2dd1 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-markread.theme-dark.scale-125.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/mail-markread.theme-dark.scale-150.png b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-markread.theme-dark.scale-150.png new file mode 100644 index 00000000..0f16c559 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-markread.theme-dark.scale-150.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/mail-markread.theme-dark.scale-200.png b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-markread.theme-dark.scale-200.png new file mode 100644 index 00000000..c6c548d0 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-markread.theme-dark.scale-200.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/mail-markread.theme-dark.scale-400.png b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-markread.theme-dark.scale-400.png new file mode 100644 index 00000000..7ead265f Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-markread.theme-dark.scale-400.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/mail-markread.theme-light.scale-100.png b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-markread.theme-light.scale-100.png new file mode 100644 index 00000000..03cde630 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-markread.theme-light.scale-100.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/mail-markread.theme-light.scale-125.png b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-markread.theme-light.scale-125.png new file mode 100644 index 00000000..559a08e9 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-markread.theme-light.scale-125.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/mail-markread.theme-light.scale-150.png b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-markread.theme-light.scale-150.png new file mode 100644 index 00000000..403a5962 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-markread.theme-light.scale-150.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/mail-markread.theme-light.scale-200.png b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-markread.theme-light.scale-200.png new file mode 100644 index 00000000..5d56de68 Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-markread.theme-light.scale-200.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/mail-markread.theme-light.scale-400.png b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-markread.theme-light.scale-400.png new file mode 100644 index 00000000..2ddd86dd Binary files /dev/null and b/Wino.Mail.WinUI/Assets/NotificationIcons/mail-markread.theme-light.scale-400.png differ diff --git a/Wino.Mail.WinUI/Assets/NotificationIcons/markread.png b/Wino.Mail.WinUI/Assets/NotificationIcons/markread.png deleted file mode 100644 index 114ab059..00000000 Binary files a/Wino.Mail.WinUI/Assets/NotificationIcons/markread.png and /dev/null differ diff --git a/Wino.Mail.WinUI/MailAppShell.xaml.cs b/Wino.Mail.WinUI/MailAppShell.xaml.cs index 049c360a..0c826a5a 100644 --- a/Wino.Mail.WinUI/MailAppShell.xaml.cs +++ b/Wino.Mail.WinUI/MailAppShell.xaml.cs @@ -38,6 +38,8 @@ public sealed partial class MailAppShell : MailAppShellAbstract, IRecipient, IRecipient { + public Frame GetShellFrame() => InnerShellFrame; + [GeneratedDependencyProperty] public partial UIElement? TopShellContent { get; set; } diff --git a/Wino.Mail.WinUI/Services/NavigationService.cs b/Wino.Mail.WinUI/Services/NavigationService.cs index d804245d..61f5d7ea 100644 --- a/Wino.Mail.WinUI/Services/NavigationService.cs +++ b/Wino.Mail.WinUI/Services/NavigationService.cs @@ -129,6 +129,19 @@ public class NavigationService : NavigationServiceBase, INavigationService if (frameType == NavigationReferenceFrame.ShellFrame) return shellWindow.GetMainFrame(); + if (frameType == NavigationReferenceFrame.InnerShellFrame) + { + if (mainFrame.Content is MailAppShell mailAppShell) + { + return mailAppShell.GetShellFrame(); + } + + if (mainFrame.Content is CalendarAppShell calendarAppShell) + { + return calendarAppShell.GetShellFrame(); + } + } + var contentRoot = mainFrame.Content as UIElement; if (contentRoot == null) return mainFrame; diff --git a/Wino.Mail.WinUI/Services/NotificationBuilder.cs b/Wino.Mail.WinUI/Services/NotificationBuilder.cs index 372c9625..c5a44a55 100644 --- a/Wino.Mail.WinUI/Services/NotificationBuilder.cs +++ b/Wino.Mail.WinUI/Services/NotificationBuilder.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.Messaging; using Microsoft.Toolkit.Uwp.Notifications; +using Microsoft.UI.Xaml; using Serilog; using Windows.Data.Xml.Dom; using Windows.UI.Notifications; @@ -12,6 +14,7 @@ using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Extensions; using Wino.Core.Domain.Interfaces; using Wino.Messaging.UI; @@ -20,21 +23,29 @@ namespace Wino.Mail.WinUI.Services; public class NotificationBuilder : INotificationBuilder { private const string MailApplicationId = "App"; + private const string NotificationIconRootUri = "ms-appx:///Assets/NotificationIcons/"; + private static readonly int[] SupportedIconScales = [100, 125, 150, 200, 400]; private readonly IAccountService _accountService; private readonly IFolderService _folderService; private readonly IMailService _mailService; private readonly IThumbnailService _thumbnailService; + private readonly IPreferencesService _preferencesService; + private readonly IUnderlyingThemeService _underlyingThemeService; public NotificationBuilder(IAccountService accountService, IFolderService folderService, IMailService mailService, - IThumbnailService thumbnailService) + IThumbnailService thumbnailService, + IPreferencesService preferencesService, + IUnderlyingThemeService underlyingThemeService) { _accountService = accountService; _folderService = folderService; _mailService = mailService; _thumbnailService = thumbnailService; + _preferencesService = preferencesService; + _underlyingThemeService = underlyingThemeService; WeakReferenceMessenger.Default.Register(this, (r, msg) => { @@ -156,12 +167,12 @@ public class NotificationBuilder : INotificationBuilder private ToastButton GetDismissButton() => new ToastButton() .SetDismissActivation() - .SetImageUri(new Uri("ms-appx:///Assets/NotificationIcons/dismiss.png")); + .SetImageUri(GetNotificationIconUri("dismiss")); - private static ToastButton GetArchiveButton(Guid mailUniqueId) + private ToastButton GetArchiveButton(Guid mailUniqueId) => new ToastButton() .SetContent(Translator.MailOperation_Archive) - .SetImageUri(new Uri("ms-appx:///Assets/NotificationIcons/archive.png")) + .SetImageUri(GetNotificationIconUri("mail-archive")) .AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString()) .AddArgument(Constants.ToastActionKey, MailOperation.Archive) .AddArgument(Constants.ToastModeKey, Constants.ToastModeMail) @@ -170,16 +181,16 @@ public class NotificationBuilder : INotificationBuilder private ToastButton GetDeleteButton(Guid mailUniqueId) => new ToastButton() .SetContent(Translator.MailOperation_Delete) - .SetImageUri(new Uri("ms-appx:///Assets/NotificationIcons/delete.png")) + .SetImageUri(GetNotificationIconUri("mail-delete")) .AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString()) .AddArgument(Constants.ToastActionKey, MailOperation.SoftDelete) .AddArgument(Constants.ToastModeKey, Constants.ToastModeMail) .SetBackgroundActivation(); - private static ToastButton GetMarkAsReadButton(Guid mailUniqueId) + private ToastButton GetMarkAsReadButton(Guid mailUniqueId) => new ToastButton() .SetContent(Translator.MailOperation_MarkAsRead) - .SetImageUri(new System.Uri("ms-appx:///Assets/NotificationIcons/markread.png")) + .SetImageUri(GetNotificationIconUri("mail-markread")) .AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString()) .AddArgument(Constants.ToastActionKey, MailOperation.MarkAsRead) .AddArgument(Constants.ToastModeKey, Constants.ToastModeMail) @@ -290,11 +301,9 @@ public class NotificationBuilder : INotificationBuilder 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"; + var localStart = calendarItem.GetLocalStartDate(); + var nowLocal = DateTime.Now; + var reminderContext = GetCalendarReminderContext(localStart, nowLocal); builder.AddText(calendarItem.Title); builder.AddText($"{reminderContext} - {localStart:g}"); @@ -305,7 +314,54 @@ public class NotificationBuilder : INotificationBuilder builder.AddArgument(Constants.ToastCalendarActionKey, Constants.ToastCalendarNavigateAction); builder.AddArgument(Constants.ToastCalendarItemIdKey, calendarItem.Id.ToString()); builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeCalendar); - builder.AddButton(GetDismissButton()); + + var allowedSnoozeMinutes = CalendarReminderSnoozeOptions.GetAllowedSnoozeMinutes( + reminderDurationInSeconds, + _preferencesService.DefaultReminderDurationInSeconds); + + if (allowedSnoozeMinutes.Count > 0) + { + var preferredSnoozeMinutes = _preferencesService.DefaultSnoozeDurationInMinutes; + var defaultSnoozeMinutes = allowedSnoozeMinutes.Contains(preferredSnoozeMinutes) + ? preferredSnoozeMinutes + : allowedSnoozeMinutes[0]; + + var selectionBox = new ToastSelectionBox(Constants.ToastCalendarSnoozeDurationInputId) + { + DefaultSelectionBoxItemId = defaultSnoozeMinutes.ToString() + }; + + foreach (var snoozeMinutes in allowedSnoozeMinutes) + { + selectionBox.Items.Add(new ToastSelectionBoxItem( + snoozeMinutes.ToString(), + string.Format(Translator.CalendarReminder_SnoozeMinutesOption, snoozeMinutes))); + } + + builder.AddToastInput(selectionBox); + var snoozeButton = new ToastButton() + .SetContent(Translator.CalendarReminder_SnoozeAction) + .SetImageUri(GetNotificationIconUri("calendar-snooze")) + .SetBackgroundActivation(); + + builder.AddButton(snoozeButton) + .AddArgument(Constants.ToastCalendarActionKey, Constants.ToastCalendarSnoozeAction) + .AddArgument(Constants.ToastCalendarItemIdKey, calendarItem.Id.ToString()) + .AddArgument(Constants.ToastModeKey, Constants.ToastModeCalendar); + } + + builder.AddButton(new ToastButton() + .SetDismissActivation() + .SetImageUri(GetNotificationIconUri("dismiss"))); + + if (Uri.TryCreate(calendarItem.HtmlLink, UriKind.Absolute, out var joinUri)) + { + builder.AddButton(new ToastButton() + .SetContent(Translator.CalendarEventDetails_JoinOnline) + .SetImageUri(GetNotificationIconUri("calendar-join")) + .SetProtocolActivation(joinUri)); + } + builder.AddAudio(new ToastAudio() { Src = new Uri("ms-winsoundevent:Notification.Reminder") @@ -317,6 +373,36 @@ public class NotificationBuilder : INotificationBuilder return Task.CompletedTask; } + private static string GetCalendarReminderContext(DateTime localStart, DateTime nowLocal) + { + var delta = localStart - nowLocal; + var absDelta = delta.Duration(); + + if (absDelta < TimeSpan.FromMinutes(1)) + return delta.TotalSeconds >= 0 ? Translator.CalendarReminder_StartingNow : Translator.CalendarReminder_StartedNow; + + if (delta.TotalSeconds > 0) + { + if (delta.TotalHours >= 1) + { + var hours = Math.Max(1, (int)Math.Floor(delta.TotalHours)); + return string.Format(Translator.CalendarReminder_StartsInHours, hours); + } + + var minutes = Math.Max(1, (int)Math.Floor(delta.TotalMinutes)); + return string.Format(Translator.CalendarReminder_StartsInMinutes, minutes); + } + + if (absDelta.TotalHours >= 1) + { + var hoursAgo = Math.Max(1, (int)Math.Floor(absDelta.TotalHours)); + return string.Format(Translator.CalendarReminder_StartedHoursAgo, hoursAgo); + } + + var minutesAgo = Math.Max(1, (int)Math.Floor(absDelta.TotalMinutes)); + return string.Format(Translator.CalendarReminder_StartedMinutesAgo, minutesAgo); + } + private static void ShowToast(ToastContentBuilder builder, string? tag = null) { var toastNotification = new ToastNotification(builder.GetToastContent().GetXml()); @@ -329,4 +415,25 @@ public class NotificationBuilder : INotificationBuilder var notifier = ToastNotificationManager.CreateToastNotifier(); notifier.Show(toastNotification); } + + private Uri GetNotificationIconUri(string iconName) + { + var theme = _underlyingThemeService.IsUnderlyingThemeDark() ? "dark" : "light"; + var scale = GetClosestAvailableScale(); + return new($"{NotificationIconRootUri}{iconName}.theme-{theme}.scale-{scale}.png"); + } + + private static int GetClosestAvailableScale() + { + var rasterScale = 1.0; + + if (WinoApplication.MainWindow?.Content is FrameworkElement rootElement && + rootElement.XamlRoot != null) + { + rasterScale = rootElement.XamlRoot.RasterizationScale; + } + + var requestedScale = (int)Math.Round(rasterScale * 100); + return SupportedIconScales.OrderBy(s => Math.Abs(s - requestedScale)).First(); + } } diff --git a/Wino.Mail.WinUI/Services/PreferencesService.cs b/Wino.Mail.WinUI/Services/PreferencesService.cs index 9f952022..5c321dd1 100644 --- a/Wino.Mail.WinUI/Services/PreferencesService.cs +++ b/Wino.Mail.WinUI/Services/PreferencesService.cs @@ -284,6 +284,12 @@ public class PreferencesService(IConfigurationService configurationService) : Ob set => SaveProperty(propertyName: nameof(DefaultReminderDurationInSeconds), value); } + public int DefaultSnoozeDurationInMinutes + { + get => _configurationService.Get(nameof(DefaultSnoozeDurationInMinutes), 5); + set => SaveProperty(propertyName: nameof(DefaultSnoozeDurationInMinutes), value); + } + public int EmailSyncIntervalMinutes { get => _configurationService.Get(nameof(EmailSyncIntervalMinutes), 3); diff --git a/Wino.Mail.WinUI/Views/Calendar/CalendarSettingsPage.xaml b/Wino.Mail.WinUI/Views/Calendar/CalendarSettingsPage.xaml index d2990476..cfe95323 100644 --- a/Wino.Mail.WinUI/Views/Calendar/CalendarSettingsPage.xaml +++ b/Wino.Mail.WinUI/Views/Calendar/CalendarSettingsPage.xaml @@ -235,6 +235,17 @@ + + + + + + + + + diff --git a/Wino.Mail.WinUI/Views/Calendar/EventDetailsPage.xaml b/Wino.Mail.WinUI/Views/Calendar/EventDetailsPage.xaml index d07f57d0..86ceaf9b 100644 --- a/Wino.Mail.WinUI/Views/Calendar/EventDetailsPage.xaml +++ b/Wino.Mail.WinUI/Views/Calendar/EventDetailsPage.xaml @@ -207,6 +207,23 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wino.Services/CalendarService.cs b/Wino.Services/CalendarService.cs index 9ec43d34..2c46ff2c 100644 --- a/Wino.Services/CalendarService.cs +++ b/Wino.Services/CalendarService.cs @@ -336,6 +336,12 @@ public class CalendarService : BaseDatabaseService, ICalendarService }); } + public Task SnoozeCalendarItemAsync(Guid calendarItemId, DateTime snoozedUntilLocal) + => Connection.ExecuteAsync( + $"UPDATE {nameof(CalendarItem)} SET {nameof(CalendarItem.SnoozedUntil)} = ? WHERE {nameof(CalendarItem.Id)} = ?", + snoozedUntilLocal, + calendarItemId); + public async Task> CheckAndNotifyAsync(DateTime lastCheckLocal, DateTime nowLocal, ISet sentReminderKeys, CancellationToken cancellationToken = default) { if (sentReminderKeys == null) @@ -347,7 +353,8 @@ public class CalendarService : BaseDatabaseService, ICalendarService c.Id AS CalendarItemId, c.StartDate, c.StartTimeZone, - r.DurationInSeconds AS ReminderDurationInSeconds + r.DurationInSeconds AS ReminderDurationInSeconds, + c.SnoozedUntil FROM CalendarItem c INNER JOIN Reminder r ON r.CalendarItemId = c.Id INNER JOIN AccountCalendar ac ON ac.Id = c.CalendarId @@ -367,11 +374,14 @@ public class CalendarService : BaseDatabaseService, ICalendarService var eventStartLocal = candidate.StartDate.ToLocalTimeFromTimeZone(candidate.StartTimeZone); var triggerTimeLocal = eventStartLocal.AddSeconds(-candidate.ReminderDurationInSeconds); + var effectiveTriggerTimeLocal = candidate.SnoozedUntil.HasValue + ? MaxDateTime(triggerTimeLocal, candidate.SnoozedUntil.Value) + : triggerTimeLocal; - if (triggerTimeLocal <= lastCheckLocal || triggerTimeLocal > nowLocal) + if (effectiveTriggerTimeLocal <= lastCheckLocal || effectiveTriggerTimeLocal > nowLocal) continue; - var reminderKey = $"{candidate.CalendarItemId:N}:{candidate.ReminderDurationInSeconds}"; + var reminderKey = $"{candidate.CalendarItemId:N}:{candidate.ReminderDurationInSeconds}:{effectiveTriggerTimeLocal.Ticks}"; if (!sentReminderKeys.Add(reminderKey)) continue; @@ -438,11 +448,15 @@ public class CalendarService : BaseDatabaseService, ICalendarService #endregion + private static DateTime MaxDateTime(DateTime first, DateTime second) + => first >= second ? first : second; + private sealed class CalendarReminderCandidate { public Guid CalendarItemId { get; set; } public DateTime StartDate { get; set; } public string StartTimeZone { get; set; } public long ReminderDurationInSeconds { get; set; } + public DateTime? SnoozedUntil { get; set; } } } diff --git a/Wino.Services/DatabaseService.cs b/Wino.Services/DatabaseService.cs index f9981192..b2dfb7eb 100644 --- a/Wino.Services/DatabaseService.cs +++ b/Wino.Services/DatabaseService.cs @@ -120,6 +120,15 @@ public class DatabaseService : IDatabaseService .ConfigureAwait(false); } + var calendarItemColumns = await Connection.GetTableInfoAsync(nameof(CalendarItem)).ConfigureAwait(false); + + if (!calendarItemColumns.Any(c => c.Name == nameof(CalendarItem.SnoozedUntil))) + { + await Connection + .ExecuteAsync($"ALTER TABLE {nameof(CalendarItem)} ADD COLUMN {nameof(CalendarItem.SnoozedUntil)} TEXT NULL") + .ConfigureAwait(false); + } + var contactColumns = await Connection.GetTableInfoAsync(nameof(AccountContact)).ConfigureAwait(false); if (!contactColumns.Any(c => c.Name == nameof(AccountContact.ContactPictureFileId)))