diff --git a/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs b/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs
index 9277eb6c..6cafeb36 100644
--- a/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs
+++ b/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -67,6 +67,9 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
public bool IsVerticalCalendar => StatePersistenceService.CalendarDisplayType == CalendarDisplayType.Month;
+ [ObservableProperty]
+ private bool isStoreUpdateItemVisible;
+
// For updating account calendars asynchronously.
private SemaphoreSlim _accountCalendarUpdateSemaphoreSlim = new(1);
@@ -77,12 +80,14 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
IAccountCalendarStateService accountCalendarStateService,
INavigationService navigationService,
IMailDialogService dialogService,
- IUpdateManager updateManager)
+ IUpdateManager updateManager,
+ IStoreUpdateService storeUpdateService)
{
_accountService = accountService;
_calendarService = calendarService;
_dialogService = dialogService;
_updateManager = updateManager;
+ _storeUpdateService = storeUpdateService;
AccountCalendarStateService = accountCalendarStateService;
AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested;
@@ -100,6 +105,7 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
base.OnDispatcherAssigned();
AccountCalendarStateService.Dispatcher = Dispatcher;
+ _ = RefreshFooterItemsAsync(false);
}
private void PrefefencesChanged(object sender, string e)
@@ -115,10 +121,23 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
}
}
+ private async void PreferencesServiceChanged(object sender, string e)
+ {
+ if (e == nameof(IPreferencesService.IsStoreUpdateNotificationsEnabled))
+ {
+ await RefreshFooterItemsAsync(false);
+ }
+ }
+
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
{
base.OnNavigatedTo(mode, parameters);
+ PreferencesService.PreferenceChanged -= PreferencesServiceChanged;
+ PreferencesService.PreferenceChanged += PreferencesServiceChanged;
+
+ await RefreshFooterItemsAsync(mode == NavigationMode.New);
+
// Preserve the existing calendar shell frame state when the user switches
// between Mail and Calendar modes. Back/forward restoration should not
// force a new CalendarPage navigation, otherwise pages like
@@ -141,6 +160,13 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
TodayClicked();
}
+ public override void OnNavigatedFrom(NavigationMode mode, object parameters)
+ {
+ base.OnNavigatedFrom(mode, parameters);
+
+ PreferencesService.PreferenceChanged -= PreferencesServiceChanged;
+ }
+
private async Task ShowWhatIsNewIfNeededAsync()
{
if (!_updateManager.ShouldShowUpdateNotes())
@@ -154,6 +180,22 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
await _dialogService.ShowWhatIsNewDialogAsync(notes);
}
+ private async Task RefreshFooterItemsAsync(bool showNotification)
+ {
+ await _storeUpdateService.RefreshAvailabilityAsync(showNotification).ConfigureAwait(false);
+
+ await ExecuteUIThread(() =>
+ {
+ IsStoreUpdateItemVisible = _storeUpdateService.HasAvailableUpdate && PreferencesService.IsStoreUpdateNotificationsEnabled;
+ });
+ }
+
+ private async Task StartStoreUpdateAsync()
+ {
+ await _storeUpdateService.StartUpdateAsync().ConfigureAwait(false);
+ await RefreshFooterItemsAsync(false).ConfigureAwait(false);
+ }
+
private async void AccountCalendarStateCollectivelyChanged(object sender, GroupedAccountCalendarViewModel e)
{
// When using three-state checkbox, multiple accounts will be selected/unselected at the same time.
@@ -211,40 +253,34 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
private void ForceNavigateCalendarDate()
{
- if (SelectedMenuItemIndex == -1)
+ var args = new CalendarPageNavigationArgs()
{
- var args = new CalendarPageNavigationArgs()
- {
- NavigationDate = _navigationDate ?? DateTime.Now.Date
- };
+ NavigationDate = _navigationDate ?? DateTime.Now.Date
+ };
- // Already on calendar. Just navigate.
- NavigationService.Navigate(WinoPage.CalendarPage, args);
-
- _navigationDate = null;
- }
- else
- {
- SelectedMenuItemIndex = -1;
- }
+ NavigationService.Navigate(WinoPage.CalendarPage, args);
+ _navigationDate = null;
}
partial void OnSelectedMenuItemIndexChanged(int oldValue, int newValue)
{
- switch (newValue)
+ if (newValue < 0)
+ return;
+
+ if (newValue == 0)
{
- case -1:
- ForceNavigateCalendarDate();
- break;
- case 0:
- NavigationService.Navigate(WinoPage.ManageAccountsPage);
- break;
- case 1:
- NavigationService.Navigate(WinoPage.SettingsPage);
- break;
- default:
- break;
+ NavigationService.Navigate(WinoPage.ManageAccountsPage);
}
+ else if (newValue == 1)
+ {
+ NavigationService.Navigate(WinoPage.SettingsPage);
+ }
+ else if (IsStoreUpdateItemVisible && newValue == 2)
+ {
+ _ = StartStoreUpdateAsync();
+ }
+
+ SelectedMenuItemIndex = -1;
}
[RelayCommand]
@@ -301,6 +337,7 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
private readonly ICalendarService _calendarService;
private readonly IMailDialogService _dialogService;
private readonly IUpdateManager _updateManager;
+ private readonly IStoreUpdateService _storeUpdateService;
#region Commands
@@ -476,7 +513,7 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
public async void Receive(CalendarEnableStatusChangedMessage message)
=> await ExecuteUIThread(() => IsCalendarEnabled = message.IsEnabled);
- public void Receive(NavigateManageAccountsRequested message) => SelectedMenuItemIndex = 1;
+ public void Receive(NavigateManageAccountsRequested message) => SelectedMenuItemIndex = 0;
public void Receive(CalendarDisplayTypeChangedMessage message)
{
@@ -539,3 +576,12 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
return (startDate, startDate.AddMinutes(30));
}
}
+
+
+
+
+
+
+
+
+
diff --git a/Wino.Core.Domain/Constants.cs b/Wino.Core.Domain/Constants.cs
index 72d49850..cded77eb 100644
--- a/Wino.Core.Domain/Constants.cs
+++ b/Wino.Core.Domain/Constants.cs
@@ -1,4 +1,4 @@
-namespace Wino.Core.Domain;
+namespace Wino.Core.Domain;
public static class Constants
{
@@ -21,6 +21,8 @@ public static class Constants
public const string ToastModeKey = nameof(ToastModeKey);
public const string ToastModeMail = nameof(ToastModeMail);
public const string ToastModeCalendar = nameof(ToastModeCalendar);
+ public const string ToastStoreUpdateActionKey = nameof(ToastStoreUpdateActionKey);
+ public const string ToastStoreUpdateActionInstall = nameof(ToastStoreUpdateActionInstall);
public const string ClientLogFile = "Client_.log";
public const string ServerLogFile = "Server_.log";
public const string LogArchiveFileName = "WinoLogs.zip";
@@ -28,3 +30,4 @@ public static class Constants
public const string WinoMailIdentiifer = nameof(WinoMailIdentiifer);
public const string WinoCalendarIdentifier = nameof(WinoCalendarIdentifier);
}
+
diff --git a/Wino.Core.Domain/Interfaces/INotificationBuilder.cs b/Wino.Core.Domain/Interfaces/INotificationBuilder.cs
index aae5b2b2..ed41e474 100644
--- a/Wino.Core.Domain/Interfaces/INotificationBuilder.cs
+++ b/Wino.Core.Domain/Interfaces/INotificationBuilder.cs
@@ -36,8 +36,14 @@ public interface INotificationBuilder
///
void CreateWebView2RuntimeMissingNotification();
+ ///
+ /// Shows a notification when a Microsoft Store update is available.
+ ///
+ void CreateStoreUpdateNotification();
+
///
/// Creates a calendar reminder toast for the specified calendar item.
///
Task CreateCalendarReminderNotificationAsync(CalendarItem calendarItem, long reminderDurationInSeconds);
}
+
diff --git a/Wino.Core.Domain/Interfaces/IPreferencesService.cs b/Wino.Core.Domain/Interfaces/IPreferencesService.cs
index 21701be5..8d4f4464 100644
--- a/Wino.Core.Domain/Interfaces/IPreferencesService.cs
+++ b/Wino.Core.Domain/Interfaces/IPreferencesService.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.ComponentModel;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Calendar;
@@ -57,6 +57,11 @@ public interface IPreferencesService : INotifyPropertyChanged
///
WinoApplicationMode DefaultApplicationMode { get; set; }
+ ///
+ /// Setting: Whether Microsoft Store update notifications should be shown.
+ ///
+ bool IsStoreUpdateNotificationsEnabled { get; set; }
+
#endregion
#region Mail
@@ -241,3 +246,4 @@ public interface IPreferencesService : INotifyPropertyChanged
#endregion
}
+
diff --git a/Wino.Core.Domain/Interfaces/IStoreUpdateService.cs b/Wino.Core.Domain/Interfaces/IStoreUpdateService.cs
new file mode 100644
index 00000000..1977b13e
--- /dev/null
+++ b/Wino.Core.Domain/Interfaces/IStoreUpdateService.cs
@@ -0,0 +1,12 @@
+using System.Threading.Tasks;
+
+namespace Wino.Core.Domain.Interfaces;
+
+public interface IStoreUpdateService
+{
+ bool HasAvailableUpdate { get; }
+
+ Task RefreshAvailabilityAsync(bool showNotification = false);
+
+ Task StartUpdateAsync();
+}
diff --git a/Wino.Core.Domain/MenuItems/StoreUpdateMenuItem.cs b/Wino.Core.Domain/MenuItems/StoreUpdateMenuItem.cs
new file mode 100644
index 00000000..6afa77c4
--- /dev/null
+++ b/Wino.Core.Domain/MenuItems/StoreUpdateMenuItem.cs
@@ -0,0 +1,3 @@
+namespace Wino.Core.Domain.MenuItems;
+
+public class StoreUpdateMenuItem : MenuItemBase { }
diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json
index b01acc19..58821952 100644
--- a/Wino.Core.Domain/Translations/en_US/resources.json
+++ b/Wino.Core.Domain/Translations/en_US/resources.json
@@ -599,6 +599,7 @@
"MenuNewMail": "New Mail",
"MenuRate": "Rate Wino",
"MenuSettings": "Settings",
+ "MenuUpdateAvailable": "Update Available",
"MergedAccountCommonFolderArchive": "Archive",
"MergedAccountCommonFolderDraft": "Draft",
"MergedAccountCommonFolderInbox": "Inbox",
@@ -622,6 +623,8 @@
"Notifications_MultipleNotificationsTitle": "New Mail",
"Notifications_WinoUpdatedMessage": "Checkout new version {0}",
"Notifications_WinoUpdatedTitle": "Wino Mail has been updated.",
+ "Notifications_StoreUpdateAvailableTitle": "Update available",
+ "Notifications_StoreUpdateAvailableMessage": "A newer version of Wino Mail is ready to install from Microsoft Store.",
"OnlineSearchFailed_Message": "Failed to perform search\n{0}\n\nListing offline mails.",
"OnlineSearchTry_Line1": "Can't find what you are looking for?",
"OnlineSearchTry_Line2": "Try online search.",
@@ -700,6 +703,8 @@
"SettingsAppPreferences_StartupBehavior_FatalError": "Fatal error occurred while changing the startup mode for Wino Mail.",
"SettingsAppPreferences_StartupBehavior_Title": "Start minimized on Windows startup",
"SettingsAppPreferences_Title": "App Preferences",
+ "SettingsAppPreferences_StoreUpdateNotifications_Title": "Store update notifications",
+ "SettingsAppPreferences_StoreUpdateNotifications_Description": "Show notifications and footer actions when a Microsoft Store update is available.",
"SettingsAutoSelectNextItem_Description": "Select the next item after you delete or move a mail.",
"SettingsAutoSelectNextItem_Title": "Auto select next item",
"SettingsAvailableThemes_Description": "Select a theme from Wino's own collection for your taste or apply your own themes.",
@@ -1117,3 +1122,5 @@
"AccountSetup_TryAgainButton": "Try Again",
"ImapCalDavSettings_AutoDiscoveryFailed": "Auto-discovery failed. Please enter settings manually in the Advanced tab."
}
+
+
diff --git a/Wino.Mail.ViewModels/MailAppShellViewModel.cs b/Wino.Mail.ViewModels/MailAppShellViewModel.cs
index 4f641b7e..5965ba47 100644
--- a/Wino.Mail.ViewModels/MailAppShellViewModel.cs
+++ b/Wino.Mail.ViewModels/MailAppShellViewModel.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
@@ -55,6 +55,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
private readonly SettingsItem SettingsItem = new SettingsItem();
private readonly ManageAccountsMenuItem ManageAccountsMenuItem = new ManageAccountsMenuItem();
private readonly ContactsMenuItem ContactsMenuItem = new ContactsMenuItem();
+ private readonly StoreUpdateMenuItem StoreUpdateMenuItem = new StoreUpdateMenuItem();
public IMenuItem CreateMailMenuItem = new NewMailMenuItem();
@@ -79,6 +80,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
private readonly IMimeFileService _mimeFileService;
private readonly IWebView2RuntimeValidatorService _webView2RuntimeValidatorService;
private readonly IUpdateManager _updateManager;
+ private readonly IStoreUpdateService _storeUpdateService;
private readonly INativeAppService _nativeAppService;
private readonly IMailService _mailService;
@@ -102,7 +104,8 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
IConfigurationService configurationService,
IStartupBehaviorService startupBehaviorService,
IWebView2RuntimeValidatorService webView2RuntimeValidatorService,
- IUpdateManager updateManager)
+ IUpdateManager updateManager,
+ IStoreUpdateService storeUpdateService)
{
StatePersistenceService = statePersistanceService;
@@ -124,6 +127,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
_winoRequestDelegator = winoRequestDelegator;
_webView2RuntimeValidatorService = webView2RuntimeValidatorService;
_updateManager = updateManager;
+ _storeUpdateService = storeUpdateService;
}
protected override void OnDispatcherAssigned()
@@ -142,8 +146,10 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
return _contextMenuItemService.GetFolderContextMenuActions(folder);
}
- private async Task CreateFooterItemsAsync()
+ private async Task CreateFooterItemsAsync(bool showNotification = false)
{
+ await _storeUpdateService.RefreshAvailabilityAsync(showNotification).ConfigureAwait(false);
+
await ExecuteUIThread(() =>
{
// TODO: Selected footer item container still remains selected after re-creation.
@@ -159,6 +165,12 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
FooterItems.Add(ContactsMenuItem);
FooterItems.Add(ManageAccountsMenuItem);
+
+ if (_storeUpdateService.HasAvailableUpdate && PreferencesService.IsStoreUpdateNotificationsEnabled)
+ {
+ FooterItems.Add(StoreUpdateMenuItem);
+ }
+
FooterItems.Add(SettingsItem);
});
}
@@ -223,10 +235,21 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
}
}
+ private async void PreferencesServiceChanged(object sender, string e)
+ {
+ if (e == nameof(IPreferencesService.IsStoreUpdateNotificationsEnabled))
+ {
+ await CreateFooterItemsAsync();
+ }
+ }
+
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
{
base.OnNavigatedTo(mode, parameters);
+ PreferencesService.PreferenceChanged -= PreferencesServiceChanged;
+ PreferencesService.PreferenceChanged += PreferencesServiceChanged;
+
if (mode == NavigationMode.Back)
{
// Preserve current mail/folder selection and active rendering page when
@@ -243,7 +266,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
return;
}
- await CreateFooterItemsAsync();
+ await CreateFooterItemsAsync(true);
await RecreateMenuItemsAsync();
await ProcessLaunchOptionsAsync();
@@ -268,6 +291,13 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
}
}
+ public override void OnNavigatedFrom(NavigationMode mode, object parameters)
+ {
+ base.OnNavigatedFrom(mode, parameters);
+
+ PreferencesService.PreferenceChanged -= PreferencesServiceChanged;
+ }
+
private async Task ShowWhatIsNewIfNeededAsync()
{
if (!_updateManager.ShouldShowUpdateNotes())
@@ -638,6 +668,11 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
// Don't navigate to merged account if it's already selected. Preserve user's already selected folder.
await ChangeLoadedAccountAsync(clickedMergedAccountMenuItem, true);
}
+ else if (clickedMenuItem is StoreUpdateMenuItem)
+ {
+ await _storeUpdateService.StartUpdateAsync().ConfigureAwait(false);
+ await CreateFooterItemsAsync().ConfigureAwait(false);
+ }
else if (clickedMenuItem is SettingsItem)
{
NavigationService.Navigate(WinoPage.SettingsPage, parameter, NavigationReferenceFrame.InnerShellFrame, NavigationTransitionType.None);
@@ -1051,7 +1086,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
public async void Receive(LanguageChanged message)
{
- await CreateFooterItemsAsync();
+ await CreateFooterItemsAsync(true);
await RecreateMenuItemsAsync();
await RestoreSelectedAccountAfterMenuRefreshAsync(false);
}
@@ -1245,3 +1280,10 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
});
}
}
+
+
+
+
+
+
+
diff --git a/Wino.Mail.WinUI/App.xaml.cs b/Wino.Mail.WinUI/App.xaml.cs
index 85ad8868..650cddd0 100644
--- a/Wino.Mail.WinUI/App.xaml.cs
+++ b/Wino.Mail.WinUI/App.xaml.cs
@@ -296,6 +296,13 @@ public partial class App : WinoApplication,
{
var toastArguments = ToastArguments.Parse(toastArgs.Argument);
+ if (toastArguments.TryGetValue(Constants.ToastStoreUpdateActionKey, out string storeUpdateAction) &&
+ storeUpdateAction == Constants.ToastStoreUpdateActionInstall)
+ {
+ await HandleStoreUpdateToastAsync();
+ return;
+ }
+
// Check calendar reminder toast activation first.
if (toastArguments.TryGetValue(Constants.ToastCalendarActionKey, out string calendarAction) &&
toastArguments.TryGetValue(Constants.ToastCalendarItemIdKey, out string calendarItemIdString) &&
@@ -333,6 +340,21 @@ public partial class App : WinoApplication,
}
}
+ private async Task HandleStoreUpdateToastAsync()
+ {
+ if (!IsAppRunning())
+ {
+ await CreateAndActivateWindow(null!);
+ }
+ else
+ {
+ EnsureMainWindowVisibleAndForeground();
+ }
+
+ var storeUpdateService = Services.GetRequiredService();
+ await storeUpdateService.StartUpdateAsync();
+ }
+
private async Task HandleCalendarToastNavigationAsync(Guid calendarItemId)
{
var calendarService = Services.GetRequiredService();
@@ -997,3 +1019,5 @@ public partial class App : WinoApplication,
return null;
}
}
+
+
diff --git a/Wino.Mail.WinUI/CoreUWPContainerSetup.cs b/Wino.Mail.WinUI/CoreUWPContainerSetup.cs
index bf729cab..0c1b42b2 100644
--- a/Wino.Mail.WinUI/CoreUWPContainerSetup.cs
+++ b/Wino.Mail.WinUI/CoreUWPContainerSetup.cs
@@ -31,6 +31,7 @@ public static class CoreUWPContainerSetup
services.AddTransient();
services.AddTransient();
services.AddTransient();
+ services.AddSingleton();
services.AddTransient();
services.AddTransient();
services.AddTransient();
@@ -53,3 +54,5 @@ public static class CoreUWPContainerSetup
services.AddTransient(typeof(KeyboardShortcutsPageViewModel));
}
}
+
+
diff --git a/Wino.Mail.WinUI/MailAppShell.xaml b/Wino.Mail.WinUI/MailAppShell.xaml
index bd4bf68d..26a68ea0 100644
--- a/Wino.Mail.WinUI/MailAppShell.xaml
+++ b/Wino.Mail.WinUI/MailAppShell.xaml
@@ -387,7 +387,8 @@
NewMailTemplate="{StaticResource CreateNewMailTemplate}"
RatingItemTemplate="{StaticResource RatingItemTemplate}"
SeperatorTemplate="{StaticResource SeperatorTemplate}"
- SettingsItemTemplate="{StaticResource SettingsItemTemplate}" />
+ SettingsItemTemplate="{StaticResource SettingsItemTemplate}"
+ StoreUpdateItemTemplate="{StaticResource StoreUpdateItemTemplate}" />
+
+
diff --git a/Wino.Mail.WinUI/Selectors/NavigationMenuTemplateSelector.cs b/Wino.Mail.WinUI/Selectors/NavigationMenuTemplateSelector.cs
index 69864a9e..4dce3d61 100644
--- a/Wino.Mail.WinUI/Selectors/NavigationMenuTemplateSelector.cs
+++ b/Wino.Mail.WinUI/Selectors/NavigationMenuTemplateSelector.cs
@@ -15,6 +15,7 @@ public partial class NavigationMenuTemplateSelector : DataTemplateSelector
public DataTemplate MergedAccountMoreExpansionItemTemplate { get; set; } = null!;
public DataTemplate FolderMenuTemplate { get; set; } = null!;
public DataTemplate SettingsItemTemplate { get; set; } = null!;
+ public DataTemplate StoreUpdateItemTemplate { get; set; } = null!;
public DataTemplate MoreItemsFolderTemplate { get; set; } = null!;
public DataTemplate RatingItemTemplate { get; set; } = null!;
public DataTemplate CreateNewFolderTemplate { get; set; } = null!;
@@ -32,6 +33,8 @@ public partial class NavigationMenuTemplateSelector : DataTemplateSelector
return ContactsMenuItemTemplate;
else if (item is SettingsItem)
return SettingsItemTemplate;
+ else if (item is StoreUpdateMenuItem)
+ return StoreUpdateItemTemplate;
else if (item is SeperatorItem)
return SeperatorTemplate;
else if (item is AccountMenuItem)
@@ -55,3 +58,5 @@ public partial class NavigationMenuTemplateSelector : DataTemplateSelector
return MenuItemTemplate;
}
}
+
+
diff --git a/Wino.Mail.WinUI/Services/NotificationBuilder.cs b/Wino.Mail.WinUI/Services/NotificationBuilder.cs
index c5a44a55..8651b4d5 100644
--- a/Wino.Mail.WinUI/Services/NotificationBuilder.cs
+++ b/Wino.Mail.WinUI/Services/NotificationBuilder.cs
@@ -293,6 +293,19 @@ public class NotificationBuilder : INotificationBuilder
ShowToast(builder);
}
+ public void CreateStoreUpdateNotification()
+ {
+ var builder = new ToastContentBuilder();
+ builder.SetToastScenario(ToastScenario.Default);
+
+ builder.AddText(Translator.Notifications_StoreUpdateAvailableTitle);
+ builder.AddText(Translator.Notifications_StoreUpdateAvailableMessage);
+ builder.AddArgument(Constants.ToastStoreUpdateActionKey, Constants.ToastStoreUpdateActionInstall);
+ builder.AddButton(GetDismissButton());
+
+ ShowToast(builder, "store-update-available");
+ }
+
public Task CreateCalendarReminderNotificationAsync(CalendarItem calendarItem, long reminderDurationInSeconds)
{
if (calendarItem == null)
@@ -437,3 +450,4 @@ public class NotificationBuilder : INotificationBuilder
return SupportedIconScales.OrderBy(s => Math.Abs(s - requestedScale)).First();
}
}
+
diff --git a/Wino.Mail.WinUI/Services/PreferencesService.cs b/Wino.Mail.WinUI/Services/PreferencesService.cs
index 50b8a433..401c7f64 100644
--- a/Wino.Mail.WinUI/Services/PreferencesService.cs
+++ b/Wino.Mail.WinUI/Services/PreferencesService.cs
@@ -308,6 +308,11 @@ public class PreferencesService(IConfigurationService configurationService) : Ob
set => SetPropertyAndSave(nameof(EmailSyncIntervalMinutes), value);
}
+ public bool IsStoreUpdateNotificationsEnabled
+ {
+ get => _configurationService.Get(nameof(IsStoreUpdateNotificationsEnabled), true);
+ set => SetPropertyAndSave(nameof(IsStoreUpdateNotificationsEnabled), value);
+ }
public WinoApplicationMode DefaultApplicationMode
{
get
@@ -357,3 +362,5 @@ public class PreferencesService(IConfigurationService configurationService) : Ob
return daysOfWeek;
}
}
+
+
diff --git a/Wino.Mail.WinUI/Services/StoreUpdateService.cs b/Wino.Mail.WinUI/Services/StoreUpdateService.cs
new file mode 100644
index 00000000..561d562d
--- /dev/null
+++ b/Wino.Mail.WinUI/Services/StoreUpdateService.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Windows.Services.Store;
+using Wino.Core.Domain.Interfaces;
+
+namespace Wino.Mail.WinUI.Services;
+
+public class StoreUpdateService : IStoreUpdateService
+{
+ private const string NotificationShownKeyFormat = "StoreUpdateNotificationShown_{0}";
+
+ private readonly IConfigurationService _configurationService;
+ private readonly INotificationBuilder _notificationBuilder;
+ private readonly IPreferencesService _preferencesService;
+ private readonly INativeAppService _nativeAppService;
+ private readonly SemaphoreSlim _refreshSemaphore = new(1, 1);
+ private readonly StoreContext _storeContext = StoreContext.GetDefault();
+
+ public bool HasAvailableUpdate { get; private set; }
+
+ public StoreUpdateService(IConfigurationService configurationService,
+ INotificationBuilder notificationBuilder,
+ IPreferencesService preferencesService,
+ INativeAppService nativeAppService)
+ {
+ _configurationService = configurationService;
+ _notificationBuilder = notificationBuilder;
+ _preferencesService = preferencesService;
+ _nativeAppService = nativeAppService;
+ }
+
+ public async Task RefreshAvailabilityAsync(bool showNotification = false)
+ {
+ await _refreshSemaphore.WaitAsync().ConfigureAwait(false);
+
+ try
+ {
+ var updates = await _storeContext.GetAppAndOptionalStorePackageUpdatesAsync();
+ HasAvailableUpdate = updates?.Count > 0;
+
+ if (showNotification &&
+ HasAvailableUpdate &&
+ _preferencesService.IsStoreUpdateNotificationsEnabled &&
+ !HasShownNotificationForCurrentVersion())
+ {
+ _notificationBuilder.CreateStoreUpdateNotification();
+ MarkNotificationShownForCurrentVersion();
+ }
+
+ return HasAvailableUpdate;
+ }
+ catch
+ {
+ HasAvailableUpdate = false;
+ return false;
+ }
+ finally
+ {
+ _refreshSemaphore.Release();
+ }
+ }
+
+ public async Task StartUpdateAsync()
+ {
+ try
+ {
+ var updates = await _storeContext.GetAppAndOptionalStorePackageUpdatesAsync();
+
+ if (updates == null || updates.Count == 0)
+ {
+ HasAvailableUpdate = false;
+ return false;
+ }
+
+ await _storeContext.RequestDownloadAndInstallStorePackageUpdatesAsync(updates);
+ await RefreshAvailabilityAsync(false).ConfigureAwait(false);
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ private bool HasShownNotificationForCurrentVersion()
+ => _configurationService.Get(GetNotificationShownKey(), false);
+
+ private void MarkNotificationShownForCurrentVersion()
+ => _configurationService.Set(GetNotificationShownKey(), true);
+
+ private string GetNotificationShownKey()
+ => string.Format(NotificationShownKeyFormat, _nativeAppService.GetFullAppVersion().Replace(".", "_"));
+}
diff --git a/Wino.Mail.WinUI/Styles/DataTemplates.xaml b/Wino.Mail.WinUI/Styles/DataTemplates.xaml
index c39736c2..dd622a9e 100644
--- a/Wino.Mail.WinUI/Styles/DataTemplates.xaml
+++ b/Wino.Mail.WinUI/Styles/DataTemplates.xaml
@@ -62,6 +62,14 @@
+
+
+
+
+
+
+
+
@@ -271,3 +279,7 @@
+
+
+
+
diff --git a/Wino.Mail.WinUI/Views/Calendar/CalendarAppShell.xaml b/Wino.Mail.WinUI/Views/Calendar/CalendarAppShell.xaml
index 77fbd3a4..7723c209 100644
--- a/Wino.Mail.WinUI/Views/Calendar/CalendarAppShell.xaml
+++ b/Wino.Mail.WinUI/Views/Calendar/CalendarAppShell.xaml
@@ -304,6 +304,12 @@
+
+
+
+
+
+
@@ -398,3 +404,5 @@
+
+
diff --git a/Wino.Mail.WinUI/Views/Settings/AppPreferencesPage.xaml b/Wino.Mail.WinUI/Views/Settings/AppPreferencesPage.xaml
index 298401b5..379c8e05 100644
--- a/Wino.Mail.WinUI/Views/Settings/AppPreferencesPage.xaml
+++ b/Wino.Mail.WinUI/Views/Settings/AppPreferencesPage.xaml
@@ -47,6 +47,12 @@
+
+
+
+
+
+
@@ -64,3 +70,6 @@
+
+
+