UI visuals for mail calendar items, calendar reminders.
This commit is contained in:
@@ -15,6 +15,7 @@ using Wino.Core;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
using Wino.Mail.Services;
|
||||
@@ -179,6 +180,16 @@ public partial class App : WinoApplication,
|
||||
{
|
||||
var toastArguments = ToastArguments.Parse(toastArgs.Argument);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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))
|
||||
@@ -198,6 +209,31 @@ public partial class App : WinoApplication,
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleCalendarToastNavigationAsync(Guid calendarItemId)
|
||||
{
|
||||
var calendarService = Services.GetRequiredService<ICalendarService>();
|
||||
var navigationService = Services.GetRequiredService<INavigationService>();
|
||||
|
||||
var calendarItem = await calendarService.GetCalendarItemAsync(calendarItemId).ConfigureAwait(false);
|
||||
if (calendarItem == null)
|
||||
return;
|
||||
|
||||
var target = new CalendarItemTarget(calendarItem, CalendarEventTargetType.Single);
|
||||
|
||||
if (!IsAppRunning())
|
||||
{
|
||||
await CreateAndActivateWindow(null!);
|
||||
}
|
||||
else
|
||||
{
|
||||
MainWindow.BringToFront();
|
||||
MainWindow.Activate();
|
||||
}
|
||||
|
||||
navigationService.ChangeApplicationMode(Core.Domain.Enums.WinoApplicationMode.Calendar);
|
||||
navigationService.Navigate(WinoPage.EventDetailsPage, target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles toast notification click for navigation.
|
||||
/// Creates window if not running, sets up navigation parameter.
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
xmlns:helpers="using:Wino.Helpers"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
|
||||
<Grid x:DefaultBindMode="OneWay">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
@@ -23,14 +22,30 @@
|
||||
Prefer24HourTimeFormat="{x:Bind Prefer24HourTimeFormat, Mode=OneWay}"
|
||||
ShowPreviewText="False" />
|
||||
|
||||
<TextBlock
|
||||
x:Name="EventDateText"
|
||||
<Grid
|
||||
x:Name="EventDateContainer"
|
||||
Grid.Row="1"
|
||||
Margin="53,0,12,8"
|
||||
FontSize="12"
|
||||
Opacity="0.75"
|
||||
Text="{x:Bind EventDateRangeText, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
Margin="46,0,12,8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<ContentPresenter
|
||||
x:Name="CalendarInvitationContent"
|
||||
Grid.Column="0"
|
||||
Margin="0,0,4,0"
|
||||
VerticalAlignment="Center"
|
||||
x:Load="{x:Bind MailItem.IsCalendarEvent, Mode=OneWay}"
|
||||
ContentTemplate="{StaticResource CalendarInvitationSymbolControlTemplate}" />
|
||||
<TextBlock
|
||||
x:Name="EventDateText"
|
||||
Grid.Column="1"
|
||||
Margin="4,0,2,0"
|
||||
FontSize="12"
|
||||
Opacity="0.75"
|
||||
Text="{x:Bind EventDateRangeText, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
|
||||
@@ -3,6 +3,7 @@ using Microsoft.UI.Xaml;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.ViewModels;
|
||||
using Wino.Core.WinUI.Services;
|
||||
using Wino.Mail.WinUI.Interfaces;
|
||||
using Wino.Mail.WinUI.Services;
|
||||
using Wino.Services;
|
||||
|
||||
@@ -29,6 +30,7 @@ public static class CoreUWPContainerSetup
|
||||
services.AddTransient<IStoreRatingService, StoreRatingService>();
|
||||
services.AddTransient<IKeyPressService, KeyPressService>();
|
||||
services.AddTransient<INotificationBuilder, NotificationBuilder>();
|
||||
services.AddSingleton<ICalendarReminderServer, CalendarReminderServer>();
|
||||
services.AddTransient<IClipboardService, ClipboardService>();
|
||||
services.AddTransient<IStartupBehaviorService, StartupBehaviorService>();
|
||||
services.AddSingleton<IPrintService, PrintService>();
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Wino.Mail.WinUI.Interfaces;
|
||||
|
||||
public interface ICalendarReminderServer
|
||||
{
|
||||
Task StartAsync();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
@@ -34,6 +35,7 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow,
|
||||
public ICommand ExitWinoCommand { get; set; }
|
||||
|
||||
public ObservableCollection<SynchronizationActionItem> SyncActionItems { get; } = new();
|
||||
private bool _calendarReminderServerStartAttempted;
|
||||
|
||||
public ShellWindow()
|
||||
{
|
||||
@@ -155,6 +157,12 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow,
|
||||
|
||||
private void MainFrameNavigated(object sender, Microsoft.UI.Xaml.Navigation.NavigationEventArgs e)
|
||||
{
|
||||
if (!_calendarReminderServerStartAttempted)
|
||||
{
|
||||
_calendarReminderServerStartAttempted = true;
|
||||
_ = StartCalendarReminderServerAsync();
|
||||
}
|
||||
|
||||
// Mail shell has shell content only for mail list page
|
||||
// Thus, we check if the current content is MailAppShell
|
||||
|
||||
@@ -164,6 +172,23 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow,
|
||||
ShellTitleBar.Content = basePage.ShellContent;
|
||||
}
|
||||
|
||||
private async Task StartCalendarReminderServerAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var reminderServer = WinoApplication.Current.Services.GetService<ICalendarReminderServer>();
|
||||
if (reminderServer != null)
|
||||
{
|
||||
await reminderServer.StartAsync();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_calendarReminderServerStartAttempted = false;
|
||||
Serilog.Log.Error(ex, "Failed to start calendar reminder server.");
|
||||
}
|
||||
}
|
||||
|
||||
private void PaneButtonClicked(Microsoft.UI.Xaml.Controls.TitleBar sender, object args)
|
||||
{
|
||||
PreferencesService.IsNavigationPaneOpened = !PreferencesService.IsNavigationPaneOpened;
|
||||
|
||||
@@ -29,4 +29,14 @@
|
||||
Icon="Attachment" />
|
||||
</Viewbox>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- Calendar invitation -->
|
||||
<DataTemplate x:Key="CalendarInvitationSymbolControlTemplate">
|
||||
<Viewbox
|
||||
Width="14"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Bottom">
|
||||
<controls:WinoFontIcon Margin="2,0,0,0" Icon="Calendar" />
|
||||
</Viewbox>
|
||||
</DataTemplate>
|
||||
</ResourceDictionary>
|
||||
|
||||
Reference in New Issue
Block a user