Add snooze support for calendar reminders (toast UI, service, DB) (#825)

* Filter reminder snooze options by default reminder

* Some updates.

* Fixing empty welcome page issue and attendee loading.

* Icon system for notifications and snooze options etc.
This commit is contained in:
Burak Kaan Köse
2026-03-04 00:12:52 +01:00
committed by GitHub
parent e816e87f61
commit 5b3739c6cf
85 changed files with 486 additions and 27 deletions
+1
View File
@@ -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(...)`.
@@ -50,6 +50,12 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
[ObservableProperty]
public partial int SelectedDefaultReminderIndex { get; set; }
[ObservableProperty]
public partial List<string> 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());
}
}
@@ -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<CalendarEventAttendee>();
// 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()
{
@@ -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<int> GetSupportedSnoozeMinutes()
=> SupportedSnoozeMinutes;
public static IReadOnlyList<int> 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();
}
}
+2
View File
@@ -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);
@@ -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; }
@@ -42,6 +42,7 @@ public interface ICalendarService
Task UpdateCalendarItemAsync(CalendarItem calendarItem, List<CalendarEventAttendee> attendees);
Task<List<Reminder>> GetRemindersAsync(Guid calendarItemId);
Task SaveRemindersAsync(Guid calendarItemId, List<Reminder> reminders);
Task SnoozeCalendarItemAsync(Guid calendarItemId, DateTime snoozedUntilLocal);
/// <summary>
/// Checks due reminder windows and returns reminder notifications that should trigger now.
@@ -222,6 +222,11 @@ public interface IPreferencesService : INotifyPropertyChanged
/// </summary>
long DefaultReminderDurationInSeconds { get; set; }
/// <summary>
/// Setting: Default snooze duration in minutes for calendar reminder notifications.
/// </summary>
int DefaultSnoozeDurationInMinutes { get; set; }
CalendarSettings GetCurrentCalendarSettings();
#endregion
@@ -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.",
@@ -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();
}
}
@@ -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<string> 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<CalendarItem> CreateCalendarItemWithReminderAsync(
DateTime startDate,
long reminderDurationInSeconds,
+40 -3
View File
@@ -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<ICalendarService>();
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;
}
/// <summary>
/// Handles toast notification click for navigation.
/// Creates window if not running, sets up navigation parameter.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 669 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 887 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 817 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 805 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 706 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 527 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 989 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

+2
View File
@@ -38,6 +38,8 @@ public sealed partial class MailAppShell : MailAppShellAbstract,
IRecipient<CreateNewMailWithMultipleAccountsRequested>,
IRecipient<InfoBarMessageRequested>
{
public Frame GetShellFrame() => InnerShellFrame;
[GeneratedDependencyProperty]
public partial UIElement? TopShellContent { get; set; }
@@ -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;
+120 -13
View File
@@ -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<MailReadStatusChanged>(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();
}
}
@@ -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);
@@ -235,6 +235,17 @@
<ComboBox ItemsSource="{x:Bind ViewModel.ReminderOptions, Mode=OneWay}" SelectedIndex="{x:Bind ViewModel.SelectedDefaultReminderIndex, Mode=TwoWay}" />
</controls:SettingsCard.Content>
</controls:SettingsCard>
<controls:SettingsCard
Description="{x:Bind domain:Translator.CalendarSettings_DefaultSnoozeDuration_Description}"
Header="{x:Bind domain:Translator.CalendarSettings_DefaultSnoozeDuration_Header}">
<controls:SettingsCard.HeaderIcon>
<PathIcon Data="F1 M 10 1.25 C 10.456706 1.25 10.889486 1.337565 11.298339 1.512695 C 11.707192 1.687826 12.072591 1.927409 12.394531 2.231445 C 12.716471 2.535482 12.97656 2.892253 13.173828 3.301758 C 13.371096 3.711263 13.481771 4.146484 13.505859 4.607422 L 13.505859 5 C 13.969401 5.028646 14.386068 5.16276 14.755859 5.402344 C 15.125651 5.641927 15.414713 5.947917 15.623047 6.320312 C 15.83138 6.692709 15.9349 7.096355 15.933594 7.53125 L 15.933594 8.75 L 16.25 8.75 C 16.822917 8.75 17.317057 8.95638 17.732422 9.369141 C 18.147787 9.781901 18.357422 10.273437 18.359375 10.84375 L 18.359375 16.40625 C 18.359375 16.979167 18.153971 17.473308 17.743164 17.888672 C 17.332357 18.304037 16.839844 18.511719 16.265625 18.511719 L 3.734375 18.511719 C 3.164062 18.511719 2.671549 18.304037 2.256836 17.888672 C 1.842122 17.473308 1.634114 16.979167 1.630859 16.40625 L 1.630859 10.84375 C 1.630859 10.273437 1.837565 9.781901 2.250977 9.369141 C 2.664388 8.95638 3.156901 8.75 3.728516 8.75 L 4.0625 8.75 L 4.0625 7.53125 C 4.0625 7.09375 4.166341 6.689453 4.374023 6.314453 C 4.581706 5.939453 4.86914 5.632486 5.236328 5.393555 C 5.603516 5.154623 6.019532 5.021485 6.484375 4.994141 L 6.484375 4.607422 C 6.506511 4.148438 6.617838 3.714518 6.818359 3.305664 C 7.01888 2.896811 7.282877 2.539063 7.610352 2.232422 C 7.937826 1.92578 8.308268 1.685222 8.721679 1.510742 C 9.135091 1.336264 9.56575 1.249024 10.013672 1.249024 Z M 10.013672 2.5 C 9.441406 2.5 8.947916 2.706381 8.533203 3.119141 C 8.118489 3.531901 7.911459 4.023437 7.912109 4.59375 L 7.912109 6.25 L 12.089844 6.25 L 12.089844 4.59375 C 12.089844 4.023438 11.882161 3.531902 11.466797 3.119141 C 11.051433 2.706382 10.557292 2.5 9.984375 2.5 Z M 5.3125 7.53125 L 5.3125 8.75 L 14.6875 8.75 L 14.6875 7.53125 C 14.6875 7.303385 14.605144 7.107747 14.440429 6.944336 C 14.275714 6.780925 14.080729 6.69987 13.855469 6.703125 L 6.142578 6.703125 C 5.914713 6.703125 5.71875 6.785482 5.552734 6.950195 C 5.386719 7.114909 5.30339 7.310547 5.300781 7.537109 Z M 16.25 10 L 3.75 10 C 3.540365 10 3.361328 10.071615 3.212891 10.214844 C 3.064453 10.358073 2.989258 10.534505 2.986328 10.744141 L 2.986328 16.40625 C 2.986328 16.618489 3.058595 16.797526 3.203125 16.943359 C 3.347656 17.089193 3.526693 17.161458 3.740234 17.15625 L 16.25 17.15625 C 16.458333 17.15625 16.636067 17.082683 16.783203 16.935547 C 16.930339 16.788411 17.003906 16.609375 17.003906 16.398437 L 17.003906 10.742187 C 17.003906 10.536459 16.932942 10.359049 16.791016 10.209961 C 16.64909 10.060873 16.469401 9.986328 16.257812 9.986328 Z M 10 11.5625 C 10.227865 11.5625 10.423502 11.644856 10.586914 11.809571 C 10.750325 11.974285 10.83138 12.16862 10.830078 12.393555 L 10.830078 13.427734 L 11.855469 13.427734 C 12.080404 13.427734 12.273437 13.509115 12.434571 13.671875 C 12.595704 13.834636 12.675781 14.026693 12.675781 14.248047 C 12.675781 14.46224 12.596354 14.653321 12.4375 14.813477 C 12.278646 14.973633 12.089192 15.053711 11.869141 15.053711 L 10.830078 15.053711 L 10.830078 16.074219 C 10.830078 16.296224 10.748372 16.486981 10.584961 16.646484 C 10.42155 16.805989 10.229818 16.885742 10.005859 16.885742 C 9.781901 16.885742 9.590495 16.806315 9.431641 16.647461 C 9.272786 16.488607 9.193359 16.296875 9.193359 16.072266 L 9.193359 15.053711 L 8.15625 15.053711 C 7.931314 15.053711 7.738934 14.972331 7.579102 14.80957 C 7.419269 14.646809 7.339192 14.454753 7.338867 14.233399 C 7.338867 14.008464 7.419921 13.815755 7.582031 13.655274 C 7.744141 13.494793 7.935872 13.414551 8.157227 13.414551 L 9.193359 13.414551 L 9.193359 12.394531 C 9.193359 12.166667 9.275715 11.971029 9.440429 11.807618 C 9.605144 11.644206 9.799479 11.562825 10.023437 11.5625 Z" />
</controls:SettingsCard.HeaderIcon>
<controls:SettingsCard.Content>
<ComboBox ItemsSource="{x:Bind ViewModel.SnoozeOptions, Mode=OneWay}" SelectedIndex="{x:Bind ViewModel.SelectedDefaultSnoozeIndex, Mode=TwoWay}" />
</controls:SettingsCard.Content>
</controls:SettingsCard>
</StackPanel>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="ClockIdentifierStates">
@@ -207,6 +207,23 @@
</Button.Flyout>
</Button>
<Border
Width="1"
Height="24"
Margin="4,0"
VerticalAlignment="Center"
Background="{ThemeResource DividerStrokeColorDefaultBrush}" />
<!-- Test Notification -->
<Button
Command="{x:Bind ViewModel.CreateTestNotificationCommand}"
Style="{StaticResource TransparentActionButtonStyle}">
<StackPanel Orientation="Horizontal" Spacing="8">
<coreControls:WinoFontIcon FontSize="16" Icon="Reminder" />
<TextBlock VerticalAlignment="Center" Text="Test notification" />
</StackPanel>
</Button>
<!-- Edit Series -->
<Border
Width="1"
+60
View File
@@ -98,6 +98,66 @@
</ItemGroup>
<ItemGroup>
<None Remove="Assets\CalendarWide310x150Logo.png" />
<None Remove="Assets\NotificationIcons\calendar-join.theme-dark.scale-100.png" />
<None Remove="Assets\NotificationIcons\calendar-join.theme-dark.scale-125.png" />
<None Remove="Assets\NotificationIcons\calendar-join.theme-dark.scale-150.png" />
<None Remove="Assets\NotificationIcons\calendar-join.theme-dark.scale-200.png" />
<None Remove="Assets\NotificationIcons\calendar-join.theme-dark.scale-400.png" />
<None Remove="Assets\NotificationIcons\calendar-join.theme-light.scale-100.png" />
<None Remove="Assets\NotificationIcons\calendar-join.theme-light.scale-125.png" />
<None Remove="Assets\NotificationIcons\calendar-join.theme-light.scale-150.png" />
<None Remove="Assets\NotificationIcons\calendar-join.theme-light.scale-200.png" />
<None Remove="Assets\NotificationIcons\calendar-join.theme-light.scale-400.png" />
<None Remove="Assets\NotificationIcons\calendar-snooze.theme-dark.scale-100.png" />
<None Remove="Assets\NotificationIcons\calendar-snooze.theme-dark.scale-125.png" />
<None Remove="Assets\NotificationIcons\calendar-snooze.theme-dark.scale-150.png" />
<None Remove="Assets\NotificationIcons\calendar-snooze.theme-dark.scale-200.png" />
<None Remove="Assets\NotificationIcons\calendar-snooze.theme-dark.scale-400.png" />
<None Remove="Assets\NotificationIcons\calendar-snooze.theme-light.scale-100.png" />
<None Remove="Assets\NotificationIcons\calendar-snooze.theme-light.scale-125.png" />
<None Remove="Assets\NotificationIcons\calendar-snooze.theme-light.scale-150.png" />
<None Remove="Assets\NotificationIcons\calendar-snooze.theme-light.scale-200.png" />
<None Remove="Assets\NotificationIcons\calendar-snooze.theme-light.scale-400.png" />
<None Remove="Assets\NotificationIcons\dismiss.theme-dark.scale-100.png" />
<None Remove="Assets\NotificationIcons\dismiss.theme-dark.scale-125.png" />
<None Remove="Assets\NotificationIcons\dismiss.theme-dark.scale-150.png" />
<None Remove="Assets\NotificationIcons\dismiss.theme-dark.scale-200.png" />
<None Remove="Assets\NotificationIcons\dismiss.theme-dark.scale-400.png" />
<None Remove="Assets\NotificationIcons\dismiss.theme-light.scale-100.png" />
<None Remove="Assets\NotificationIcons\dismiss.theme-light.scale-125.png" />
<None Remove="Assets\NotificationIcons\dismiss.theme-light.scale-150.png" />
<None Remove="Assets\NotificationIcons\dismiss.theme-light.scale-200.png" />
<None Remove="Assets\NotificationIcons\dismiss.theme-light.scale-400.png" />
<None Remove="Assets\NotificationIcons\mail-archive.theme-dark.scale-100.png" />
<None Remove="Assets\NotificationIcons\mail-archive.theme-dark.scale-125.png" />
<None Remove="Assets\NotificationIcons\mail-archive.theme-dark.scale-150.png" />
<None Remove="Assets\NotificationIcons\mail-archive.theme-dark.scale-200.png" />
<None Remove="Assets\NotificationIcons\mail-archive.theme-dark.scale-400.png" />
<None Remove="Assets\NotificationIcons\mail-archive.theme-light.scale-100.png" />
<None Remove="Assets\NotificationIcons\mail-archive.theme-light.scale-125.png" />
<None Remove="Assets\NotificationIcons\mail-archive.theme-light.scale-150.png" />
<None Remove="Assets\NotificationIcons\mail-archive.theme-light.scale-200.png" />
<None Remove="Assets\NotificationIcons\mail-archive.theme-light.scale-400.png" />
<None Remove="Assets\NotificationIcons\mail-delete.theme-dark.scale-100.png" />
<None Remove="Assets\NotificationIcons\mail-delete.theme-dark.scale-125.png" />
<None Remove="Assets\NotificationIcons\mail-delete.theme-dark.scale-150.png" />
<None Remove="Assets\NotificationIcons\mail-delete.theme-dark.scale-200.png" />
<None Remove="Assets\NotificationIcons\mail-delete.theme-dark.scale-400.png" />
<None Remove="Assets\NotificationIcons\mail-delete.theme-light.scale-100.png" />
<None Remove="Assets\NotificationIcons\mail-delete.theme-light.scale-125.png" />
<None Remove="Assets\NotificationIcons\mail-delete.theme-light.scale-150.png" />
<None Remove="Assets\NotificationIcons\mail-delete.theme-light.scale-200.png" />
<None Remove="Assets\NotificationIcons\mail-delete.theme-light.scale-400.png" />
<None Remove="Assets\NotificationIcons\mail-markread.theme-dark.scale-100.png" />
<None Remove="Assets\NotificationIcons\mail-markread.theme-dark.scale-125.png" />
<None Remove="Assets\NotificationIcons\mail-markread.theme-dark.scale-150.png" />
<None Remove="Assets\NotificationIcons\mail-markread.theme-dark.scale-200.png" />
<None Remove="Assets\NotificationIcons\mail-markread.theme-dark.scale-400.png" />
<None Remove="Assets\NotificationIcons\mail-markread.theme-light.scale-100.png" />
<None Remove="Assets\NotificationIcons\mail-markread.theme-light.scale-125.png" />
<None Remove="Assets\NotificationIcons\mail-markread.theme-light.scale-150.png" />
<None Remove="Assets\NotificationIcons\mail-markread.theme-light.scale-200.png" />
<None Remove="Assets\NotificationIcons\mail-markread.theme-light.scale-400.png" />
<None Remove="BackgroundImages\Acrylic.jpg" />
<None Remove="BackgroundImages\Clouds.jpg" />
<None Remove="BackgroundImages\Forest.jpg" />
+17 -3
View File
@@ -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<List<CalendarReminderNotificationRequest>> CheckAndNotifyAsync(DateTime lastCheckLocal, DateTime nowLocal, ISet<string> 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; }
}
}
+9
View File
@@ -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)))