Restore dual mail and calendar app entries
This commit is contained in:
@@ -147,6 +147,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
|
||||
private readonly ICalendarService _calendarService;
|
||||
private readonly INavigationService _navigationService;
|
||||
private readonly INativeAppService _nativeAppService;
|
||||
private readonly INotificationBuilder _notificationBuilder;
|
||||
private readonly IPreferencesService _preferencesService;
|
||||
private readonly IWinoRequestDelegator _winoRequestDelegator;
|
||||
private readonly IMailDialogService _dialogService;
|
||||
@@ -157,6 +158,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
|
||||
private bool _subscriptionsAttached;
|
||||
private CancellationTokenSource _pageLifetimeCts = new();
|
||||
private long _pageLifetimeVersion;
|
||||
private bool _isCalendarBadgeClearedForPageLifetime;
|
||||
private Dictionary<Guid, CalendarItemViewModel> _loadedCalendarItems = new();
|
||||
|
||||
[ObservableProperty]
|
||||
@@ -172,6 +174,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
|
||||
IKeyPressService keyPressService,
|
||||
INativeAppService nativeAppService,
|
||||
IAccountCalendarStateService accountCalendarStateService,
|
||||
INotificationBuilder notificationBuilder,
|
||||
IPreferencesService preferencesService,
|
||||
IWinoRequestDelegator winoRequestDelegator,
|
||||
IMailDialogService dialogService,
|
||||
@@ -183,6 +186,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
|
||||
_calendarService = calendarService;
|
||||
_navigationService = navigationService;
|
||||
_nativeAppService = nativeAppService;
|
||||
_notificationBuilder = notificationBuilder;
|
||||
_preferencesService = preferencesService;
|
||||
_winoRequestDelegator = winoRequestDelegator;
|
||||
_dialogService = dialogService;
|
||||
@@ -360,6 +364,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
|
||||
{
|
||||
CancelPendingOperations();
|
||||
_pageLifetimeCts = new CancellationTokenSource();
|
||||
_isCalendarBadgeClearedForPageLifetime = false;
|
||||
Interlocked.Increment(ref _pageLifetimeVersion);
|
||||
}
|
||||
|
||||
@@ -614,6 +619,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
|
||||
{
|
||||
var lifetimeVersion = CurrentPageLifetimeVersion;
|
||||
var hasLoadingLock = await WaitForCalendarLoadingLockAsync(lifetimeVersion).ConfigureAwait(false);
|
||||
var loadSucceeded = false;
|
||||
|
||||
if (!hasLoadingLock)
|
||||
return;
|
||||
@@ -666,6 +672,8 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
|
||||
DisplayDetailsCalendarItemViewModel = null;
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
loadSucceeded = true;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -685,6 +693,12 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
|
||||
ReleaseCalendarLoadingLock();
|
||||
await ExecuteUIThreadIfActiveAsync(lifetimeVersion, () => IsCalendarEnabled = true).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (loadSucceeded && !_isCalendarBadgeClearedForPageLifetime && IsPageActive(lifetimeVersion))
|
||||
{
|
||||
await _notificationBuilder.ClearCalendarTaskbarBadgeAsync().ConfigureAwait(false);
|
||||
_isCalendarBadgeClearedForPageLifetime = true;
|
||||
}
|
||||
}
|
||||
|
||||
public Task ReloadCurrentVisibleRangeAsync()
|
||||
@@ -692,6 +706,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
|
||||
if (CurrentVisibleRange == null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
RefreshSettings();
|
||||
return ApplyDisplayRequestAsync(new CalendarDisplayRequest(CurrentVisibleRange.DisplayType, CurrentVisibleRange.AnchorDate), forceReload: true);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,16 @@ public interface INotificationBuilder
|
||||
/// <returns></returns>
|
||||
Task UpdateTaskbarIconBadgeAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Adds to the calendar app-entry badge count for newly downloaded events.
|
||||
/// </summary>
|
||||
Task AddCalendarTaskbarBadgeCountAsync(int newlyDownloadedCount);
|
||||
|
||||
/// <summary>
|
||||
/// Clears the calendar app-entry badge.
|
||||
/// </summary>
|
||||
Task ClearCalendarTaskbarBadgeAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Removes the toast notification for a specific mail by unique id.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
using FluentAssertions;
|
||||
using Wino.Core.Activation;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Xunit;
|
||||
|
||||
namespace Wino.Core.Tests;
|
||||
|
||||
public class AppModeActivationResolverTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("--wino-mail", WinoApplicationMode.Calendar, WinoApplicationMode.Mail)]
|
||||
[InlineData("--wino-calendar", WinoApplicationMode.Mail, WinoApplicationMode.Calendar)]
|
||||
[InlineData("--mode=mail", WinoApplicationMode.Calendar, WinoApplicationMode.Mail)]
|
||||
[InlineData("--mode=calendar", WinoApplicationMode.Mail, WinoApplicationMode.Calendar)]
|
||||
[InlineData("CalendarApp", WinoApplicationMode.Mail, WinoApplicationMode.Calendar)]
|
||||
[InlineData("App", WinoApplicationMode.Calendar, WinoApplicationMode.Mail)]
|
||||
public void Resolve_PrefersKnownMailCalendarSignals(string source, WinoApplicationMode defaultMode, WinoApplicationMode expectedMode)
|
||||
{
|
||||
var resolvedMode = AppModeActivationResolver.Resolve(source, null, null, defaultMode);
|
||||
|
||||
resolvedMode.Should().Be(expectedMode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_ToggleDefaultArgumentFlipsBetweenMailAndCalendar()
|
||||
{
|
||||
AppModeActivationResolver.Resolve("--mode=toggle-default", null, null, WinoApplicationMode.Mail)
|
||||
.Should().Be(WinoApplicationMode.Calendar);
|
||||
|
||||
AppModeActivationResolver.Resolve("--mode=toggle-default", null, null, WinoApplicationMode.Calendar)
|
||||
.Should().Be(WinoApplicationMode.Mail);
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ public class CalendarPageViewModelTests
|
||||
viewModel.CurrentVisibleRange.EndDate.Should().Be(new DateOnly(2026, 3, 22));
|
||||
viewModel.LoadedDateWindow.StartDate.Should().Be(new DateTime(2026, 3, 9));
|
||||
viewModel.LoadedDateWindow.EndDate.Should().Be(new DateTime(2026, 3, 30));
|
||||
viewModel.VisibleDateRangeText.Should().Be("March 16 - March 22");
|
||||
viewModel.VisibleDateRangeText.Should().Be("March 16 - 22");
|
||||
|
||||
requestedPeriod.Should().NotBeNull();
|
||||
requestedPeriod!.Start.Should().Be(new DateTime(2026, 3, 9));
|
||||
@@ -242,6 +242,45 @@ public class CalendarPageViewModelTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApplyDisplayRequestAsync_ClearsCalendarBadgeOnlyOncePerPageLifetime()
|
||||
{
|
||||
var settings = CreateSettings();
|
||||
var preferencesService = CreatePreferencesService(settings);
|
||||
var calendarService = new Mock<ICalendarService>();
|
||||
var notificationBuilder = new Mock<INotificationBuilder>();
|
||||
|
||||
calendarService
|
||||
.Setup(service => service.GetCalendarEventsAsync(It.IsAny<IAccountCalendar>(), It.IsAny<ITimePeriod>()))
|
||||
.ReturnsAsync([]);
|
||||
|
||||
notificationBuilder
|
||||
.Setup(builder => builder.ClearCalendarTaskbarBadgeAsync())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var viewModel = CreateViewModel(
|
||||
calendarService.Object,
|
||||
preferencesService.Object,
|
||||
new DateOnly(2026, 3, 20),
|
||||
notificationBuilder: notificationBuilder.Object);
|
||||
|
||||
viewModel.OnNavigatedTo(NavigationMode.New, null!);
|
||||
|
||||
try
|
||||
{
|
||||
var request = new CalendarDisplayRequest(CalendarDisplayType.Day, new DateOnly(2026, 3, 20));
|
||||
|
||||
await viewModel.ApplyDisplayRequestAsync(request);
|
||||
await viewModel.ApplyDisplayRequestAsync(request, forceReload: true);
|
||||
|
||||
notificationBuilder.Verify(builder => builder.ClearCalendarTaskbarBadgeAsync(), Times.Once);
|
||||
}
|
||||
finally
|
||||
{
|
||||
viewModel.OnNavigatedFrom(NavigationMode.Back, null!);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CalendarItemAddedMessage_ReconcilesTrackedLocalPreviewInPlace()
|
||||
{
|
||||
@@ -332,6 +371,15 @@ public class CalendarPageViewModelTests
|
||||
ICalendarService calendarService,
|
||||
IPreferencesService preferencesService,
|
||||
DateOnly today)
|
||||
{
|
||||
return CreateViewModel(calendarService, preferencesService, today, notificationBuilder: null);
|
||||
}
|
||||
|
||||
private static CalendarPageViewModel CreateViewModel(
|
||||
ICalendarService calendarService,
|
||||
IPreferencesService preferencesService,
|
||||
DateOnly today,
|
||||
INotificationBuilder? notificationBuilder)
|
||||
{
|
||||
var account = CreateAccount();
|
||||
|
||||
@@ -339,7 +387,7 @@ public class CalendarPageViewModelTests
|
||||
var accountCalendarViewModel = new AccountCalendarViewModel(account, calendar);
|
||||
var accountCalendarStateService = new FakeAccountCalendarStateService([accountCalendarViewModel]);
|
||||
|
||||
return CreateViewModel(calendarService, preferencesService, today, accountCalendarStateService);
|
||||
return CreateViewModel(calendarService, preferencesService, today, accountCalendarStateService, notificationBuilder: notificationBuilder);
|
||||
}
|
||||
|
||||
private static CalendarPageViewModel CreateViewModel(
|
||||
@@ -356,6 +404,7 @@ public class CalendarPageViewModelTests
|
||||
IAccountCalendarStateService accountCalendarStateService,
|
||||
INavigationService? navigationService = null,
|
||||
INativeAppService? nativeAppService = null,
|
||||
INotificationBuilder? notificationBuilder = null,
|
||||
IWinoRequestDelegator? requestDelegator = null,
|
||||
IMailDialogService? dialogService = null)
|
||||
{
|
||||
@@ -371,6 +420,7 @@ public class CalendarPageViewModelTests
|
||||
Mock.Of<IKeyPressService>(),
|
||||
nativeAppService ?? Mock.Of<INativeAppService>(),
|
||||
accountCalendarStateService,
|
||||
notificationBuilder ?? Mock.Of<INotificationBuilder>(),
|
||||
preferencesService,
|
||||
requestDelegator ?? Mock.Of<IWinoRequestDelegator>(),
|
||||
dialogService ?? Mock.Of<IMailDialogService>(),
|
||||
|
||||
@@ -159,7 +159,7 @@ public class CalendarRangeResolverTests
|
||||
|
||||
var text = formatter.Format(range, new TestDateContextProvider("de-DE", today: new DateOnly(2026, 3, 20)));
|
||||
|
||||
text.Should().Be("16. März - 22. März");
|
||||
text.Should().Be("16. März - 22");
|
||||
}
|
||||
|
||||
private static CalendarSettings CreateSettings(
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Core.Activation;
|
||||
|
||||
public static class AppModeActivationResolver
|
||||
{
|
||||
public static WinoApplicationMode Resolve(string? launchArguments, string? tileId, string? appId, WinoApplicationMode defaultMode = WinoApplicationMode.Mail)
|
||||
{
|
||||
if (TryResolveFromText(launchArguments, defaultMode, out var mode))
|
||||
return mode;
|
||||
|
||||
if (TryResolveFromText(tileId, defaultMode, out mode))
|
||||
return mode;
|
||||
|
||||
if (TryResolveFromText(appId, defaultMode, out mode))
|
||||
return mode;
|
||||
|
||||
return defaultMode;
|
||||
}
|
||||
|
||||
private static bool TryResolveFromText(string? value, WinoApplicationMode defaultMode, out WinoApplicationMode mode)
|
||||
{
|
||||
mode = defaultMode;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return false;
|
||||
|
||||
if (Contains(value, "--mode=toggle-default") ||
|
||||
Contains(value, "mode=toggle-default"))
|
||||
{
|
||||
mode = GetOpposite(defaultMode);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Contains(value, "--wino-calendar") ||
|
||||
Contains(value, "wino-calendar") ||
|
||||
Contains(value, "--mode=calendar") ||
|
||||
Contains(value, "mode=calendar") ||
|
||||
Contains(value, "calendarapp") ||
|
||||
EqualsToken(value, "calendar"))
|
||||
{
|
||||
mode = WinoApplicationMode.Calendar;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Contains(value, "wino-contacts") ||
|
||||
Contains(value, "--mode=contacts") ||
|
||||
Contains(value, "mode=contacts") ||
|
||||
Contains(value, "contactsapp") ||
|
||||
EqualsToken(value, "contacts"))
|
||||
{
|
||||
mode = WinoApplicationMode.Contacts;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Contains(value, "wino-settings") ||
|
||||
Contains(value, "--mode=settings") ||
|
||||
Contains(value, "mode=settings") ||
|
||||
Contains(value, "settingsapp") ||
|
||||
EqualsToken(value, "settings"))
|
||||
{
|
||||
mode = WinoApplicationMode.Settings;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Contains(value, "--wino-mail") ||
|
||||
Contains(value, "wino-mail") ||
|
||||
Contains(value, "--mode=mail") ||
|
||||
Contains(value, "mode=mail") ||
|
||||
Contains(value, "!app") ||
|
||||
Contains(value, "mailapp") ||
|
||||
EqualsToken(value, "app") ||
|
||||
EqualsToken(value, "mail"))
|
||||
{
|
||||
mode = WinoApplicationMode.Mail;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool Contains(string source, string token)
|
||||
=> source.Contains(token, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool EqualsToken(string source, string token)
|
||||
=> string.Equals(source.Trim(), token, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static WinoApplicationMode GetOpposite(WinoApplicationMode defaultMode)
|
||||
=> defaultMode switch
|
||||
{
|
||||
WinoApplicationMode.Mail => WinoApplicationMode.Calendar,
|
||||
WinoApplicationMode.Calendar => WinoApplicationMode.Mail,
|
||||
_ => WinoApplicationMode.Mail
|
||||
};
|
||||
}
|
||||
@@ -465,13 +465,15 @@ public class SynchronizationManager : ISynchronizationManager
|
||||
try
|
||||
{
|
||||
var result = await synchronizer.SynchronizeCalendarEventsAsync(options, linkedCancellationTokenSource.Token);
|
||||
var downloadedEventCount = result.DownloadedEvents?.Count() ?? 0;
|
||||
|
||||
_logger.Information("Calendar synchronization completed for account {AccountId} with state {State}",
|
||||
options.AccountId, result.CompletedState);
|
||||
|
||||
// TODO: Create notifications for new calendar events when INotificationBuilder supports it
|
||||
// if (result.DownloadedEvents?.Any() ?? false)
|
||||
// await _notificationBuilder.CreateCalendarNotificationsAsync(result.DownloadedEvents);
|
||||
if (downloadedEventCount > 0)
|
||||
{
|
||||
await _notificationBuilder.AddCalendarTaskbarBadgeCountAsync(downloadedEventCount).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
using Windows.ApplicationModel;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Mail.WinUI.Activation;
|
||||
|
||||
internal static class AppEntryConstants
|
||||
{
|
||||
public const string MailApplicationId = "App";
|
||||
public const string CalendarApplicationId = "CalendarApp";
|
||||
public const string MailLaunchArgument = "--wino-mail";
|
||||
public const string CalendarLaunchArgument = "--wino-calendar";
|
||||
|
||||
public static string GetModeLaunchArgument(WinoApplicationMode mode)
|
||||
=> mode switch
|
||||
{
|
||||
WinoApplicationMode.Calendar => CalendarLaunchArgument,
|
||||
WinoApplicationMode.Contacts => "--mode=contacts",
|
||||
WinoApplicationMode.Settings => "--mode=settings",
|
||||
_ => MailLaunchArgument
|
||||
};
|
||||
|
||||
public static string? GetPackagedApplicationId(WinoApplicationMode mode)
|
||||
=> mode switch
|
||||
{
|
||||
WinoApplicationMode.Calendar => CalendarApplicationId,
|
||||
WinoApplicationMode.Mail => MailApplicationId,
|
||||
_ => null
|
||||
};
|
||||
|
||||
public static string GetAppUserModelId(string packageFamilyName, WinoApplicationMode mode)
|
||||
=> $"{packageFamilyName}!{GetPackagedApplicationId(mode) ?? MailApplicationId}";
|
||||
|
||||
public static string GetAppUserModelId(WinoApplicationMode mode)
|
||||
=> GetAppUserModelId(Package.Current.Id.FamilyName, mode);
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Mail.WinUI.Activation;
|
||||
@@ -6,87 +5,5 @@ namespace Wino.Mail.WinUI.Activation;
|
||||
internal static class AppModeActivationResolver
|
||||
{
|
||||
public static WinoApplicationMode Resolve(string? launchArguments, string? tileId, string? appId, WinoApplicationMode defaultMode = WinoApplicationMode.Mail)
|
||||
{
|
||||
if (TryResolveFromText(launchArguments, defaultMode, out var mode))
|
||||
return mode;
|
||||
|
||||
if (TryResolveFromText(tileId, defaultMode, out mode))
|
||||
return mode;
|
||||
|
||||
if (TryResolveFromText(appId, defaultMode, out mode))
|
||||
return mode;
|
||||
|
||||
return defaultMode;
|
||||
}
|
||||
|
||||
private static bool TryResolveFromText(string? value, WinoApplicationMode defaultMode, out WinoApplicationMode mode)
|
||||
{
|
||||
mode = defaultMode;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return false;
|
||||
|
||||
if (Contains(value, "--mode=toggle-default") ||
|
||||
Contains(value, "mode=toggle-default"))
|
||||
{
|
||||
mode = GetOpposite(defaultMode);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Contains(value, "wino-calendar") ||
|
||||
Contains(value, "--mode=calendar") ||
|
||||
Contains(value, "mode=calendar") ||
|
||||
Contains(value, "calendarapp") ||
|
||||
EqualsToken(value, "calendar"))
|
||||
{
|
||||
mode = WinoApplicationMode.Calendar;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Contains(value, "wino-contacts") ||
|
||||
Contains(value, "--mode=contacts") ||
|
||||
Contains(value, "mode=contacts") ||
|
||||
Contains(value, "contactsapp") ||
|
||||
EqualsToken(value, "contacts"))
|
||||
{
|
||||
mode = WinoApplicationMode.Contacts;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Contains(value, "wino-settings") ||
|
||||
Contains(value, "--mode=settings") ||
|
||||
Contains(value, "mode=settings") ||
|
||||
Contains(value, "settingsapp") ||
|
||||
EqualsToken(value, "settings"))
|
||||
{
|
||||
mode = WinoApplicationMode.Settings;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Contains(value, "wino-mail") ||
|
||||
Contains(value, "--mode=mail") ||
|
||||
Contains(value, "mode=mail") ||
|
||||
Contains(value, "mailapp") ||
|
||||
EqualsToken(value, "mail"))
|
||||
{
|
||||
mode = WinoApplicationMode.Mail;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool Contains(string source, string token)
|
||||
=> source.Contains(token, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool EqualsToken(string source, string token)
|
||||
=> string.Equals(source.Trim(), token, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static WinoApplicationMode GetOpposite(WinoApplicationMode defaultMode)
|
||||
=> defaultMode switch
|
||||
{
|
||||
WinoApplicationMode.Mail => WinoApplicationMode.Calendar,
|
||||
WinoApplicationMode.Calendar => WinoApplicationMode.Mail,
|
||||
_ => WinoApplicationMode.Mail
|
||||
};
|
||||
=> Wino.Core.Activation.AppModeActivationResolver.Resolve(launchArguments, tileId, appId, defaultMode);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
using Microsoft.Windows.AppNotifications;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Mail.WinUI.Activation;
|
||||
|
||||
internal static class ToastActivationResolver
|
||||
{
|
||||
public static bool TryParse(string? argument, out NotificationArguments toastArguments)
|
||||
{
|
||||
toastArguments = default!;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(argument))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
var parsedArguments = NotificationArguments.Parse(argument);
|
||||
if (!ContainsKnownToastKey(parsedArguments))
|
||||
return false;
|
||||
|
||||
toastArguments = parsedArguments;
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool ShouldBringToForeground(NotificationArguments toastArguments)
|
||||
{
|
||||
if (toastArguments.TryGetValue(Constants.ToastStoreUpdateActionKey, out string storeUpdateAction) &&
|
||||
storeUpdateAction == Constants.ToastStoreUpdateActionInstall)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (toastArguments.TryGetValue(Constants.ToastCalendarActionKey, out string calendarAction))
|
||||
{
|
||||
return calendarAction == Constants.ToastCalendarNavigateAction;
|
||||
}
|
||||
|
||||
if (toastArguments.TryGetValue(Constants.ToastActionKey, out MailOperation mailAction))
|
||||
{
|
||||
return mailAction == MailOperation.Navigate;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool ContainsKnownToastKey(NotificationArguments toastArguments)
|
||||
=> toastArguments.TryGetValue(Constants.ToastStoreUpdateActionKey, out string _) ||
|
||||
toastArguments.TryGetValue(Constants.ToastCalendarActionKey, out string _) ||
|
||||
toastArguments.TryGetValue(Constants.ToastActionKey, out string _);
|
||||
}
|
||||
+67
-36
@@ -167,19 +167,31 @@ public partial class App : WinoApplication,
|
||||
if (!_hasConfiguredAccounts)
|
||||
return ActivateWelcomeWindowAsync();
|
||||
|
||||
return ActivateShellWindowAsync(_preferencesService?.DefaultApplicationMode);
|
||||
return LaunchEntryOrActivateShellAsync(_preferencesService?.DefaultApplicationMode ?? WinoApplicationMode.Mail);
|
||||
}
|
||||
|
||||
private Task OpenMailFromTrayAsync()
|
||||
=> _hasConfiguredAccounts
|
||||
? ActivateShellWindowAsync(WinoApplicationMode.Mail)
|
||||
? LaunchEntryOrActivateShellAsync(WinoApplicationMode.Mail)
|
||||
: ActivateWelcomeWindowAsync();
|
||||
|
||||
private Task OpenCalendarFromTrayAsync()
|
||||
=> _hasConfiguredAccounts
|
||||
? ActivateShellWindowAsync(WinoApplicationMode.Calendar)
|
||||
? LaunchEntryOrActivateShellAsync(WinoApplicationMode.Calendar)
|
||||
: ActivateWelcomeWindowAsync();
|
||||
|
||||
private async Task LaunchEntryOrActivateShellAsync(WinoApplicationMode mode)
|
||||
{
|
||||
if (AppEntryConstants.GetPackagedApplicationId(mode) != null)
|
||||
{
|
||||
var appEntryLauncher = Services.GetRequiredService<PackagedAppEntryLauncher>();
|
||||
if (await appEntryLauncher.LaunchAsync(mode))
|
||||
return;
|
||||
}
|
||||
|
||||
await ActivateShellWindowAsync(mode);
|
||||
}
|
||||
|
||||
private async Task ActivateWelcomeWindowAsync()
|
||||
{
|
||||
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
|
||||
@@ -218,7 +230,7 @@ public partial class App : WinoApplication,
|
||||
return;
|
||||
|
||||
if (mode.HasValue)
|
||||
shellWindow.HandleAppActivation(GetModeLaunchArgument(mode.Value));
|
||||
shellWindow.HandleAppActivation(AppEntryConstants.GetModeLaunchArgument(mode.Value));
|
||||
|
||||
CloseWelcomeWindowIfPresent();
|
||||
await ActivateWindowAsync((WindowEx)shellWindow);
|
||||
@@ -455,7 +467,15 @@ public partial class App : WinoApplication,
|
||||
TryMarkInitialNotificationActivationHandled())
|
||||
{
|
||||
LogActivation($"Processing notification activation from OnLaunched. Arguments: {toastArgs.Argument}");
|
||||
await HandleToastActivationAsync(toastArgs);
|
||||
await HandleToastActivationAsync(toastArgs.Argument, toastArgs.UserInput);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ToastActivationResolver.TryParse(args.Arguments, out var launchToastArguments) &&
|
||||
TryMarkInitialNotificationActivationHandled())
|
||||
{
|
||||
LogActivation($"Processing toast launch activation from OnLaunched. Arguments: {args.Arguments}");
|
||||
await HandleToastActivationAsync(launchToastArguments);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -512,18 +532,18 @@ public partial class App : WinoApplication,
|
||||
LogActivation($"Processing initial notification activation from application startup. Arguments: {toastArgs.Argument}");
|
||||
|
||||
await EnsureActivationInfrastructureAsync();
|
||||
await HandleToastActivationAsync(toastArgs);
|
||||
await HandleToastActivationAsync(toastArgs.Argument, toastArgs.UserInput);
|
||||
}
|
||||
|
||||
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)
|
||||
if (MainWindow?.DispatcherQueue?.TryEnqueue(() => _ = HandleToastActivationAsync(args.Argument, args.UserInput)) == true)
|
||||
return;
|
||||
|
||||
LogActivation($"Processing notification activation from NotificationInvoked. Arguments: {args.Argument}");
|
||||
_ = HandleToastActivationAsync(args);
|
||||
_ = HandleToastActivationAsync(args.Argument, args.UserInput);
|
||||
}
|
||||
|
||||
private void TryRegisterAppNotifications()
|
||||
@@ -546,11 +566,9 @@ public partial class App : WinoApplication,
|
||||
/// <summary>
|
||||
/// Handles toast notification activation scenarios.
|
||||
/// </summary>
|
||||
private async Task HandleToastActivationAsync(AppNotificationActivatedEventArgs toastArgs)
|
||||
private async Task HandleToastActivationAsync(NotificationArguments toastArguments, IDictionary<string, string>? userInput = null)
|
||||
{
|
||||
LogActivation($"Handling app notification activation. Arguments: {toastArgs.Argument}");
|
||||
|
||||
var toastArguments = NotificationArguments.Parse(toastArgs.Argument);
|
||||
LogActivation("Handling app notification activation.");
|
||||
|
||||
if (toastArguments.TryGetValue(Constants.ToastStoreUpdateActionKey, out string storeUpdateAction) &&
|
||||
storeUpdateAction == Constants.ToastStoreUpdateActionInstall)
|
||||
@@ -572,7 +590,7 @@ public partial class App : WinoApplication,
|
||||
|
||||
if (calendarAction == Constants.ToastCalendarSnoozeAction)
|
||||
{
|
||||
await HandleCalendarToastSnoozeAsync(toastArgs, calendarItemId);
|
||||
await HandleCalendarToastSnoozeAsync(userInput, calendarItemId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -600,6 +618,17 @@ public partial class App : WinoApplication,
|
||||
LogActivation("App notification activation did not match any known handler.");
|
||||
}
|
||||
|
||||
private Task HandleToastActivationAsync(string toastArgument, IDictionary<string, string>? userInput = null)
|
||||
{
|
||||
if (!ToastActivationResolver.TryParse(toastArgument, out var toastArguments))
|
||||
{
|
||||
LogActivation($"Ignoring non-toast launch argument: {toastArgument}");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return HandleToastActivationAsync(toastArguments, userInput);
|
||||
}
|
||||
|
||||
private async Task<IWinoShellWindow?> EnsureShellWindowAsync(WinoApplicationMode mode, bool activateWindow, bool suppressStartupFlows = true)
|
||||
{
|
||||
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
|
||||
@@ -612,7 +641,7 @@ public partial class App : WinoApplication,
|
||||
|
||||
CreateWindow(
|
||||
null,
|
||||
GetModeLaunchArgument(mode),
|
||||
AppEntryConstants.GetModeLaunchArgument(mode),
|
||||
new ShellModeActivationContext
|
||||
{
|
||||
SuppressStartupFlows = suppressStartupFlows
|
||||
@@ -671,9 +700,9 @@ public partial class App : WinoApplication,
|
||||
navigationService.Navigate(WinoPage.EventDetailsPage, target);
|
||||
}
|
||||
|
||||
private async Task HandleCalendarToastSnoozeAsync(AppNotificationActivatedEventArgs toastArgs, Guid calendarItemId)
|
||||
private async Task HandleCalendarToastSnoozeAsync(IDictionary<string, string>? userInput, Guid calendarItemId)
|
||||
{
|
||||
if (!TryGetSnoozeDurationMinutes(toastArgs, out var snoozeDurationMinutes))
|
||||
if (!TryGetSnoozeDurationMinutes(userInput, out var snoozeDurationMinutes))
|
||||
return;
|
||||
|
||||
var calendarService = Services.GetRequiredService<ICalendarService>();
|
||||
@@ -682,15 +711,15 @@ public partial class App : WinoApplication,
|
||||
await calendarService.SnoozeCalendarItemAsync(calendarItemId, snoozedUntilLocal);
|
||||
}
|
||||
|
||||
private static bool TryGetSnoozeDurationMinutes(AppNotificationActivatedEventArgs toastArgs, out int snoozeDurationMinutes)
|
||||
private bool TryGetSnoozeDurationMinutes(IDictionary<string, string>? userInput, out int snoozeDurationMinutes)
|
||||
{
|
||||
snoozeDurationMinutes = 0;
|
||||
snoozeDurationMinutes = _preferencesService?.DefaultSnoozeDurationInMinutes ?? 0;
|
||||
|
||||
if (toastArgs.UserInput == null ||
|
||||
!toastArgs.UserInput.TryGetValue(Constants.ToastCalendarSnoozeDurationInputId, out var selectedValue) ||
|
||||
if (userInput == null ||
|
||||
!userInput.TryGetValue(Constants.ToastCalendarSnoozeDurationInputId, out var selectedValue) ||
|
||||
selectedValue == null)
|
||||
{
|
||||
return false;
|
||||
return snoozeDurationMinutes > 0;
|
||||
}
|
||||
|
||||
var selectedText = selectedValue.ToString();
|
||||
@@ -910,7 +939,7 @@ public partial class App : WinoApplication,
|
||||
|
||||
if (TryResolveActivationMode(activationArgs, defaultMode, out var activationMode))
|
||||
{
|
||||
shellWindow.HandleAppActivation(GetModeLaunchArgument(activationMode));
|
||||
shellWindow.HandleAppActivation(AppEntryConstants.GetModeLaunchArgument(activationMode));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1049,7 +1078,7 @@ public partial class App : WinoApplication,
|
||||
MainWindow?.DispatcherQueue?.TryEnqueue(async () =>
|
||||
{
|
||||
// Create and activate ShellWindow — ActiveWindowChanged fires and rebinds the dispatcher.
|
||||
CreateWindow(null, GetModeLaunchArgument(WinoApplicationMode.Mail));
|
||||
CreateWindow(null, AppEntryConstants.GetModeLaunchArgument(WinoApplicationMode.Mail));
|
||||
CloseWelcomeWindowIfPresent();
|
||||
if (MainWindow != null)
|
||||
await ActivateWindowAsync(MainWindow);
|
||||
@@ -1085,7 +1114,7 @@ public partial class App : WinoApplication,
|
||||
|
||||
CreateWindow(
|
||||
null,
|
||||
GetModeLaunchArgument(WinoApplicationMode.Mail),
|
||||
AppEntryConstants.GetModeLaunchArgument(WinoApplicationMode.Mail),
|
||||
new ShellModeActivationContext
|
||||
{
|
||||
SuppressStartupFlows = true
|
||||
@@ -1394,14 +1423,24 @@ public partial class App : WinoApplication,
|
||||
// Handle toast notification activation
|
||||
var toastArgs = (AppNotificationActivatedEventArgs)args.Data;
|
||||
LogActivation($"Processing redirected notification activation. Arguments: {toastArgs.Argument}");
|
||||
_ = HandleToastActivationAsync(toastArgs);
|
||||
_ = HandleToastActivationAsync(toastArgs.Argument, toastArgs.UserInput);
|
||||
}
|
||||
else
|
||||
{
|
||||
var shouldActivateWindow = true;
|
||||
|
||||
if (MainWindow is IWinoShellWindow shellWindow)
|
||||
{
|
||||
if (args.Kind == ExtendedActivationKind.Launch &&
|
||||
args.Data is ILaunchActivatedEventArgs launchArgs)
|
||||
{
|
||||
if (ToastActivationResolver.TryParse(launchArgs.Arguments, out var launchToastArguments))
|
||||
{
|
||||
shouldActivateWindow = ToastActivationResolver.ShouldBringToForeground(launchToastArguments);
|
||||
LogActivation($"Processing redirected toast launch activation. Arguments: {launchArgs.Arguments}");
|
||||
_ = HandleToastActivationAsync(launchToastArguments);
|
||||
}
|
||||
else
|
||||
{
|
||||
var launchArguments = launchArgs.Arguments;
|
||||
|
||||
@@ -1412,15 +1451,16 @@ public partial class App : WinoApplication,
|
||||
|
||||
shellWindow.HandleAppActivation(launchArguments, launchArgs.TileId);
|
||||
}
|
||||
}
|
||||
else if (TryResolveActivationMode(args, _preferencesService?.DefaultApplicationMode ?? WinoApplicationMode.Mail, out var redirectedMode))
|
||||
{
|
||||
shellWindow.HandleAppActivation(GetModeLaunchArgument(redirectedMode));
|
||||
shellWindow.HandleAppActivation(AppEntryConstants.GetModeLaunchArgument(redirectedMode));
|
||||
}
|
||||
}
|
||||
|
||||
// Redirected launches can target a shell window that is currently hidden in the tray.
|
||||
// Restore it through the window manager so Show/BringToFront/Activate happen together.
|
||||
if (MainWindow is WindowEx mainWindow)
|
||||
if (shouldActivateWindow && MainWindow is WindowEx mainWindow)
|
||||
{
|
||||
Services.GetRequiredService<IWinoWindowManager>().ActivateWindow(mainWindow);
|
||||
}
|
||||
@@ -1434,15 +1474,6 @@ public partial class App : WinoApplication,
|
||||
_ = HandleRedirectedActivationAsync();
|
||||
}
|
||||
|
||||
private static string GetModeLaunchArgument(WinoApplicationMode mode)
|
||||
=> mode switch
|
||||
{
|
||||
WinoApplicationMode.Calendar => "--mode=calendar",
|
||||
WinoApplicationMode.Contacts => "--mode=contacts",
|
||||
WinoApplicationMode.Settings => "--mode=settings",
|
||||
_ => "--mode=mail"
|
||||
};
|
||||
|
||||
private static string AppendLaunchArgument(string? launchArguments, string launchArgument)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(launchArguments)
|
||||
|
||||
@@ -36,6 +36,7 @@ public static class CoreUWPContainerSetup
|
||||
services.AddTransient<IWebView2RuntimeValidatorService, WebView2RuntimeValidatorService>();
|
||||
services.AddTransient<INotificationBuilder, NotificationBuilder>();
|
||||
services.AddSingleton<ICalendarReminderServer, CalendarReminderServer>();
|
||||
services.AddSingleton<PackagedAppEntryLauncher>();
|
||||
services.AddTransient<IClipboardService, ClipboardService>();
|
||||
services.AddTransient<IStartupBehaviorService, StartupBehaviorService>();
|
||||
services.AddSingleton<IPrintService, PrintService>();
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using WinUIEx;
|
||||
|
||||
namespace Wino.Mail.WinUI.Helpers;
|
||||
|
||||
internal static class WindowAppUserModelIdHelper
|
||||
{
|
||||
private static readonly Guid PropertyStoreGuid = new("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99");
|
||||
private static readonly PropertyKey AppUserModelIdPropertyKey = new(new Guid("9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3"), 5);
|
||||
|
||||
public static void TrySet(WindowEx window, string appUserModelId)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(window);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(appUserModelId))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(window);
|
||||
if (hwnd == IntPtr.Zero)
|
||||
return;
|
||||
|
||||
var propertyStoreGuid = PropertyStoreGuid;
|
||||
var appUserModelIdPropertyKey = AppUserModelIdPropertyKey;
|
||||
var hr = SHGetPropertyStoreForWindow(hwnd, ref propertyStoreGuid, out var propertyStore);
|
||||
if (hr < 0 || propertyStore == null)
|
||||
return;
|
||||
|
||||
using (propertyStore)
|
||||
{
|
||||
using var value = PropVariant.FromString(appUserModelId);
|
||||
propertyStore.SetValue(ref appUserModelIdPropertyKey, value);
|
||||
propertyStore.Commit();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best effort only. Some Windows builds may keep the original taskbar identity.
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("shell32.dll")]
|
||||
private static extern int SHGetPropertyStoreForWindow(
|
||||
IntPtr hwnd,
|
||||
ref Guid riid,
|
||||
[MarshalAs(UnmanagedType.Interface)] out IPropertyStore propertyStore);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
private readonly struct PropertyKey(Guid fmtid, uint pid)
|
||||
{
|
||||
public Guid FormatId { get; } = fmtid;
|
||||
public uint PropertyId { get; } = pid;
|
||||
}
|
||||
|
||||
[ComImport]
|
||||
[Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
private interface IPropertyStore : IDisposable
|
||||
{
|
||||
uint GetCount();
|
||||
void GetAt(uint propertyIndex, out PropertyKey key);
|
||||
void GetValue(ref PropertyKey key, out PropVariant pv);
|
||||
void SetValue(ref PropertyKey key, PropVariant pv);
|
||||
void Commit();
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
private sealed class PropVariant : IDisposable
|
||||
{
|
||||
[FieldOffset(0)]
|
||||
private ushort _valueType;
|
||||
|
||||
[FieldOffset(8)]
|
||||
private IntPtr _pointerValue;
|
||||
|
||||
private PropVariant(string value)
|
||||
{
|
||||
_valueType = 31;
|
||||
_pointerValue = Marshal.StringToCoTaskMemUni(value);
|
||||
}
|
||||
|
||||
public static PropVariant FromString(string value) => new(value);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
PropVariantClear(this);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
~PropVariant()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
|
||||
[DllImport("ole32.dll")]
|
||||
private static extern int PropVariantClear([In, Out] PropVariant propvar);
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@
|
||||
<Application Id="App"
|
||||
Executable="$targetnametoken$.exe"
|
||||
EntryPoint="$targetentrypoint$"
|
||||
uap10:Parameters="--mode=mail">
|
||||
uap10:Parameters="--wino-mail">
|
||||
<uap:VisualElements
|
||||
DisplayName="Wino Mail"
|
||||
Description="Wino.Mail.WinUI"
|
||||
@@ -97,7 +97,45 @@
|
||||
</uap:Protocol>
|
||||
</uap:Extension>
|
||||
|
||||
<!-- Protocol activation: webcal -->
|
||||
<!-- File Assosication: EML -->
|
||||
<uap:Extension Category="windows.fileTypeAssociation">
|
||||
<uap:FileTypeAssociation Name="eml">
|
||||
<uap:Logo>EML\eml.png</uap:Logo>
|
||||
<uap:SupportedFileTypes>
|
||||
<uap:FileType>.eml</uap:FileType>
|
||||
</uap:SupportedFileTypes>
|
||||
</uap:FileTypeAssociation>
|
||||
</uap:Extension>
|
||||
</Extensions>
|
||||
</Application>
|
||||
|
||||
<Application Id="CalendarApp"
|
||||
Executable="$targetnametoken$.exe"
|
||||
EntryPoint="$targetentrypoint$"
|
||||
uap10:Parameters="--wino-calendar">
|
||||
<uap:VisualElements
|
||||
DisplayName="Wino Calendar"
|
||||
Description="Wino.Mail.WinUI.Calendar"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Assets\AppEntries\CalendarAssets\Square150x150Logo.png"
|
||||
Square44x44Logo="Assets\AppEntries\CalendarAssets\Square44x44Logo.png">
|
||||
<uap:DefaultTile Wide310x150Logo="Assets\AppEntries\CalendarAssets\Wide310x150Logo.png" Square71x71Logo="Assets\AppEntries\CalendarAssets\SmallTile.png" Square310x310Logo="Assets\AppEntries\CalendarAssets\LargeTile.png"/>
|
||||
<uap:SplashScreen Image="Assets\AppEntries\CalendarAssets\SplashScreen.png" />
|
||||
</uap:VisualElements>
|
||||
|
||||
<Extensions>
|
||||
<desktop:Extension Category="windows.toastNotificationActivation">
|
||||
<desktop:ToastNotificationActivation ToastActivatorCLSID="44c05d2b-aa1d-4e59-9d7d-8b4c8607cb8d" />
|
||||
</desktop:Extension>
|
||||
|
||||
<com:Extension Category="windows.comServer">
|
||||
<com:ComServer>
|
||||
<com:ExeServer Executable="Wino.Mail.WinUI.exe" Arguments="----AppNotificationActivated:" DisplayName="Calendar toast activator">
|
||||
<com:Class Id="44c05d2b-aa1d-4e59-9d7d-8b4c8607cb8d" DisplayName="Calendar toast activator"/>
|
||||
</com:ExeServer>
|
||||
</com:ComServer>
|
||||
</com:Extension>
|
||||
|
||||
<uap:Extension Category="windows.protocol">
|
||||
<uap:Protocol Name="webcal">
|
||||
<uap:DisplayName>Calendar Protocol</uap:DisplayName>
|
||||
@@ -110,17 +148,6 @@
|
||||
</uap:Protocol>
|
||||
</uap:Extension>
|
||||
|
||||
<!-- File Assosication: EML -->
|
||||
<uap:Extension Category="windows.fileTypeAssociation">
|
||||
<uap:FileTypeAssociation Name="eml">
|
||||
<uap:Logo>EML\eml.png</uap:Logo>
|
||||
<uap:SupportedFileTypes>
|
||||
<uap:FileType>.eml</uap:FileType>
|
||||
</uap:SupportedFileTypes>
|
||||
</uap:FileTypeAssociation>
|
||||
</uap:Extension>
|
||||
|
||||
<!-- File Association: ICS -->
|
||||
<uap:Extension Category="windows.fileTypeAssociation">
|
||||
<uap:FileTypeAssociation Name="ics">
|
||||
<uap:Logo>Assets\AppEntries\CalendarAssets\Square44x44Logo.png</uap:Logo>
|
||||
|
||||
+10
-20
@@ -7,8 +7,7 @@ using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.Windows.AppNotifications;
|
||||
using Microsoft.Windows.AppLifecycle;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Mail.WinUI.Activation;
|
||||
|
||||
namespace Wino.Mail.WinUI;
|
||||
|
||||
@@ -200,28 +199,19 @@ public class Program
|
||||
|
||||
private static bool ShouldBringWindowToForegroundAfterRedirection(AppActivationArguments args)
|
||||
{
|
||||
if (args.Kind != ExtendedActivationKind.AppNotification ||
|
||||
args.Data is not AppNotificationActivatedEventArgs toastArgs)
|
||||
if (args.Kind == ExtendedActivationKind.AppNotification &&
|
||||
args.Data is AppNotificationActivatedEventArgs toastArgs)
|
||||
{
|
||||
return true;
|
||||
return ToastActivationResolver.TryParse(toastArgs.Argument, out var toastArguments)
|
||||
? ToastActivationResolver.ShouldBringToForeground(toastArguments)
|
||||
: true;
|
||||
}
|
||||
|
||||
var toastArguments = NotificationArguments.Parse(toastArgs.Argument);
|
||||
|
||||
if (toastArguments.TryGetValue(Constants.ToastStoreUpdateActionKey, out string storeUpdateAction) &&
|
||||
storeUpdateAction == Constants.ToastStoreUpdateActionInstall)
|
||||
if (args.Kind == ExtendedActivationKind.Launch &&
|
||||
args.Data is Windows.ApplicationModel.Activation.ILaunchActivatedEventArgs launchArgs &&
|
||||
ToastActivationResolver.TryParse(launchArgs.Arguments, out var launchToastArguments))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (toastArguments.TryGetValue(Constants.ToastCalendarActionKey, out string calendarAction))
|
||||
{
|
||||
return calendarAction == Constants.ToastCalendarNavigateAction;
|
||||
}
|
||||
|
||||
if (toastArguments.TryGetValue(Constants.ToastActionKey, out MailOperation mailAction))
|
||||
{
|
||||
return mailAction == MailOperation.Navigate;
|
||||
return ToastActivationResolver.ShouldBringToForeground(launchToastArguments);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -2,12 +2,13 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.Windows.AppNotifications;
|
||||
using Microsoft.Windows.AppNotifications.Builder;
|
||||
using CommunityToolkit.WinUI.Notifications;
|
||||
using Serilog;
|
||||
using Windows.Data.Xml.Dom;
|
||||
using Windows.Storage;
|
||||
using Windows.UI.Notifications;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
@@ -16,14 +17,15 @@ using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Extensions;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Mail.WinUI.Activation;
|
||||
using Wino.Messaging.UI;
|
||||
|
||||
namespace Wino.Mail.WinUI.Services;
|
||||
|
||||
public class NotificationBuilder : INotificationBuilder
|
||||
{
|
||||
private const string MailApplicationId = "App";
|
||||
private const string NotificationIconRootUri = "ms-appx:///Assets/NotificationIcons/";
|
||||
private static int _calendarTaskbarBadgeCount;
|
||||
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly IFolderService _folderService;
|
||||
@@ -53,50 +55,33 @@ public class NotificationBuilder : INotificationBuilder
|
||||
{
|
||||
try
|
||||
{
|
||||
// Filter mails to only include Inbox folder items
|
||||
var inboxMailItems = new List<MailCopy>();
|
||||
|
||||
foreach (var item in downloadedMailItems)
|
||||
{
|
||||
var mailItem = await _mailService.GetSingleMailItemAsync(item.UniqueId);
|
||||
|
||||
//if (mailItem == null || mailItem.AssignedFolder == null)
|
||||
// continue;
|
||||
|
||||
//// Only create notifications for Inbox folder mails
|
||||
//if (mailItem.AssignedFolder.SpecialFolderType != SpecialFolderType.Inbox)
|
||||
// continue;
|
||||
|
||||
//// Skip folders with synchronization disabled
|
||||
//if (!mailItem.AssignedFolder.IsSynchronizationEnabled)
|
||||
// continue;
|
||||
|
||||
//// Skip already read mails
|
||||
//if (mailItem.IsRead)
|
||||
//{
|
||||
// RemoveNotification(mailItem.UniqueId);
|
||||
// continue;
|
||||
//}
|
||||
|
||||
if (mailItem != null)
|
||||
{
|
||||
inboxMailItems.Add(mailItem);
|
||||
}
|
||||
}
|
||||
|
||||
var mailCount = inboxMailItems.Count;
|
||||
|
||||
if (mailCount == 0)
|
||||
return;
|
||||
|
||||
// If there are more than 3 mails, just display 1 general notification.
|
||||
if (mailCount > 3)
|
||||
{
|
||||
var builder = CreateBuilder();
|
||||
var builder = new ToastContentBuilder()
|
||||
.AddText(Translator.Notifications_MultipleNotificationsTitle)
|
||||
.AddText(string.Format(Translator.Notifications_MultipleNotificationsMessage, mailCount))
|
||||
.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail)
|
||||
.AddAudio(new ToastAudio
|
||||
{
|
||||
Src = new Uri("ms-winsoundevent:Notification.Mail")
|
||||
});
|
||||
|
||||
builder.AddText(Translator.Notifications_MultipleNotificationsTitle);
|
||||
builder.AddText(string.Format(Translator.Notifications_MultipleNotificationsMessage, mailCount));
|
||||
builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
||||
builder.SetAudioUri(new Uri("ms-winsoundevent:Notification.Mail"));
|
||||
|
||||
ShowNotification(builder);
|
||||
ShowToast(builder, ToastTargetApp.Mail);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -114,104 +99,28 @@ public class NotificationBuilder : INotificationBuilder
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CreateSingleNotificationAsync(MailCopy mailItem)
|
||||
{
|
||||
var builder = CreateBuilder();
|
||||
|
||||
var avatarThumbnail = await _thumbnailService.GetThumbnailAsync(mailItem.FromAddress, awaitLoad: true);
|
||||
if (!string.IsNullOrEmpty(avatarThumbnail))
|
||||
{
|
||||
var tempFile = await Windows.Storage.ApplicationData.Current.TemporaryFolder.CreateFileAsync($"{Guid.NewGuid()}.png", Windows.Storage.CreationCollisionOption.ReplaceExisting);
|
||||
await using (var stream = await tempFile.OpenStreamForWriteAsync())
|
||||
{
|
||||
var bytes = Convert.FromBase64String(avatarThumbnail);
|
||||
await stream.WriteAsync(bytes);
|
||||
}
|
||||
|
||||
builder.SetAppLogoOverride(new Uri($"ms-appdata:///temp/{tempFile.Name}"), AppNotificationImageCrop.Default);
|
||||
}
|
||||
|
||||
builder.SetTimeStamp(mailItem.CreationDate.ToLocalTime());
|
||||
builder.AddText(mailItem.FromName);
|
||||
builder.AddText(mailItem.Subject);
|
||||
builder.AddText(mailItem.PreviewText);
|
||||
|
||||
builder.AddArgument(Constants.ToastMailUniqueIdKey, mailItem.UniqueId.ToString());
|
||||
builder.AddArgument(Constants.ToastActionKey, MailOperation.Navigate.ToString());
|
||||
builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
||||
|
||||
builder.AddButton(GetMarkAsReadButton(mailItem.UniqueId));
|
||||
builder.AddButton(GetDeleteButton(mailItem.UniqueId));
|
||||
builder.AddButton(GetArchiveButton(mailItem.UniqueId));
|
||||
builder.SetAudioUri(new Uri("ms-winsoundevent:Notification.Mail"));
|
||||
|
||||
ShowNotification(builder, mailItem.UniqueId.ToString());
|
||||
}
|
||||
|
||||
private AppNotificationButton GetArchiveButton(Guid mailUniqueId)
|
||||
=> new AppNotificationButton(Translator.MailOperation_Archive)
|
||||
.SetIcon(GetNotificationIconUri("mail-archive"))
|
||||
.AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString())
|
||||
.AddArgument(Constants.ToastActionKey, MailOperation.Archive.ToString())
|
||||
.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
||||
|
||||
private AppNotificationButton GetDeleteButton(Guid mailUniqueId)
|
||||
=> new AppNotificationButton(Translator.MailOperation_Delete)
|
||||
.SetIcon(GetNotificationIconUri("mail-delete"))
|
||||
.AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString())
|
||||
.AddArgument(Constants.ToastActionKey, MailOperation.SoftDelete.ToString())
|
||||
.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
||||
|
||||
private AppNotificationButton GetMarkAsReadButton(Guid mailUniqueId)
|
||||
=> new AppNotificationButton(Translator.MailOperation_MarkAsRead)
|
||||
.SetIcon(GetNotificationIconUri("mail-markread"))
|
||||
.AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString())
|
||||
.AddArgument(Constants.ToastActionKey, MailOperation.MarkAsRead.ToString())
|
||||
.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
||||
|
||||
public async Task UpdateTaskbarIconBadgeAsync()
|
||||
{
|
||||
int totalUnreadCount = 0;
|
||||
var totalUnreadCount = 0;
|
||||
|
||||
try
|
||||
{
|
||||
var badgeUpdater = BadgeUpdateManager.CreateBadgeUpdaterForApplication(MailApplicationId);
|
||||
var accounts = await _accountService.GetAccountsAsync();
|
||||
|
||||
foreach (var account in accounts)
|
||||
{
|
||||
if (!account.Preferences.IsTaskbarBadgeEnabled) continue;
|
||||
if (!account.Preferences.IsTaskbarBadgeEnabled)
|
||||
continue;
|
||||
|
||||
var accountInbox = await _folderService.GetSpecialFolderByAccountIdAsync(account.Id, SpecialFolderType.Inbox);
|
||||
|
||||
if (accountInbox == null)
|
||||
continue;
|
||||
|
||||
var inboxUnreadCount = await _folderService.GetFolderNotificationBadgeAsync(accountInbox.Id);
|
||||
|
||||
totalUnreadCount += inboxUnreadCount;
|
||||
}
|
||||
|
||||
if (totalUnreadCount > 0)
|
||||
{
|
||||
XmlDocument badgeXml = BadgeUpdateManager.GetTemplateContent(BadgeTemplateType.BadgeNumber);
|
||||
|
||||
XmlElement? badgeElement = badgeXml.SelectSingleNode("/badge") as XmlElement;
|
||||
if (badgeElement == null)
|
||||
{
|
||||
badgeUpdater.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
badgeElement.SetAttribute("value", totalUnreadCount.ToString());
|
||||
|
||||
BadgeNotification badge = new(badgeXml);
|
||||
badgeUpdater.Update(badge);
|
||||
}
|
||||
else
|
||||
{
|
||||
badgeUpdater.Clear();
|
||||
}
|
||||
UpdateBadge(AppEntryConstants.MailApplicationId, totalUnreadCount > 0 ? totalUnreadCount : null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -219,15 +128,34 @@ public class NotificationBuilder : INotificationBuilder
|
||||
}
|
||||
}
|
||||
|
||||
public Task AddCalendarTaskbarBadgeCountAsync(int newlyDownloadedCount)
|
||||
{
|
||||
if (newlyDownloadedCount <= 0)
|
||||
return Task.CompletedTask;
|
||||
|
||||
var badgeCount = Interlocked.Add(ref _calendarTaskbarBadgeCount, newlyDownloadedCount);
|
||||
UpdateBadge(AppEntryConstants.CalendarApplicationId, badgeCount > 0 ? badgeCount : null);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task ClearCalendarTaskbarBadgeAsync()
|
||||
{
|
||||
Interlocked.Exchange(ref _calendarTaskbarBadgeCount, 0);
|
||||
UpdateBadge(AppEntryConstants.CalendarApplicationId, null);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void RemoveNotification(Guid mailUniqueId)
|
||||
{
|
||||
try
|
||||
{
|
||||
AppNotificationManager.Default.RemoveByTagAsync(mailUniqueId.ToString()).AsTask().GetAwaiter().GetResult();
|
||||
ToastNotificationManager.History.Remove(
|
||||
mailUniqueId.ToString(),
|
||||
null,
|
||||
AppEntryConstants.GetAppUserModelId(WinoApplicationMode.Mail));
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
// Notification does not exists. Ignore.
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -237,39 +165,38 @@ public class NotificationBuilder : INotificationBuilder
|
||||
|
||||
public void CreateAttentionRequiredNotification(MailAccount account)
|
||||
{
|
||||
var builder = CreateBuilder();
|
||||
|
||||
builder.AddText(Translator.Exception_AccountNeedsAttention_Title);
|
||||
builder.AddText(string.Format(Translator.Exception_AccountNeedsAttention_Message, account.Name));
|
||||
builder.AddArgument(Constants.ToastMailAccountIdKey, account.Id.ToString());
|
||||
builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
||||
builder.AddButton(new AppNotificationButton(Translator.Buttons_FixAccount)
|
||||
var builder = new ToastContentBuilder()
|
||||
.AddText(Translator.Exception_AccountNeedsAttention_Title)
|
||||
.AddText(string.Format(Translator.Exception_AccountNeedsAttention_Message, account.Name))
|
||||
.AddArgument(Constants.ToastMailAccountIdKey, account.Id.ToString())
|
||||
.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail)
|
||||
.AddButton(new ToastButton()
|
||||
.SetContent(Translator.Buttons_FixAccount)
|
||||
.AddArgument(Constants.ToastMailAccountIdKey, account.Id.ToString())
|
||||
.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail));
|
||||
|
||||
ShowNotification(builder);
|
||||
ShowToast(builder, ToastTargetApp.Mail);
|
||||
}
|
||||
|
||||
public void CreateWebView2RuntimeMissingNotification()
|
||||
{
|
||||
var builder = CreateBuilder();
|
||||
var builder = new ToastContentBuilder()
|
||||
.AddText(Translator.Exception_WebView2RuntimeMissing_Title)
|
||||
.AddText(Translator.Exception_WebView2RuntimeMissing_Message)
|
||||
.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
||||
|
||||
builder.AddText(Translator.Exception_WebView2RuntimeMissing_Title);
|
||||
builder.AddText(Translator.Exception_WebView2RuntimeMissing_Message);
|
||||
builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
||||
|
||||
ShowNotification(builder);
|
||||
ShowToast(builder, ToastTargetApp.Mail);
|
||||
}
|
||||
|
||||
public void CreateStoreUpdateNotification()
|
||||
{
|
||||
var builder = CreateBuilder();
|
||||
var builder = new ToastContentBuilder()
|
||||
.AddText(Translator.Notifications_StoreUpdateAvailableTitle)
|
||||
.AddText(Translator.Notifications_StoreUpdateAvailableMessage)
|
||||
.AddArgument(Constants.ToastStoreUpdateActionKey, Constants.ToastStoreUpdateActionInstall)
|
||||
.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
||||
|
||||
builder.AddText(Translator.Notifications_StoreUpdateAvailableTitle);
|
||||
builder.AddText(Translator.Notifications_StoreUpdateAvailableMessage);
|
||||
builder.AddArgument(Constants.ToastStoreUpdateActionKey, Constants.ToastStoreUpdateActionInstall);
|
||||
|
||||
ShowNotification(builder, "store-update-available");
|
||||
ShowToast(builder, ToastTargetApp.Mail, "store-update-available");
|
||||
}
|
||||
|
||||
public Task CreateCalendarReminderNotificationAsync(CalendarItem calendarItem, long reminderDurationInSeconds)
|
||||
@@ -277,11 +204,10 @@ public class NotificationBuilder : INotificationBuilder
|
||||
if (calendarItem == null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
var builder = CreateBuilder(AppNotificationScenario.Reminder);
|
||||
|
||||
var builder = new ToastContentBuilder()
|
||||
.SetToastScenario(ToastScenario.Reminder);
|
||||
var localStart = calendarItem.GetLocalStartDate();
|
||||
var nowLocal = DateTime.Now;
|
||||
var reminderContext = GetCalendarReminderContext(localStart, nowLocal);
|
||||
var reminderContext = GetCalendarReminderContext(localStart, DateTime.Now);
|
||||
|
||||
builder.AddText(calendarItem.Title);
|
||||
builder.AddText($"{reminderContext} - {localStart:g}");
|
||||
@@ -292,6 +218,10 @@ public class NotificationBuilder : INotificationBuilder
|
||||
builder.AddArgument(Constants.ToastCalendarActionKey, Constants.ToastCalendarNavigateAction);
|
||||
builder.AddArgument(Constants.ToastCalendarItemIdKey, calendarItem.Id.ToString());
|
||||
builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeCalendar);
|
||||
builder.AddAudio(new ToastAudio
|
||||
{
|
||||
Src = new Uri("ms-winsoundevent:Notification.Reminder")
|
||||
});
|
||||
|
||||
var allowedSnoozeMinutes = CalendarReminderSnoozeOptions.GetAllowedSnoozeMinutes(
|
||||
reminderDurationInSeconds,
|
||||
@@ -304,42 +234,108 @@ public class NotificationBuilder : INotificationBuilder
|
||||
? preferredSnoozeMinutes
|
||||
: allowedSnoozeMinutes[0];
|
||||
|
||||
var selectionBox = new AppNotificationComboBox(Constants.ToastCalendarSnoozeDurationInputId)
|
||||
.SetSelectedItem(defaultSnoozeMinutes.ToString());
|
||||
var selectionBox = new ToastSelectionBox(Constants.ToastCalendarSnoozeDurationInputId)
|
||||
{
|
||||
DefaultSelectionBoxItemId = defaultSnoozeMinutes.ToString()
|
||||
};
|
||||
|
||||
foreach (var snoozeMinutes in allowedSnoozeMinutes)
|
||||
{
|
||||
selectionBox.AddItem(
|
||||
selectionBox.Items.Add(new ToastSelectionBoxItem(
|
||||
snoozeMinutes.ToString(),
|
||||
string.Format(Translator.CalendarReminder_SnoozeMinutesOption, snoozeMinutes));
|
||||
string.Format(Translator.CalendarReminder_SnoozeMinutesOption, snoozeMinutes)));
|
||||
}
|
||||
|
||||
builder.AddComboBox(selectionBox);
|
||||
builder.AddButton(new AppNotificationButton(Translator.CalendarReminder_SnoozeAction)
|
||||
builder.AddToastInput(selectionBox);
|
||||
builder.AddButton(new ToastButton()
|
||||
.SetContent(Translator.CalendarReminder_SnoozeAction)
|
||||
.SetImageUri(GetNotificationIconUri("calendar-snooze"))
|
||||
.SetBackgroundActivation()
|
||||
.AddArgument(Constants.ToastCalendarActionKey, Constants.ToastCalendarSnoozeAction)
|
||||
.AddArgument(Constants.ToastCalendarItemIdKey, calendarItem.Id.ToString())
|
||||
.AddArgument(Constants.ToastModeKey, Constants.ToastModeCalendar));
|
||||
}
|
||||
|
||||
builder.AddButton(new AppNotificationButton(Translator.Buttons_Open)
|
||||
builder.AddButton(new ToastButton()
|
||||
.SetContent(Translator.Buttons_Open)
|
||||
.SetBackgroundActivation()
|
||||
.AddArgument(Constants.ToastCalendarActionKey, Constants.ToastCalendarNavigateAction)
|
||||
.AddArgument(Constants.ToastCalendarItemIdKey, calendarItem.Id.ToString())
|
||||
.AddArgument(Constants.ToastModeKey, Constants.ToastModeCalendar));
|
||||
|
||||
if (Uri.TryCreate(calendarItem.HtmlLink, UriKind.Absolute, out var joinUri))
|
||||
{
|
||||
builder.AddButton(new AppNotificationButton(Translator.CalendarEventDetails_JoinOnline)
|
||||
.SetInvokeUri(joinUri));
|
||||
builder.AddButton(new ToastButton()
|
||||
.SetContent(Translator.CalendarEventDetails_JoinOnline)
|
||||
.SetImageUri(GetNotificationIconUri("calendar-join"))
|
||||
.SetProtocolActivation(joinUri));
|
||||
}
|
||||
|
||||
builder.SetAudioUri(new Uri("ms-winsoundevent:Notification.Reminder"));
|
||||
|
||||
var tag = $"calendar-reminder-{calendarItem.Id:N}-{reminderDurationInSeconds}";
|
||||
ShowNotification(builder, tag);
|
||||
ShowToast(builder, ToastTargetApp.Calendar, tag);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task CreateSingleNotificationAsync(MailCopy mailItem)
|
||||
{
|
||||
var builder = new ToastContentBuilder();
|
||||
|
||||
var avatarThumbnail = await _thumbnailService.GetThumbnailAsync(mailItem.FromAddress, awaitLoad: true);
|
||||
if (!string.IsNullOrEmpty(avatarThumbnail))
|
||||
{
|
||||
var tempFile = await Windows.Storage.ApplicationData.Current.TemporaryFolder.CreateFileAsync(
|
||||
$"{Guid.NewGuid()}.png",
|
||||
Windows.Storage.CreationCollisionOption.ReplaceExisting);
|
||||
|
||||
await using (var stream = await tempFile.OpenStreamForWriteAsync())
|
||||
{
|
||||
var bytes = Convert.FromBase64String(avatarThumbnail);
|
||||
await stream.WriteAsync(bytes);
|
||||
}
|
||||
|
||||
builder.AddAppLogoOverride(new Uri($"ms-appdata:///temp/{tempFile.Name}"), ToastGenericAppLogoCrop.Default);
|
||||
}
|
||||
|
||||
builder.AddCustomTimeStamp(mailItem.CreationDate.ToLocalTime());
|
||||
builder.AddText(mailItem.FromName);
|
||||
builder.AddText(mailItem.Subject);
|
||||
builder.AddText(mailItem.PreviewText);
|
||||
builder.AddArgument(Constants.ToastMailUniqueIdKey, mailItem.UniqueId.ToString());
|
||||
builder.AddArgument(Constants.ToastActionKey, MailOperation.Navigate);
|
||||
builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
||||
builder.AddButton(GetMarkAsReadButton(mailItem.UniqueId));
|
||||
builder.AddButton(GetDeleteButton(mailItem.UniqueId));
|
||||
builder.AddButton(GetArchiveButton(mailItem.UniqueId));
|
||||
builder.AddAudio(new ToastAudio
|
||||
{
|
||||
Src = new Uri("ms-winsoundevent:Notification.Mail")
|
||||
});
|
||||
|
||||
ShowToast(builder, ToastTargetApp.Mail, mailItem.UniqueId.ToString());
|
||||
}
|
||||
|
||||
private void UpdateBadge(string applicationId, int? badgeCount)
|
||||
{
|
||||
var badgeUpdater = BadgeUpdateManager.CreateBadgeUpdaterForApplication(applicationId);
|
||||
|
||||
if (!badgeCount.HasValue || badgeCount.Value <= 0)
|
||||
{
|
||||
badgeUpdater.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
XmlDocument badgeXml = BadgeUpdateManager.GetTemplateContent(BadgeTemplateType.BadgeNumber);
|
||||
if (badgeXml.SelectSingleNode("/badge") is not XmlElement badgeElement)
|
||||
{
|
||||
badgeUpdater.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
badgeElement.SetAttribute("value", badgeCount.Value.ToString());
|
||||
badgeUpdater.Update(new BadgeNotification(badgeXml));
|
||||
}
|
||||
|
||||
private static string GetCalendarReminderContext(DateTime localStart, DateTime nowLocal)
|
||||
{
|
||||
var delta = localStart - nowLocal;
|
||||
@@ -370,22 +366,55 @@ public class NotificationBuilder : INotificationBuilder
|
||||
return string.Format(Translator.CalendarReminder_StartedMinutesAgo, minutesAgo);
|
||||
}
|
||||
|
||||
private static AppNotificationBuilder CreateBuilder(AppNotificationScenario scenario = AppNotificationScenario.Default)
|
||||
=> new AppNotificationBuilder().SetScenario(scenario);
|
||||
private ToastButton GetArchiveButton(Guid mailUniqueId)
|
||||
=> new ToastButton()
|
||||
.SetContent(Translator.MailOperation_Archive)
|
||||
.SetImageUri(GetNotificationIconUri("mail-archive"))
|
||||
.SetBackgroundActivation()
|
||||
.AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString())
|
||||
.AddArgument(Constants.ToastActionKey, MailOperation.Archive)
|
||||
.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
||||
|
||||
private static void ShowNotification(AppNotificationBuilder builder, string? tag = null)
|
||||
private ToastButton GetDeleteButton(Guid mailUniqueId)
|
||||
=> new ToastButton()
|
||||
.SetContent(Translator.MailOperation_Delete)
|
||||
.SetImageUri(GetNotificationIconUri("mail-delete"))
|
||||
.SetBackgroundActivation()
|
||||
.AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString())
|
||||
.AddArgument(Constants.ToastActionKey, MailOperation.SoftDelete)
|
||||
.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
||||
|
||||
private ToastButton GetMarkAsReadButton(Guid mailUniqueId)
|
||||
=> new ToastButton()
|
||||
.SetContent(Translator.MailOperation_MarkAsRead)
|
||||
.SetImageUri(GetNotificationIconUri("mail-markread"))
|
||||
.SetBackgroundActivation()
|
||||
.AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString())
|
||||
.AddArgument(Constants.ToastActionKey, MailOperation.MarkAsRead)
|
||||
.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
||||
|
||||
private static void ShowToast(ToastContentBuilder builder, ToastTargetApp targetApp, string? tag = null)
|
||||
{
|
||||
var notification = builder.BuildNotification();
|
||||
var toastNotification = new ToastNotification(builder.GetToastContent().GetXml());
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(tag))
|
||||
notification.Tag = tag;
|
||||
|
||||
AppNotificationManager.Default.Show(notification);
|
||||
}
|
||||
|
||||
private Uri GetNotificationIconUri(string iconName)
|
||||
{
|
||||
// Keep the URI unqualified so Windows resolves the best matching theme/scale asset from the package.
|
||||
return new($"{NotificationIconRootUri}{iconName}.png");
|
||||
toastNotification.Tag = tag;
|
||||
}
|
||||
|
||||
ToastNotificationManager
|
||||
.CreateToastNotifier(AppEntryConstants.GetAppUserModelId(targetApp == ToastTargetApp.Calendar
|
||||
? WinoApplicationMode.Calendar
|
||||
: WinoApplicationMode.Mail))
|
||||
.Show(toastNotification);
|
||||
}
|
||||
|
||||
private static Uri GetNotificationIconUri(string iconName)
|
||||
=> new($"{NotificationIconRootUri}{iconName}.png");
|
||||
|
||||
private enum ToastTargetApp
|
||||
{
|
||||
Mail,
|
||||
Calendar
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.ApplicationModel;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Mail.WinUI.Activation;
|
||||
|
||||
namespace Wino.Mail.WinUI.Services;
|
||||
|
||||
internal sealed class PackagedAppEntryLauncher
|
||||
{
|
||||
public async Task<bool> LaunchAsync(WinoApplicationMode mode)
|
||||
{
|
||||
var targetApplicationId = AppEntryConstants.GetPackagedApplicationId(mode);
|
||||
if (string.IsNullOrWhiteSpace(targetApplicationId))
|
||||
return false;
|
||||
|
||||
var targetAppUserModelId = AppEntryConstants.GetAppUserModelId(mode);
|
||||
var appEntries = await Package.Current.GetAppListEntriesAsync();
|
||||
var appEntry = appEntries.FirstOrDefault(entry =>
|
||||
string.Equals(entry.AppUserModelId, targetAppUserModelId, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
return appEntry != null && await appEntry.LaunchAsync();
|
||||
}
|
||||
}
|
||||
@@ -117,6 +117,7 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow,
|
||||
public void HandleAppActivation(string? launchArguments, string? tileId = null, string? appId = null)
|
||||
{
|
||||
var targetMode = AppModeActivationResolver.Resolve(launchArguments, tileId, appId, PreferencesService.DefaultApplicationMode);
|
||||
WindowAppUserModelIdHelper.TrySet(this, AppEntryConstants.GetAppUserModelId(targetMode));
|
||||
NavigationService.ChangeApplicationMode(targetMode);
|
||||
}
|
||||
|
||||
|
||||
@@ -174,6 +174,7 @@
|
||||
<PackageReference Include="CommunityToolkit.Diagnostics" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
<PackageReference Include="CommunityToolkit.Labs.WinUI.DependencyPropertyGenerator" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Notifications" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Animations" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" />
|
||||
|
||||
Reference in New Issue
Block a user