From c1568d33e6c25361c187cd8cd3ed501f85afd253 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Sun, 8 Mar 2026 11:22:41 +0100 Subject: [PATCH] Live store update notifications. --- .../CalendarAppShellViewModel.cs | 104 +++++++++++++----- Wino.Core.Domain/Constants.cs | 5 +- .../Interfaces/INotificationBuilder.cs | 6 + .../Interfaces/IPreferencesService.cs | 8 +- .../Interfaces/IStoreUpdateService.cs | 12 ++ .../MenuItems/StoreUpdateMenuItem.cs | 3 + .../Translations/en_US/resources.json | 7 ++ Wino.Mail.ViewModels/MailAppShellViewModel.cs | 52 ++++++++- Wino.Mail.WinUI/App.xaml.cs | 24 ++++ Wino.Mail.WinUI/CoreUWPContainerSetup.cs | 3 + Wino.Mail.WinUI/MailAppShell.xaml | 5 +- .../NavigationMenuTemplateSelector.cs | 5 + .../Services/NotificationBuilder.cs | 14 +++ .../Services/PreferencesService.cs | 7 ++ .../Services/StoreUpdateService.cs | 94 ++++++++++++++++ Wino.Mail.WinUI/Styles/DataTemplates.xaml | 12 ++ .../Views/Calendar/CalendarAppShell.xaml | 8 ++ .../Views/Settings/AppPreferencesPage.xaml | 9 ++ 18 files changed, 341 insertions(+), 37 deletions(-) create mode 100644 Wino.Core.Domain/Interfaces/IStoreUpdateService.cs create mode 100644 Wino.Core.Domain/MenuItems/StoreUpdateMenuItem.cs create mode 100644 Wino.Mail.WinUI/Services/StoreUpdateService.cs 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 @@ + + +