UI visuals for mail calendar items, calendar reminders.
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Serilog;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Mail.WinUI.Interfaces;
|
||||
|
||||
namespace Wino.Mail.WinUI.Services;
|
||||
|
||||
public class CalendarReminderServer : ICalendarReminderServer
|
||||
{
|
||||
private static readonly TimeSpan PollingInterval = TimeSpan.FromSeconds(30);
|
||||
|
||||
private readonly ICalendarService _calendarService;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly INotificationBuilder _notificationBuilder;
|
||||
private readonly ILogger _logger = Log.ForContext<CalendarReminderServer>();
|
||||
private readonly SemaphoreSlim _startLock = new(1, 1);
|
||||
private readonly HashSet<string> _sentReminderKeys = [];
|
||||
|
||||
private Task? _loopTask;
|
||||
private CancellationTokenSource? _loopCts;
|
||||
private DateTime _lastCheckLocal = DateTime.MinValue;
|
||||
|
||||
public CalendarReminderServer(ICalendarService calendarService, IAccountService accountService, INotificationBuilder notificationBuilder)
|
||||
{
|
||||
_calendarService = calendarService;
|
||||
_accountService = accountService;
|
||||
_notificationBuilder = notificationBuilder;
|
||||
}
|
||||
|
||||
public async Task StartAsync()
|
||||
{
|
||||
await _startLock.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
if (_loopTask != null)
|
||||
return;
|
||||
|
||||
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
|
||||
|
||||
var hasCalendarAccess = accounts.Exists(a => a.IsCalendarAccessGranted);
|
||||
|
||||
if (!hasCalendarAccess)
|
||||
{
|
||||
_logger.Information("Calendar reminder server will not start because no account has calendar access.");
|
||||
return;
|
||||
}
|
||||
|
||||
_lastCheckLocal = DateTime.Now.AddSeconds(-30);
|
||||
_loopCts = new CancellationTokenSource();
|
||||
_loopTask = RunLoopAsync(_loopCts.Token);
|
||||
|
||||
_logger.Information("Calendar reminder server started.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_startLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using var timer = new PeriodicTimer(PollingInterval);
|
||||
|
||||
try
|
||||
{
|
||||
while (await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
await ExecuteTickAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// no-op
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Calendar reminder server loop terminated unexpectedly.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExecuteTickAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var nowLocal = DateTime.Now;
|
||||
|
||||
if (_lastCheckLocal == DateTime.MinValue)
|
||||
_lastCheckLocal = nowLocal.AddSeconds(-PollingInterval.TotalSeconds);
|
||||
|
||||
var dueNotifications = await _calendarService
|
||||
.CheckAndNotifyAsync(_lastCheckLocal, nowLocal, _sentReminderKeys, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
foreach (var reminder in dueNotifications)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await _notificationBuilder
|
||||
.CreateCalendarReminderNotificationAsync(reminder.CalendarItem, reminder.ReminderDurationInSeconds)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_lastCheckLocal = nowLocal;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ using Microsoft.Toolkit.Uwp.Notifications;
|
||||
using Serilog;
|
||||
using Windows.Data.Xml.Dom;
|
||||
using Windows.UI.Notifications;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
@@ -250,4 +251,38 @@ public class NotificationBuilder : INotificationBuilder
|
||||
builder.AddButton(new ToastButton().SetContent(Translator.Buttons_FixAccount));
|
||||
builder.Show();
|
||||
}
|
||||
|
||||
public Task CreateCalendarReminderNotificationAsync(CalendarItem calendarItem, long reminderDurationInSeconds)
|
||||
{
|
||||
if (calendarItem == null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
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";
|
||||
|
||||
builder.AddText(calendarItem.Title);
|
||||
builder.AddText($"{reminderContext} - {localStart:g}");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(calendarItem.Location))
|
||||
builder.AddText(calendarItem.Location);
|
||||
|
||||
builder.AddArgument(Constants.ToastCalendarActionKey, Constants.ToastCalendarNavigateAction);
|
||||
builder.AddArgument(Constants.ToastCalendarItemIdKey, calendarItem.Id.ToString());
|
||||
builder.AddButton(GetDismissButton());
|
||||
builder.AddAudio(new ToastAudio()
|
||||
{
|
||||
Src = new Uri("ms-winsoundevent:Notification.Reminder")
|
||||
});
|
||||
|
||||
var tag = $"calendar-reminder-{calendarItem.Id:N}-{reminderDurationInSeconds}";
|
||||
builder.Show(toast => toast.Tag = tag);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user