From d45d3faa89f0db8ac154aab14e548f0554f3289a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Mon, 2 Mar 2026 00:44:29 +0100 Subject: [PATCH] Whats new implementation. --- .../CalendarAppShellViewModel.cs | 23 ++++- Wino.Core.Domain/BasicTypesJsonContext.cs | 3 + Wino.Core.Domain/Constants.cs | 1 + Wino.Core.Domain/Interfaces/IAppMigration.cs | 15 ++++ .../Interfaces/IDialogServiceBase.cs | 7 ++ .../Interfaces/INotificationBuilder.cs | 6 ++ Wino.Core.Domain/Interfaces/IUpdateManager.cs | 26 ++++++ .../Models/Updates/UpdateNoteSection.cs | 27 ++++++ .../Models/Updates/UpdateNotes.cs | 13 +++ .../Translations/en_US/resources.json | 5 +- Wino.Mail.ViewModels/MailAppShellViewModel.cs | 19 +++- Wino.Mail.WinUI/App.xaml.cs | 55 +++++++++++- Wino.Mail.WinUI/Assets/UpdateNotes/vnext.json | 40 +++++++++ Wino.Mail.WinUI/Dialogs/WhatIsNewDialog.xaml | 70 +++++++++++++++ .../Dialogs/WhatIsNewDialog.xaml.cs | 72 +++++++++++++++ Wino.Mail.WinUI/Services/DialogService.cs | 4 +- Wino.Mail.WinUI/Services/DialogServiceBase.cs | 15 +++- .../Services/NotificationBuilder.cs | 14 +++ Wino.Mail.WinUI/Wino.Mail.WinUI.csproj | 1 + Wino.Services/ServicesContainerSetup.cs | 1 + Wino.Services/UpdateManager.cs | 88 +++++++++++++++++++ 21 files changed, 496 insertions(+), 9 deletions(-) create mode 100644 Wino.Core.Domain/Interfaces/IAppMigration.cs create mode 100644 Wino.Core.Domain/Interfaces/IUpdateManager.cs create mode 100644 Wino.Core.Domain/Models/Updates/UpdateNoteSection.cs create mode 100644 Wino.Core.Domain/Models/Updates/UpdateNotes.cs create mode 100644 Wino.Mail.WinUI/Assets/UpdateNotes/vnext.json create mode 100644 Wino.Mail.WinUI/Dialogs/WhatIsNewDialog.xaml create mode 100644 Wino.Mail.WinUI/Dialogs/WhatIsNewDialog.xaml.cs create mode 100644 Wino.Services/UpdateManager.cs diff --git a/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs b/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs index 7ca56bba..42eaaaa1 100644 --- a/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs +++ b/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs @@ -72,10 +72,14 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel, IAccountService accountService, ICalendarService calendarService, IAccountCalendarStateService accountCalendarStateService, - INavigationService navigationService) + INavigationService navigationService, + IDialogServiceBase dialogService, + IUpdateManager updateManager) { _accountService = accountService; _calendarService = calendarService; + _dialogService = dialogService; + _updateManager = updateManager; AccountCalendarStateService = accountCalendarStateService; AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested; @@ -123,9 +127,24 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel, await InitializeAccountCalendarsAsync(); + await ShowWhatIsNewIfNeededAsync(); + TodayClicked(); } + private async Task ShowWhatIsNewIfNeededAsync() + { + if (!_updateManager.ShouldShowUpdateNotes()) + return; + + var notes = await _updateManager.GetLatestUpdateNotesAsync(); + + if (notes.Sections.Count == 0) + return; + + await _dialogService.ShowWhatIsNewDialogAsync(notes); + } + private async void AccountCalendarStateCollectivelyChanged(object sender, GroupedAccountCalendarViewModel e) { // When using three-state checkbox, multiple accounts will be selected/unselected at the same time. @@ -271,6 +290,8 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel, private DateTime? _navigationDate; private readonly IAccountService _accountService; private readonly ICalendarService _calendarService; + private readonly IDialogServiceBase _dialogService; + private readonly IUpdateManager _updateManager; #region Commands diff --git a/Wino.Core.Domain/BasicTypesJsonContext.cs b/Wino.Core.Domain/BasicTypesJsonContext.cs index b9dba02d..f6085c1c 100644 --- a/Wino.Core.Domain/BasicTypesJsonContext.cs +++ b/Wino.Core.Domain/BasicTypesJsonContext.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Text.Json.Serialization; +using Wino.Core.Domain.Models.Updates; namespace Wino.Core.Domain; @@ -8,4 +9,6 @@ namespace Wino.Core.Domain; [JsonSerializable(typeof(int))] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(bool))] +[JsonSerializable(typeof(UpdateNotes))] +[JsonSerializable(typeof(List))] public partial class BasicTypesJsonContext : JsonSerializerContext; diff --git a/Wino.Core.Domain/Constants.cs b/Wino.Core.Domain/Constants.cs index 6c57207b..9a9bb2f6 100644 --- a/Wino.Core.Domain/Constants.cs +++ b/Wino.Core.Domain/Constants.cs @@ -19,6 +19,7 @@ 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 ToastMigrationRequiredKey = nameof(ToastMigrationRequiredKey); public const string ClientLogFile = "Client_.log"; public const string ServerLogFile = "Server_.log"; diff --git a/Wino.Core.Domain/Interfaces/IAppMigration.cs b/Wino.Core.Domain/Interfaces/IAppMigration.cs new file mode 100644 index 00000000..cb2f31c2 --- /dev/null +++ b/Wino.Core.Domain/Interfaces/IAppMigration.cs @@ -0,0 +1,15 @@ +using System.Threading.Tasks; + +namespace Wino.Core.Domain.Interfaces; + +/// +/// Represents a one-time app or data migration that runs when a user updates to a new version. +/// +public interface IAppMigration +{ + /// Gets the unique identifier for this migration, used to track completion in local settings. + string MigrationId { get; } + + /// Executes the migration logic. + Task ExecuteAsync(); +} diff --git a/Wino.Core.Domain/Interfaces/IDialogServiceBase.cs b/Wino.Core.Domain/Interfaces/IDialogServiceBase.cs index 10189f83..11a9f6f3 100644 --- a/Wino.Core.Domain/Interfaces/IDialogServiceBase.cs +++ b/Wino.Core.Domain/Interfaces/IDialogServiceBase.cs @@ -5,6 +5,7 @@ using Wino.Core.Domain.Enums; using Wino.Core.Domain.Models.Accounts; using Wino.Core.Domain.Models.Common; using Wino.Core.Domain.Models.Printing; +using Wino.Core.Domain.Models.Updates; namespace Wino.Core.Domain.Interfaces; @@ -30,4 +31,10 @@ public interface IDialogServiceBase Task> PickFilesAsync(params object[] typeFilters); Task PickFilePathAsync(string saveFileName); Task ShowPrintDialogAsync(WebView2PrintSettingsModel initialSettings = null); + + /// + /// Presents the "What's New" dialog for the current version. + /// This dialog is undismissable and runs any pending migrations when the user clicks "Get Started". + /// + Task ShowWhatIsNewDialogAsync(UpdateNotes notes); } diff --git a/Wino.Core.Domain/Interfaces/INotificationBuilder.cs b/Wino.Core.Domain/Interfaces/INotificationBuilder.cs index aae5b2b2..bf43239a 100644 --- a/Wino.Core.Domain/Interfaces/INotificationBuilder.cs +++ b/Wino.Core.Domain/Interfaces/INotificationBuilder.cs @@ -40,4 +40,10 @@ public interface INotificationBuilder /// Creates a calendar reminder toast for the specified calendar item. /// Task CreateCalendarReminderNotificationAsync(CalendarItem calendarItem, long reminderDurationInSeconds); + + /// + /// Shows a notification that a migration is required for the new app version. + /// Synchronization is stopped and the user is prompted to open the app. + /// + void CreateMigrationRequiredNotification(); } diff --git a/Wino.Core.Domain/Interfaces/IUpdateManager.cs b/Wino.Core.Domain/Interfaces/IUpdateManager.cs new file mode 100644 index 00000000..9a593439 --- /dev/null +++ b/Wino.Core.Domain/Interfaces/IUpdateManager.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Wino.Core.Domain.Models.Updates; + +namespace Wino.Core.Domain.Interfaces; + +public interface IUpdateManager +{ + /// Loads and parses the update notes for the current version from the bundled asset file. + Task GetLatestUpdateNotesAsync(); + + /// Returns true if the current version's update notes have not yet been shown to the user. + bool ShouldShowUpdateNotes(); + + /// Stores a flag in local settings indicating the update notes for the current version have been seen. + void MarkUpdateNotesAsSeen(); + + /// Returns true if any registered migration has not yet been completed. + bool HasPendingMigrations(); + + /// Runs all pending migrations in order and marks each as completed in local settings. + Task RunPendingMigrationsAsync(); + + /// Registers migrations to be tracked and executed by this manager. + void RegisterMigrations(IEnumerable migrations); +} diff --git a/Wino.Core.Domain/Models/Updates/UpdateNoteSection.cs b/Wino.Core.Domain/Models/Updates/UpdateNoteSection.cs new file mode 100644 index 00000000..0bb8f7c8 --- /dev/null +++ b/Wino.Core.Domain/Models/Updates/UpdateNoteSection.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Serialization; + +namespace Wino.Core.Domain.Models.Updates; + +public class UpdateNoteSection +{ + [JsonPropertyName("title")] + public string Title { get; set; } = string.Empty; + + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + + [JsonPropertyName("imageUrl")] + public string ImageUrl { get; set; } = string.Empty; + + [JsonPropertyName("imageWidth")] + public double? ImageWidth { get; set; } + + [JsonPropertyName("imageHeight")] + public double? ImageHeight { get; set; } + + /// Gets the image width for binding, returning NaN for auto-sizing when not specified. + public double ActualImageWidth => ImageWidth ?? double.NaN; + + /// Gets the image height for binding, returning NaN for auto-sizing when not specified. + public double ActualImageHeight => ImageHeight ?? double.NaN; +} diff --git a/Wino.Core.Domain/Models/Updates/UpdateNotes.cs b/Wino.Core.Domain/Models/Updates/UpdateNotes.cs new file mode 100644 index 00000000..50e3e708 --- /dev/null +++ b/Wino.Core.Domain/Models/Updates/UpdateNotes.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Wino.Core.Domain.Models.Updates; + +public class UpdateNotes +{ + [JsonPropertyName("hasMigrations")] + public bool HasMigrations { get; set; } + + [JsonPropertyName("sections")] + public List Sections { get; set; } = []; +} diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json index ba2f5ee1..51de54c8 100644 --- a/Wino.Core.Domain/Translations/en_US/resources.json +++ b/Wino.Core.Domain/Translations/en_US/resources.json @@ -983,5 +983,8 @@ "CalendarAccountSettings_DefaultShowAs": "Default Show As Status", "CalendarAccountSettings_DefaultShowAsDescription": "Default availability status for new events created with this account", "CalendarAccountSettings_PrimaryCalendar": "Primary Calendar", - "CalendarAccountSettings_PrimaryCalendarDescription": "Mark this calendar as the primary calendar for the account" + "CalendarAccountSettings_PrimaryCalendarDescription": "Mark this calendar as the primary calendar for the account", + "WhatIsNew_GetStartedButton": "Get Started", + "WhatIsNew_MigrationNotification_Title": "Wino Mail Updated", + "WhatIsNew_MigrationNotification_Message": "Open the app to complete the update and see what's new." } diff --git a/Wino.Mail.ViewModels/MailAppShellViewModel.cs b/Wino.Mail.ViewModels/MailAppShellViewModel.cs index 2aa65413..9848c150 100644 --- a/Wino.Mail.ViewModels/MailAppShellViewModel.cs +++ b/Wino.Mail.ViewModels/MailAppShellViewModel.cs @@ -78,6 +78,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel, private readonly IMailDialogService _dialogService; private readonly IMimeFileService _mimeFileService; private readonly IWebView2RuntimeValidatorService _webView2RuntimeValidatorService; + private readonly IUpdateManager _updateManager; private readonly INativeAppService _nativeAppService; private readonly IMailService _mailService; @@ -100,7 +101,8 @@ public partial class MailAppShellViewModel : MailBaseViewModel, IStatePersistanceService statePersistanceService, IConfigurationService configurationService, IStartupBehaviorService startupBehaviorService, - IWebView2RuntimeValidatorService webView2RuntimeValidatorService) + IWebView2RuntimeValidatorService webView2RuntimeValidatorService, + IUpdateManager updateManager) { StatePersistenceService = statePersistanceService; @@ -121,6 +123,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel, _notificationBuilder = notificationBuilder; _winoRequestDelegator = winoRequestDelegator; _webView2RuntimeValidatorService = webView2RuntimeValidatorService; + _updateManager = updateManager; } protected override void OnDispatcherAssigned() @@ -251,6 +254,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel, await ForceAllAccountSynchronizationsAsync(); } + await ShowWhatIsNewIfNeededAsync(); await MakeSureEnableStartupLaunchAsync(); } @@ -264,6 +268,19 @@ public partial class MailAppShellViewModel : MailBaseViewModel, } } + private async Task ShowWhatIsNewIfNeededAsync() + { + if (!_updateManager.ShouldShowUpdateNotes()) + return; + + var notes = await _updateManager.GetLatestUpdateNotesAsync(); + + if (notes.Sections.Count == 0) + return; + + await _dialogService.ShowWhatIsNewDialogAsync(notes); + } + private async Task MakeSureEnableStartupLaunchAsync() { if (!_configurationService.Get(IsActivateStartupLaunchAskedKey, false)) diff --git a/Wino.Mail.WinUI/App.xaml.cs b/Wino.Mail.WinUI/App.xaml.cs index 161a5fcc..870bb2a3 100644 --- a/Wino.Mail.WinUI/App.xaml.cs +++ b/Wino.Mail.WinUI/App.xaml.cs @@ -22,6 +22,7 @@ using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Calendar; using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.Synchronization; +using Wino.Core.Domain.Models.Updates; using Wino.Mail.Services; using Wino.Mail.ViewModels; using Wino.Mail.WinUI.Activation; @@ -153,9 +154,18 @@ public partial class App : WinoApplication, _preferencesService = Services.GetRequiredService(); _accountService = Services.GetRequiredService(); + // Check whether the new version requires a migration before starting sync. + var updateManager = Services.GetRequiredService(); + var updateNotes = await updateManager.GetLatestUpdateNotesAsync(); + bool hasPendingMigration = updateNotes.HasMigrations && updateManager.HasPendingMigrations(); + _preferencesService.PreferenceChanged -= PreferencesServiceChanged; _preferencesService.PreferenceChanged += PreferencesServiceChanged; - RestartAutoSynchronizationLoop(); + + // Hold off sync loop when a migration is required in startup-task (tray-only) mode. + // In foreground mode the sync loop starts normally; the ViewModel dialog handles migrations before sync kicks in. + if (!hasPendingMigration || !IsStartupTaskLaunch()) + RestartAutoSynchronizationLoop(); // Check if launched from toast notification. if (IsNotificationActivation(out AppNotificationActivatedEventArgs toastArgs)) @@ -179,12 +189,21 @@ public partial class App : WinoApplication, // Otherwise, activate the window normally. if (isStartupTaskLaunch) { - LogActivation("Launched by startup task. Window created but hidden (system tray only)."); - // Window is created but not activated. User can show it from system tray. + if (hasPendingMigration) + { + // Notify the user to open the app to complete the update before sync can resume. + Services.GetRequiredService().CreateMigrationRequiredNotification(); + LogActivation("Migration required for new version. Sync skipped. User notified via toast."); + } + else + { + LogActivation("Launched by startup task. Window created but hidden (system tray only)."); + } } else { // Normal launch - show and activate the window. + // The What's New dialog is shown from MailAppShellViewModel.OnNavigatedTo once XamlRoot is ready. MainWindow?.Activate(); LogActivation("Window created and activated."); } @@ -222,6 +241,13 @@ public partial class App : WinoApplication, { var toastArguments = ToastArguments.Parse(toastArgs.Argument); + // Check migration notification activation first. + if (toastArguments.Contains(Constants.ToastMigrationRequiredKey)) + { + await HandleMigrationToastActivationAsync(); + return; + } + // Check calendar reminder toast activation first. if (toastArguments.TryGetValue(Constants.ToastCalendarActionKey, out string calendarAction) && calendarAction == Constants.ToastCalendarNavigateAction && @@ -251,6 +277,29 @@ public partial class App : WinoApplication, } } + /// + /// Handles activation from the migration-required toast notification. + /// Opens the app so the shell ViewModel can show the What's New dialog and run migrations. + /// + private async Task HandleMigrationToastActivationAsync() + { + LogActivation("Handling migration toast activation."); + + if (!IsAppRunning()) + { + await CreateAndActivateWindow(null!); + } + else + { + EnsureMainWindowVisibleAndForeground(); + } + + // The MailAppShellViewModel.OnNavigatedTo will detect ShouldShowUpdateNotes() == true + // and display the What's New dialog (including running migrations) once the XamlRoot is ready. + // Restart sync in case it was blocked. + RestartAutoSynchronizationLoop(); + } + private async Task HandleCalendarToastNavigationAsync(Guid calendarItemId) { var calendarService = Services.GetRequiredService(); diff --git a/Wino.Mail.WinUI/Assets/UpdateNotes/vnext.json b/Wino.Mail.WinUI/Assets/UpdateNotes/vnext.json new file mode 100644 index 00000000..628505ad --- /dev/null +++ b/Wino.Mail.WinUI/Assets/UpdateNotes/vnext.json @@ -0,0 +1,40 @@ +{ + "hasMigrations": false, + "sections": [ + { + "title": "# Wino Calendar is here!", + "description": "You can now create local or remote CalDAV-compatible calendars, manage recurring events, and respond to invitations — all from within Wino.", + "imageUrl": "ms-appx:///Assets/AboutPageTile.png", + "imageWidth": 128, + "imageHeight": 128 + }, + { + "title": "# S/MIME Signing & Encryption", + "description": "Wino now supports signing and encrypting your emails with personal certificates. Keep your communications secure and verifiable.", + "imageUrl": "ms-appx:///Assets/AboutPageTile.png", + "imageWidth": 128, + "imageHeight": 128 + }, + { + "title": "# Threaded Mail View", + "description": "Emails are now grouped by conversation, making it easier to follow long discussions without losing context.", + "imageUrl": "ms-appx:///Assets/AboutPageTile.png", + "imageWidth": 128, + "imageHeight": 128 + }, + { + "title": "# Smarter Notifications", + "description": "Act on your emails directly from toast notifications — mark as read, delete, or archive without opening the app.", + "imageUrl": "ms-appx:///Assets/AboutPageTile.png", + "imageWidth": 128, + "imageHeight": 128 + }, + { + "title": "# And much more...", + "description": "Folder management, swipe actions, keyboard shortcuts, a custom print dialog, and significant performance improvements are all included in this release.\n\nThank you for using Wino Mail!", + "imageUrl": "ms-appx:///Assets/AboutPageTile.png", + "imageWidth": 128, + "imageHeight": 128 + } + ] +} diff --git a/Wino.Mail.WinUI/Dialogs/WhatIsNewDialog.xaml b/Wino.Mail.WinUI/Dialogs/WhatIsNewDialog.xaml new file mode 100644 index 00000000..fb7c1c14 --- /dev/null +++ b/Wino.Mail.WinUI/Dialogs/WhatIsNewDialog.xaml @@ -0,0 +1,70 @@ + + + + 480 + 560 + 480 + 700 + + + + + + + + + + + + + + + + + + + + + + + + + +