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.
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,14 +226,22 @@ 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))
|
||||
{
|
||||
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).
|
||||
if (toastArguments.TryGetValue(Constants.ToastActionKey, out MailOperation action) &&
|
||||
Guid.TryParse(toastArguments[Constants.ToastMailUniqueIdKey], out Guid mailItemUniqueId))
|
||||
@@ -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.
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 459 B |
|
After Width: | Height: | Size: 545 B |
|
After Width: | Height: | Size: 669 B |
|
After Width: | Height: | Size: 887 B |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 434 B |
|
After Width: | Height: | Size: 507 B |
|
After Width: | Height: | Size: 615 B |
|
After Width: | Height: | Size: 817 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 351 B |
|
After Width: | Height: | Size: 445 B |
|
After Width: | Height: | Size: 495 B |
|
After Width: | Height: | Size: 685 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 323 B |
|
After Width: | Height: | Size: 417 B |
|
After Width: | Height: | Size: 459 B |
|
After Width: | Height: | Size: 636 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 180 B |
|
After Width: | Height: | Size: 234 B |
|
After Width: | Height: | Size: 230 B |
|
After Width: | Height: | Size: 283 B |
|
After Width: | Height: | Size: 521 B |
|
After Width: | Height: | Size: 174 B |
|
After Width: | Height: | Size: 226 B |
|
After Width: | Height: | Size: 229 B |
|
After Width: | Height: | Size: 275 B |
|
After Width: | Height: | Size: 509 B |
|
After Width: | Height: | Size: 279 B |
|
After Width: | Height: | Size: 359 B |
|
After Width: | Height: | Size: 383 B |
|
After Width: | Height: | Size: 371 B |
|
After Width: | Height: | Size: 805 B |
|
After Width: | Height: | Size: 272 B |
|
After Width: | Height: | Size: 324 B |
|
After Width: | Height: | Size: 362 B |
|
After Width: | Height: | Size: 328 B |
|
After Width: | Height: | Size: 706 B |
|
After Width: | Height: | Size: 303 B |
|
After Width: | Height: | Size: 426 B |
|
After Width: | Height: | Size: 496 B |
|
After Width: | Height: | Size: 541 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 289 B |
|
After Width: | Height: | Size: 408 B |
|
After Width: | Height: | Size: 465 B |
|
After Width: | Height: | Size: 527 B |
|
After Width: | Height: | Size: 989 B |
|
After Width: | Height: | Size: 336 B |
|
After Width: | Height: | Size: 418 B |
|
After Width: | Height: | Size: 458 B |
|
After Width: | Height: | Size: 595 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 308 B |
|
After Width: | Height: | Size: 382 B |
|
After Width: | Height: | Size: 447 B |
|
After Width: | Height: | Size: 574 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 31 KiB |
@@ -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;
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)))
|
||||
|
||||