Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c6cd06c65f | |||
| c942066878 |
@@ -0,0 +1,26 @@
|
||||
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> 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.
|
||||
|
||||
@@ -133,6 +133,7 @@
|
||||
"CalendarEventDetails_People": "People",
|
||||
"CalendarEventDetails_ReadOnlyEvent": "Read-only event",
|
||||
"CalendarEventDetails_Reminder": "Reminder",
|
||||
"CalendarReminder_SnoozeMinutesOption": "{0} minutes",
|
||||
"CalendarEventDetails_ShowAs": "Show as",
|
||||
"CalendarShowAs_Free": "Free",
|
||||
"CalendarShowAs_Tentative": "Tentative",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -215,14 +215,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))
|
||||
@@ -267,6 +275,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.
|
||||
|
||||
@@ -5,7 +5,6 @@ using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.Toolkit.Uwp.Notifications;
|
||||
using Serilog;
|
||||
using Windows.ApplicationModel;
|
||||
using Windows.Data.Xml.Dom;
|
||||
using Windows.UI.Notifications;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
@@ -26,16 +25,19 @@ public class NotificationBuilder : INotificationBuilder
|
||||
private readonly IFolderService _folderService;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly IThumbnailService _thumbnailService;
|
||||
private readonly IPreferencesService _preferencesService;
|
||||
|
||||
public NotificationBuilder(IAccountService accountService,
|
||||
IFolderService folderService,
|
||||
IMailService mailService,
|
||||
IThumbnailService thumbnailService)
|
||||
IThumbnailService thumbnailService,
|
||||
IPreferencesService preferencesService)
|
||||
{
|
||||
_accountService = accountService;
|
||||
_folderService = folderService;
|
||||
_mailService = mailService;
|
||||
_thumbnailService = thumbnailService;
|
||||
_preferencesService = preferencesService;
|
||||
|
||||
WeakReferenceMessenger.Default.Register<MailReadStatusChanged>(this, (r, msg) =>
|
||||
{
|
||||
@@ -96,7 +98,7 @@ public class NotificationBuilder : INotificationBuilder
|
||||
Src = new Uri("ms-winsoundevent:Notification.Mail")
|
||||
});
|
||||
|
||||
ShowToast(builder, ToastTargetApp.Mail);
|
||||
ShowToast(builder);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -151,7 +153,7 @@ public class NotificationBuilder : INotificationBuilder
|
||||
});
|
||||
|
||||
// Use UniqueId as tag to allow removal
|
||||
ShowToast(builder, ToastTargetApp.Mail, mailItem.UniqueId.ToString());
|
||||
ShowToast(builder, mailItem.UniqueId.ToString());
|
||||
}
|
||||
|
||||
private ToastButton GetDismissButton()
|
||||
@@ -242,7 +244,7 @@ public class NotificationBuilder : INotificationBuilder
|
||||
{
|
||||
try
|
||||
{
|
||||
ToastNotificationManager.History.Remove(mailUniqueId.ToString(), null, GetAppUserModelId(ToastTargetApp.Mail));
|
||||
ToastNotificationManager.History.Remove(mailUniqueId.ToString());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -263,7 +265,7 @@ public class NotificationBuilder : INotificationBuilder
|
||||
builder.AddArgument(Constants.ToastMailAccountIdKey, account.Id.ToString());
|
||||
builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
||||
builder.AddButton(new ToastButton().SetContent(Translator.Buttons_FixAccount));
|
||||
ShowToast(builder, ToastTargetApp.Mail);
|
||||
ShowToast(builder);
|
||||
}
|
||||
|
||||
public void CreateWebView2RuntimeMissingNotification()
|
||||
@@ -276,7 +278,7 @@ public class NotificationBuilder : INotificationBuilder
|
||||
|
||||
builder.AddButton(GetDismissButton());
|
||||
builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
||||
ShowToast(builder, ToastTargetApp.Mail);
|
||||
ShowToast(builder);
|
||||
}
|
||||
|
||||
public Task CreateCalendarReminderNotificationAsync(CalendarItem calendarItem, long reminderDurationInSeconds)
|
||||
@@ -302,19 +304,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 selectionBox = new ToastSelectionBox(Constants.ToastCalendarSnoozeDurationInputId)
|
||||
{
|
||||
DefaultSelectionBoxItemId = allowedSnoozeMinutes[0].ToString()
|
||||
};
|
||||
|
||||
foreach (var snoozeMinutes in allowedSnoozeMinutes)
|
||||
{
|
||||
selectionBox.Items.Add(new ToastSelectionBoxItem(
|
||||
snoozeMinutes.ToString(),
|
||||
string.Format(Translator.CalendarReminder_SnoozeMinutesOption, snoozeMinutes)));
|
||||
}
|
||||
|
||||
builder.AddInput(selectionBox);
|
||||
builder.AddButton(new ToastButtonSnooze()
|
||||
.SetSelectionBoxId(Constants.ToastCalendarSnoozeDurationInputId)
|
||||
.AddArgument(Constants.ToastCalendarActionKey, Constants.ToastCalendarSnoozeAction)
|
||||
.AddArgument(Constants.ToastCalendarItemIdKey, calendarItem.Id.ToString())
|
||||
.AddArgument(Constants.ToastModeKey, Constants.ToastModeCalendar));
|
||||
}
|
||||
|
||||
builder.AddButton(new ToastButtonDismiss());
|
||||
|
||||
if (Uri.TryCreate(calendarItem.HtmlLink, UriKind.Absolute, out var joinUri))
|
||||
{
|
||||
builder.AddButton(new ToastButton()
|
||||
.SetContent(Translator.CalendarEventDetails_JoinOnline)
|
||||
.SetProtocolActivation(joinUri));
|
||||
}
|
||||
|
||||
builder.AddAudio(new ToastAudio()
|
||||
{
|
||||
Src = new Uri("ms-winsoundevent:Notification.Reminder")
|
||||
});
|
||||
|
||||
var tag = $"calendar-reminder-{calendarItem.Id:N}-{reminderDurationInSeconds}";
|
||||
ShowToast(builder, ToastTargetApp.Calendar, tag);
|
||||
ShowToast(builder, tag);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static void ShowToast(ToastContentBuilder builder, ToastTargetApp targetApp, string? tag = null)
|
||||
private static void ShowToast(ToastContentBuilder builder, string? tag = null)
|
||||
{
|
||||
var toastNotification = new ToastNotification(builder.GetToastContent().GetXml());
|
||||
|
||||
@@ -323,20 +360,7 @@ public class NotificationBuilder : INotificationBuilder
|
||||
toastNotification.Tag = tag;
|
||||
}
|
||||
|
||||
var appUserModelId = GetAppUserModelId(targetApp);
|
||||
var notifier = ToastNotificationManager.CreateToastNotifier(appUserModelId);
|
||||
var notifier = ToastNotificationManager.CreateToastNotifier();
|
||||
notifier.Show(toastNotification);
|
||||
}
|
||||
|
||||
private static string GetAppUserModelId(ToastTargetApp targetApp)
|
||||
{
|
||||
_ = targetApp;
|
||||
return $"{Package.Current.Id.FamilyName}!{MailApplicationId}";
|
||||
}
|
||||
|
||||
private enum ToastTargetApp
|
||||
{
|
||||
Mail,
|
||||
Calendar
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +117,15 @@ public class DatabaseService : IDatabaseService
|
||||
.ExecuteAsync($"ALTER TABLE {nameof(CustomServerInformation)} ADD COLUMN {nameof(CustomServerInformation.CalendarSupportMode)} INTEGER NOT NULL DEFAULT 0")
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EnsureIndexesAsync()
|
||||
|
||||
Reference in New Issue
Block a user