From bab3272970bf889bd34882b282d8a5d742c902b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Wed, 21 Aug 2024 19:50:14 +0200 Subject: [PATCH 01/10] Fix couple potential crash issues due to unsupported APIs. --- Wino.Core.UWP/Services/NotificationBuilder.cs | 148 +++++++++--------- 1 file changed, 77 insertions(+), 71 deletions(-) diff --git a/Wino.Core.UWP/Services/NotificationBuilder.cs b/Wino.Core.UWP/Services/NotificationBuilder.cs index c11aa04f..c06c1ccb 100644 --- a/Wino.Core.UWP/Services/NotificationBuilder.cs +++ b/Wino.Core.UWP/Services/NotificationBuilder.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using CommunityToolkit.WinUI.Notifications; +using Serilog; using Windows.Data.Xml.Dom; using Windows.UI.Notifications; using Wino.Core.Domain; @@ -38,83 +39,90 @@ namespace Wino.Core.UWP.Services { var mailCount = downloadedMailItems.Count(); - // If there are more than 3 mails, just display 1 general toast. - if (mailCount > 3) + try { - var builder = new ToastContentBuilder(); - builder.SetToastScenario(ToastScenario.Default); - - builder.AddText(Translator.Notifications_MultipleNotificationsTitle); - builder.AddText(string.Format(Translator.Notifications_MultipleNotificationsMessage, mailCount)); - - builder.AddButton(GetDismissButton()); - - builder.Show(); - } - else - { - var validItems = new List(); - - // Fetch mails again to fill up assigned folder data and latest statuses. - // They've been marked as read by executing synchronizer tasks until inital sync finishes. - - foreach (var item in downloadedMailItems) + // If there are more than 3 mails, just display 1 general toast. + if (mailCount > 3) { - var mailItem = await _mailService.GetSingleMailItemAsync(item.UniqueId); - - if (mailItem != null && mailItem.AssignedFolder != null) - { - validItems.Add(mailItem); - } - } - - foreach (var mailItem in validItems) - { - //if (mailItem.IsRead) - // continue; - var builder = new ToastContentBuilder(); builder.SetToastScenario(ToastScenario.Default); - var host = ThumbnailService.GetHost(mailItem.FromAddress); + builder.AddText(Translator.Notifications_MultipleNotificationsTitle); + builder.AddText(string.Format(Translator.Notifications_MultipleNotificationsMessage, mailCount)); - var knownTuple = ThumbnailService.CheckIsKnown(host); - - bool isKnown = knownTuple.Item1; - host = knownTuple.Item2; - - if (isKnown) - builder.AddAppLogoOverride(new System.Uri(ThumbnailService.GetKnownHostImage(host)), hintCrop: ToastGenericAppLogoCrop.Default); - else - { - // TODO: https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts?tabs=toolkit - // Follow official guides for icons/theme. - - bool isOSDarkTheme = _underlyingThemeService.IsUnderlyingThemeDark(); - string profileLogoName = isOSDarkTheme ? "profile-dark.png" : "profile-light.png"; - - builder.AddAppLogoOverride(new System.Uri($"ms-appx:///Assets/NotificationIcons/{profileLogoName}"), hintCrop: ToastGenericAppLogoCrop.Circle); - } - - // Override system notification timetamp with received date of the mail. - // It may create confusion for some users, but still it's the truth... - 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.AddButton(GetMarkedAsRead(mailItem.UniqueId)); - builder.AddButton(GetDeleteButton(mailItem.UniqueId)); builder.AddButton(GetDismissButton()); builder.Show(); } + else + { + var validItems = new List(); - await UpdateTaskbarIconBadgeAsync(); + // Fetch mails again to fill up assigned folder data and latest statuses. + // They've been marked as read by executing synchronizer tasks until inital sync finishes. + + foreach (var item in downloadedMailItems) + { + var mailItem = await _mailService.GetSingleMailItemAsync(item.UniqueId); + + if (mailItem != null && mailItem.AssignedFolder != null) + { + validItems.Add(mailItem); + } + } + + foreach (var mailItem in validItems) + { + if (mailItem.IsRead) + continue; + + var builder = new ToastContentBuilder(); + builder.SetToastScenario(ToastScenario.Default); + + var host = ThumbnailService.GetHost(mailItem.FromAddress); + + var knownTuple = ThumbnailService.CheckIsKnown(host); + + bool isKnown = knownTuple.Item1; + host = knownTuple.Item2; + + if (isKnown) + builder.AddAppLogoOverride(new System.Uri(ThumbnailService.GetKnownHostImage(host)), hintCrop: ToastGenericAppLogoCrop.Default); + else + { + // TODO: https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts?tabs=toolkit + // Follow official guides for icons/theme. + + bool isOSDarkTheme = _underlyingThemeService.IsUnderlyingThemeDark(); + string profileLogoName = isOSDarkTheme ? "profile-dark.png" : "profile-light.png"; + + builder.AddAppLogoOverride(new System.Uri($"ms-appx:///Assets/NotificationIcons/{profileLogoName}"), hintCrop: ToastGenericAppLogoCrop.Circle); + } + + // Override system notification timetamp with received date of the mail. + // It may create confusion for some users, but still it's the truth... + 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.AddButton(GetMarkedAsRead(mailItem.UniqueId)); + builder.AddButton(GetDeleteButton(mailItem.UniqueId)); + builder.AddButton(GetDismissButton()); + + builder.Show(); + } + + await UpdateTaskbarIconBadgeAsync(); + } + } + catch (Exception ex) + { + Log.Error(ex, "Failed to create notifications."); } } @@ -142,10 +150,10 @@ namespace Wino.Core.UWP.Services public async Task UpdateTaskbarIconBadgeAsync() { int totalUnreadCount = 0; - var badgeUpdater = BadgeUpdateManager.CreateBadgeUpdaterForApplication(); try { + var badgeUpdater = BadgeUpdateManager.CreateBadgeUpdaterForApplication(); var accounts = await _accountService.GetAccountsAsync(); foreach (var account in accounts) @@ -178,11 +186,9 @@ namespace Wino.Core.UWP.Services else badgeUpdater.Clear(); } - catch (System.Exception ex) + catch (Exception ex) { - // TODO: Log exceptions. - - badgeUpdater.Clear(); + Log.Error(ex, "Error while updating taskbar badge."); } } From f627226da9519fffdc8ec27a3b2a78394aedcd89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Wed, 21 Aug 2024 22:42:52 +0200 Subject: [PATCH 02/10] Server termination and refactoring message dialogs. --- .../Enums/WinoCustomMessageDialogIcon.cs | 10 + .../Interfaces/IConfirmationDialog.cs | 9 - Wino.Core.Domain/Interfaces/IDialogService.cs | 14 +- .../Interfaces/IStoreRatingDialog.cs | 8 - Wino.Core.UWP/Services/StoreRatingService.cs | 22 +-- Wino.Mail.ViewModels/AboutPageViewModel.cs | 4 +- .../AliasManagementPageViewModel.cs | 16 +- Wino.Mail.ViewModels/AppShellViewModel.cs | 45 ++++- Wino.Mail.ViewModels/ComposePageViewModel.cs | 4 +- .../NewMailItemRenderingRequestedEvent.cs | 11 +- Wino.Mail/App.xaml | 9 +- Wino.Mail/App.xaml.cs | 87 +++++++++ Wino.Mail/Dialogs/ConfirmationDialog.xaml | 29 --- Wino.Mail/Dialogs/ConfirmationDialog.xaml.cs | 80 -------- ...CustomMessageDialogInformationContainer.cs | 24 +++ Wino.Mail/Dialogs/StoreRatingDialog.xaml | 24 --- Wino.Mail/Dialogs/StoreRatingDialog.xaml.cs | 21 -- Wino.Mail/Dialogs/WinoMessageDialog.xaml | 25 --- Wino.Mail/Dialogs/WinoMessageDialog.xaml.cs | 60 ------ .../CustomWinoMessageDialogIconSelector.cs | 38 ++++ Wino.Mail/Services/DialogService.cs | 182 +++++++++++------- .../Styles/CustomMessageDialogStyles.xaml | 89 +++++++++ .../Styles/CustomMessageDialogStyles.xaml.cs | 12 ++ Wino.Mail/Views/SettingsPage.xaml.cs | 10 + Wino.Mail/Wino.Mail.csproj | 41 ++-- .../NavigateAppPreferencesRequested.cs | 7 + .../Server/TerminateServerRequested.cs | 9 + Wino.Packaging/Package.appxmanifest | 1 + .../Core/ServerMessageHandlerFactory.cs | 2 + .../TerminateServerRequestHandler.cs | 26 +++ Wino.Server/ServerContext.cs | 17 ++ 31 files changed, 548 insertions(+), 388 deletions(-) create mode 100644 Wino.Core.Domain/Enums/WinoCustomMessageDialogIcon.cs delete mode 100644 Wino.Core.Domain/Interfaces/IConfirmationDialog.cs delete mode 100644 Wino.Core.Domain/Interfaces/IStoreRatingDialog.cs delete mode 100644 Wino.Mail/Dialogs/ConfirmationDialog.xaml delete mode 100644 Wino.Mail/Dialogs/ConfirmationDialog.xaml.cs create mode 100644 Wino.Mail/Dialogs/CustomMessageDialogInformationContainer.cs delete mode 100644 Wino.Mail/Dialogs/StoreRatingDialog.xaml delete mode 100644 Wino.Mail/Dialogs/StoreRatingDialog.xaml.cs delete mode 100644 Wino.Mail/Dialogs/WinoMessageDialog.xaml delete mode 100644 Wino.Mail/Dialogs/WinoMessageDialog.xaml.cs create mode 100644 Wino.Mail/Selectors/CustomWinoMessageDialogIconSelector.cs create mode 100644 Wino.Mail/Styles/CustomMessageDialogStyles.xaml create mode 100644 Wino.Mail/Styles/CustomMessageDialogStyles.xaml.cs create mode 100644 Wino.Messages/Client/Navigation/NavigateAppPreferencesRequested.cs create mode 100644 Wino.Messages/Server/TerminateServerRequested.cs create mode 100644 Wino.Server/MessageHandlers/TerminateServerRequestHandler.cs diff --git a/Wino.Core.Domain/Enums/WinoCustomMessageDialogIcon.cs b/Wino.Core.Domain/Enums/WinoCustomMessageDialogIcon.cs new file mode 100644 index 00000000..c85bb5af --- /dev/null +++ b/Wino.Core.Domain/Enums/WinoCustomMessageDialogIcon.cs @@ -0,0 +1,10 @@ +namespace Wino.Core.Domain.Enums +{ + public enum WinoCustomMessageDialogIcon + { + Information, + Warning, + Error, + Question + } +} diff --git a/Wino.Core.Domain/Interfaces/IConfirmationDialog.cs b/Wino.Core.Domain/Interfaces/IConfirmationDialog.cs deleted file mode 100644 index 34630448..00000000 --- a/Wino.Core.Domain/Interfaces/IConfirmationDialog.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Threading.Tasks; - -namespace Wino.Core.Domain.Interfaces -{ - public interface IConfirmationDialog - { - Task ShowDialogAsync(string title, string message, string approveButtonTitle); - } -} diff --git a/Wino.Core.Domain/Interfaces/IDialogService.cs b/Wino.Core.Domain/Interfaces/IDialogService.cs index 9d5fad1b..f0754684 100644 --- a/Wino.Core.Domain/Interfaces/IDialogService.cs +++ b/Wino.Core.Domain/Interfaces/IDialogService.cs @@ -15,11 +15,10 @@ namespace Wino.Core.Domain.Interfaces Task PickWindowsFileContentAsync(params object[] typeFilters); Task ShowConfirmationDialogAsync(string question, string title, string confirmationButtonTitle); Task ShowHardDeleteConfirmationAsync(); - Task ShowRatingDialogAsync(); Task HandleSystemFolderConfigurationDialogAsync(Guid accountId, IFolderService folderService); Task ShowCustomThemeBuilderDialogAsync(); - Task ShowMessageAsync(string message, string title); + Task ShowMessageAsync(string message, string title, WinoCustomMessageDialogIcon icon); void InfoBarMessage(string title, string message, InfoBarMessageType messageType); void InfoBarMessage(string title, string message, InfoBarMessageType messageType, string actionButtonText, Action action); @@ -53,6 +52,17 @@ namespace Wino.Core.Domain.Interfaces /// /// Signature information. Null if canceled. Task ShowSignatureEditorDialog(AccountSignature signatureModel = null); + + /// + /// Presents a dialog to the user for account alias creation/modification. + /// + /// Created alias model if not canceled. Task ShowCreateAccountAliasDialogAsync(); + Task ShowWinoCustomMessageDialogAsync(string title, + string description, + string approveButtonText, + WinoCustomMessageDialogIcon? icon, + string cancelButtonText = "", + string dontAskAgainConfigurationKey = ""); } } diff --git a/Wino.Core.Domain/Interfaces/IStoreRatingDialog.cs b/Wino.Core.Domain/Interfaces/IStoreRatingDialog.cs deleted file mode 100644 index c56ef4a4..00000000 --- a/Wino.Core.Domain/Interfaces/IStoreRatingDialog.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Wino.Core.Domain.Interfaces -{ - public interface IStoreRatingDialog - { - bool DontAskAgain { get; } - bool RateWinoClicked { get; } - } -} diff --git a/Wino.Core.UWP/Services/StoreRatingService.cs b/Wino.Core.UWP/Services/StoreRatingService.cs index df33dcab..d6852459 100644 --- a/Wino.Core.UWP/Services/StoreRatingService.cs +++ b/Wino.Core.UWP/Services/StoreRatingService.cs @@ -11,7 +11,7 @@ namespace Wino.Core.UWP.Services { public class StoreRatingService : IStoreRatingService { - private const string RatedStorageKey = nameof(RatedStorageKey); + private const string RatedStorageKey = "a"; // nameof(RatedStorageKey); private const string LatestAskedKey = nameof(LatestAskedKey); private readonly IConfigurationService _configurationService; @@ -23,9 +23,6 @@ namespace Wino.Core.UWP.Services _dialogService = dialogService; } - private void SetRated() - => _configurationService.SetRoaming(RatedStorageKey, true); - private bool IsAskingThresholdExceeded() { var latestAskedDate = _configurationService.Get(LatestAskedKey, DateTime.MinValue); @@ -62,15 +59,14 @@ namespace Wino.Core.UWP.Services { if (!IsAskingThresholdExceeded()) return; - var ratingDialogResult = await _dialogService.ShowRatingDialogAsync(); + var isRateWinoApproved = await _dialogService.ShowWinoCustomMessageDialogAsync(Translator.StoreRatingDialog_Title, + Translator.StoreRatingDialog_MessageFirstLine, + Translator.Buttons_RateWino, + Domain.Enums.WinoCustomMessageDialogIcon.Question, + Translator.Buttons_No, + RatedStorageKey); - if (ratingDialogResult == null) - return; - - if (ratingDialogResult.DontAskAgain) - SetRated(); - - if (ratingDialogResult.RateWinoClicked) + if (isRateWinoApproved) { // In case of failure of this call, we will navigate users to Store page directly. @@ -107,7 +103,7 @@ namespace Wino.Core.UWP.Services else _dialogService.InfoBarMessage(Translator.Info_ReviewSuccessTitle, Translator.Info_ReviewNewMessage, Domain.Enums.InfoBarMessageType.Success); - SetRated(); + _configurationService.Set(RatedStorageKey, true); break; case StoreRateAndReviewStatus.CanceledByUser: break; diff --git a/Wino.Mail.ViewModels/AboutPageViewModel.cs b/Wino.Mail.ViewModels/AboutPageViewModel.cs index a767c199..9496e05d 100644 --- a/Wino.Mail.ViewModels/AboutPageViewModel.cs +++ b/Wino.Mail.ViewModels/AboutPageViewModel.cs @@ -95,7 +95,9 @@ namespace Wino.Mail.ViewModels { // Discord disclaimer message about server. if (stringUrl == DiscordChannelUrl) - await DialogService.ShowMessageAsync(Translator.DiscordChannelDisclaimerMessage, Translator.DiscordChannelDisclaimerTitle); + await DialogService.ShowMessageAsync(Translator.DiscordChannelDisclaimerMessage, + Translator.DiscordChannelDisclaimerTitle, + WinoCustomMessageDialogIcon.Warning); await _nativeAppService.LaunchUriAsync(new Uri(stringUrl)); } diff --git a/Wino.Mail.ViewModels/AliasManagementPageViewModel.cs b/Wino.Mail.ViewModels/AliasManagementPageViewModel.cs index c11b0daa..32a77d4d 100644 --- a/Wino.Mail.ViewModels/AliasManagementPageViewModel.cs +++ b/Wino.Mail.ViewModels/AliasManagementPageViewModel.cs @@ -98,14 +98,18 @@ namespace Wino.Mail.ViewModels // Check existence. if (AccountAliases.Any(a => a.AliasAddress == newAlias.AliasAddress)) { - await DialogService.ShowMessageAsync(Translator.DialogMessage_AliasExistsTitle, Translator.DialogMessage_AliasExistsMessage); + await DialogService.ShowMessageAsync(Translator.DialogMessage_AliasExistsTitle, + Translator.DialogMessage_AliasExistsMessage, + WinoCustomMessageDialogIcon.Warning); return; } // Validate all addresses. if (!EmailValidator.Validate(newAlias.AliasAddress) || (!string.IsNullOrEmpty(newAlias.ReplyToAddress) && !EmailValidator.Validate(newAlias.ReplyToAddress))) { - await DialogService.ShowMessageAsync(Translator.DialogMessage_InvalidAliasMessage, Translator.DialogMessage_InvalidAliasTitle); + await DialogService.ShowMessageAsync(Translator.DialogMessage_InvalidAliasMessage, + Translator.DialogMessage_InvalidAliasTitle, + WinoCustomMessageDialogIcon.Warning); return; } @@ -125,14 +129,18 @@ namespace Wino.Mail.ViewModels // Primary aliases can't be deleted. if (alias.IsPrimary) { - await DialogService.ShowMessageAsync(Translator.Info_CantDeletePrimaryAliasMessage, Translator.GeneralTitle_Warning); + await DialogService.ShowMessageAsync(Translator.Info_CantDeletePrimaryAliasMessage, + Translator.GeneralTitle_Warning, + WinoCustomMessageDialogIcon.Warning); return; } // Root aliases can't be deleted. if (alias.IsRootAlias) { - await DialogService.ShowMessageAsync(Translator.DialogMessage_CantDeleteRootAliasTitle, Translator.DialogMessage_CantDeleteRootAliasMessage); + await DialogService.ShowMessageAsync(Translator.DialogMessage_CantDeleteRootAliasTitle, + Translator.DialogMessage_CantDeleteRootAliasMessage, + WinoCustomMessageDialogIcon.Warning); return; } diff --git a/Wino.Mail.ViewModels/AppShellViewModel.cs b/Wino.Mail.ViewModels/AppShellViewModel.cs index 0acc62da..b87bd135 100644 --- a/Wino.Mail.ViewModels/AppShellViewModel.cs +++ b/Wino.Mail.ViewModels/AppShellViewModel.cs @@ -37,7 +37,8 @@ namespace Wino.Mail.ViewModels IRecipient, IRecipient, IRecipient, - IRecipient + IRecipient, + IRecipient { #region Menu Items @@ -65,6 +66,7 @@ namespace Wino.Mail.ViewModels public IWinoNavigationService NavigationService { get; } private readonly IFolderService _folderService; + private readonly IConfigurationService _configurationService; private readonly IAccountService _accountService; private readonly IContextMenuItemService _contextMenuItemService; private readonly IStoreRatingService _storeRatingService; @@ -98,7 +100,8 @@ namespace Wino.Mail.ViewModels IWinoRequestDelegator winoRequestDelegator, IFolderService folderService, IStatePersistanceService statePersistanceService, - IWinoServerConnectionManager serverConnectionManager) : base(dialogService) + IWinoServerConnectionManager serverConnectionManager, + IConfigurationService configurationService) : base(dialogService) { StatePersistenceService = statePersistanceService; ServerConnectionManager = serverConnectionManager; @@ -115,6 +118,7 @@ namespace Wino.Mail.ViewModels PreferencesService = preferencesService; NavigationService = navigationService; + _configurationService = configurationService; _backgroundTaskService = backgroundTaskService; _mimeFileService = mimeFileService; _nativeAppService = nativeAppService; @@ -229,7 +233,6 @@ namespace Wino.Mail.ViewModels public override async void OnNavigatedTo(NavigationMode mode, object parameters) { base.OnNavigatedTo(mode, parameters); - await CreateFooterItemsAsync(); await RecreateMenuItemsAsync(); @@ -527,7 +530,7 @@ namespace Wino.Mail.ViewModels StatePersistenceService.CoreWindowTitle = "Wino Mail"; } - public async Task MenuItemInvokedOrSelectedAsync(IMenuItem clickedMenuItem) + public async Task MenuItemInvokedOrSelectedAsync(IMenuItem clickedMenuItem, object parameter = null) { if (clickedMenuItem == null) return; @@ -561,11 +564,11 @@ namespace Wino.Mail.ViewModels } else if (clickedMenuItem is SettingsItem) { - NavigationService.Navigate(WinoPage.SettingsPage); + NavigationService.Navigate(WinoPage.SettingsPage, parameter, NavigationReferenceFrame.ShellFrame, NavigationTransitionType.None); } else if (clickedMenuItem is ManageAccountsMenuItem) { - NavigationService.Navigate(WinoPage.AccountManagementPage); + NavigationService.Navigate(WinoPage.AccountManagementPage, parameter, NavigationReferenceFrame.ShellFrame, NavigationTransitionType.None); } else if (clickedMenuItem is IAccountMenuItem clickedAccountMenuItem && latestSelectedAccountMenuItem != clickedAccountMenuItem) { @@ -721,7 +724,19 @@ namespace Wino.Mail.ViewModels if (!accounts.Any()) { - await DialogService.ShowMessageAsync(Translator.DialogMessage_NoAccountsForCreateMailMessage, Translator.DialogMessage_NoAccountsForCreateMailTitle); + var isManageAccountClicked = await DialogService.ShowWinoCustomMessageDialogAsync(Translator.DialogMessage_NoAccountsForCreateMailMessage, + Translator.DialogMessage_NoAccountsForCreateMailTitle, + Translator.MenuManageAccounts, + WinoCustomMessageDialogIcon.Information, + string.Empty); + + + + if (isManageAccountClicked) + { + SelectedMenuItem = ManageAccountsMenuItem; + } + return; } @@ -794,11 +809,16 @@ namespace Wino.Mail.ViewModels protected override async void OnAccountUpdated(MailAccount updatedAccount) { - await ExecuteUIThread(() => + await ExecuteUIThread(async () => { if (MenuItems.TryGetAccountMenuItem(updatedAccount.Id, out IAccountMenuItem foundAccountMenuItem)) { foundAccountMenuItem.UpdateAccount(updatedAccount); + + if (latestSelectedAccountMenuItem == foundAccountMenuItem) + { + await ChangeLoadedAccountAsync(foundAccountMenuItem, false); + } } }); } @@ -850,7 +870,9 @@ namespace Wino.Mail.ViewModels if (!accounts.Any()) { - await DialogService.ShowMessageAsync(Translator.DialogMessage_NoAccountsForCreateMailMessage, Translator.DialogMessage_NoAccountsForCreateMailTitle); + await DialogService.ShowMessageAsync(Translator.DialogMessage_NoAccountsForCreateMailMessage, + Translator.DialogMessage_NoAccountsForCreateMailTitle, + WinoCustomMessageDialogIcon.Warning); } else if (accounts.Count == 1) { @@ -961,5 +983,10 @@ namespace Wino.Mail.ViewModels await ExecuteUIThread(() => { accountMenuItem.SynchronizationProgress = message.Progress; }); } + + public async void Receive(NavigateAppPreferencesRequested message) + { + await MenuItemInvokedOrSelectedAsync(SettingsItem, WinoPage.AppPreferencesPage); + } } } diff --git a/Wino.Mail.ViewModels/ComposePageViewModel.cs b/Wino.Mail.ViewModels/ComposePageViewModel.cs index f91ca00a..fb84b3ae 100644 --- a/Wino.Mail.ViewModels/ComposePageViewModel.cs +++ b/Wino.Mail.ViewModels/ComposePageViewModel.cs @@ -161,7 +161,9 @@ namespace Wino.Mail.ViewModels if (!ToItems.Any()) { - await DialogService.ShowMessageAsync(Translator.DialogMessage_ComposerMissingRecipientMessage, Translator.DialogMessage_ComposerValidationFailedTitle); + await DialogService.ShowMessageAsync(Translator.DialogMessage_ComposerMissingRecipientMessage, + Translator.DialogMessage_ComposerValidationFailedTitle, + WinoCustomMessageDialogIcon.Warning); return; } diff --git a/Wino.Mail.ViewModels/Messages/NewMailItemRenderingRequestedEvent.cs b/Wino.Mail.ViewModels/Messages/NewMailItemRenderingRequestedEvent.cs index 49fa8ab6..6da28499 100644 --- a/Wino.Mail.ViewModels/Messages/NewMailItemRenderingRequestedEvent.cs +++ b/Wino.Mail.ViewModels/Messages/NewMailItemRenderingRequestedEvent.cs @@ -6,13 +6,6 @@ namespace Wino.Mail.ViewModels.Messages /// When the rendering page is active, but new item is requested to be rendered. /// To not trigger navigation again and re-use existing Chromium. /// - public class NewMailItemRenderingRequestedEvent - { - public NewMailItemRenderingRequestedEvent(MailItemViewModel mailItemViewModel) - { - MailItemViewModel = mailItemViewModel; - } - - public MailItemViewModel MailItemViewModel { get; } - } + /// + public record NewMailItemRenderingRequestedEvent(MailItemViewModel MailItemViewModel); } diff --git a/Wino.Mail/App.xaml b/Wino.Mail/App.xaml index 57cb3f20..57476ef1 100644 --- a/Wino.Mail/App.xaml +++ b/Wino.Mail/App.xaml @@ -4,7 +4,9 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="using:Wino.Controls" xmlns:selectors="using:Wino.Selectors" - xmlns:wino="using:Wino"> + xmlns:wino="using:Wino" + xmlns:dialogs="using:Wino.Core.Domain.Models.Dialogs" + xmlns:styles="using:Wino.Styles"> @@ -18,6 +20,7 @@ + @@ -166,6 +169,10 @@ + + + + diff --git a/Wino.Mail/App.xaml.cs b/Wino.Mail/App.xaml.cs index 25086165..8fc1a810 100644 --- a/Wino.Mail/App.xaml.cs +++ b/Wino.Mail/App.xaml.cs @@ -21,6 +21,7 @@ using Windows.Foundation.Metadata; using Windows.Storage; using Windows.System.Profile; using Windows.UI; +using Windows.UI.Core.Preview; using Windows.UI.Notifications; using Windows.UI.ViewManagement; using Windows.UI.Xaml; @@ -38,6 +39,7 @@ using Wino.Core.UWP; using Wino.Core.UWP.Services; using Wino.Mail.ViewModels; using Wino.Messaging.Client.Connection; +using Wino.Messaging.Client.Navigation; using Wino.Messaging.Server; using Wino.Services; @@ -107,6 +109,78 @@ namespace Wino WeakReferenceMessenger.Default.Register(this); } + private async void ApplicationCloseRequested(object sender, SystemNavigationCloseRequestedPreviewEventArgs e) + { + var deferral = e.GetDeferral(); + + // Wino should notify user on app close if: + // 1. User has at least 1 registered account. + // 2. Startup behavior is not Enabled. + // 3. Server terminate behavior is set to Terminate. + + var accountService = Services.GetService(); + + var accounts = await accountService.GetAccountsAsync(); + + if (accounts.Count > 0) + { + // User has some accounts. Check if Wino Server runs on system startup. + + var dialogService = Services.GetService(); + var startupBehaviorService = Services.GetService(); + var preferencesService = Services.GetService(); + + var currentStartupBehavior = await startupBehaviorService.GetCurrentStartupBehaviorAsync(); + + bool? isGoToAppPreferencesRequested = null; + + if (preferencesService.ServerTerminationBehavior == ServerBackgroundMode.Terminate) + { + // Starting the server is fine, but check if server termination behavior is set to terminate. + // This state will kill the server once the app is terminated. + + isGoToAppPreferencesRequested = await _dialogService.ShowWinoCustomMessageDialogAsync("Background Synchronization", + "You are terminating Wino Mail and your app close behavior is set to 'Terminate'.\nThis will stop all background synchronizations and notifications.\nDo you want to go to App Preferences to set Wino Mail to run minimized or in the background?", + Translator.Buttons_Yes, + WinoCustomMessageDialogIcon.Warning, + Translator.Buttons_No, + "DontAskTerminateServerBehavior"); + } + + if (isGoToAppPreferencesRequested == null && currentStartupBehavior != StartupBehaviorResult.Enabled) + { + // Startup behavior is not enabled. + + isGoToAppPreferencesRequested = await dialogService.ShowWinoCustomMessageDialogAsync("Background Synchronization", + "Application has not been set to launch on Windows startup.\nThis may impact notifications and background synchronization.\nDo you want to go to App Preferences page to enable it?", + Translator.Buttons_Yes, + WinoCustomMessageDialogIcon.Warning, + Translator.Buttons_No, + "DontAskDisabledStartup"); + } + + if (isGoToAppPreferencesRequested == true) + { + WeakReferenceMessenger.Default.Send(new NavigateAppPreferencesRequested()); + e.Handled = true; + } + else if (preferencesService.ServerTerminationBehavior == ServerBackgroundMode.Terminate) + { + try + { + var isServerKilled = await _appServiceConnectionManager.GetResponseAsync(new TerminateServerRequested()); + Log.Information("Server is killed."); + } + catch (Exception ex) + { + Log.Error(ex, "Failed to kill server."); + } + } + } + + deferral.Complete(); + } + private async void OnResuming(object sender, object e) { // App Service connection was lost on suspension. @@ -233,6 +307,7 @@ namespace Wino LogActivation("Window is created."); ConfigureTitleBar(); + TryRegisterAppCloseChange(); } protected override async void OnLaunched(LaunchActivatedEventArgs args) @@ -245,6 +320,18 @@ namespace Wino } } + private void TryRegisterAppCloseChange() + { + try + { + var systemNavigationManagerPreview = SystemNavigationManagerPreview.GetForCurrentView(); + + systemNavigationManagerPreview.CloseRequested -= ApplicationCloseRequested; + systemNavigationManagerPreview.CloseRequested += ApplicationCloseRequested; + } + catch { } + } + protected override async void OnFileActivated(FileActivatedEventArgs args) { base.OnFileActivated(args); diff --git a/Wino.Mail/Dialogs/ConfirmationDialog.xaml b/Wino.Mail/Dialogs/ConfirmationDialog.xaml deleted file mode 100644 index 5f7c5070..00000000 --- a/Wino.Mail/Dialogs/ConfirmationDialog.xaml +++ /dev/null @@ -1,29 +0,0 @@ - - - - 250 - 500 - 200 - 756 - - - - diff --git a/Wino.Mail/Dialogs/ConfirmationDialog.xaml.cs b/Wino.Mail/Dialogs/ConfirmationDialog.xaml.cs deleted file mode 100644 index a2257157..00000000 --- a/Wino.Mail/Dialogs/ConfirmationDialog.xaml.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System.Threading.Tasks; -using Windows.UI.Xaml; -using Windows.UI.Xaml.Controls; -using Wino.Core.Domain.Interfaces; - -namespace Wino.Dialogs -{ - public sealed partial class ConfirmationDialog : ContentDialog, IConfirmationDialog - { - private TaskCompletionSource _completionSource; - - #region Dependency Properties - - public string DialogTitle - { - get { return (string)GetValue(DialogTitleProperty); } - set { SetValue(DialogTitleProperty, value); } - } - - public static readonly DependencyProperty DialogTitleProperty = DependencyProperty.Register(nameof(DialogTitle), typeof(string), typeof(ConfirmationDialog), new PropertyMetadata(string.Empty)); - - public string Message - { - get { return (string)GetValue(MessageProperty); } - set { SetValue(MessageProperty, value); } - } - - public static readonly DependencyProperty MessageProperty = DependencyProperty.Register(nameof(Message), typeof(string), typeof(ConfirmationDialog), new PropertyMetadata(string.Empty)); - - public string ApproveButtonTitle - { - get { return (string)GetValue(ApproveButtonTitleProperty); } - set { SetValue(ApproveButtonTitleProperty, value); } - } - - public static readonly DependencyProperty ApproveButtonTitleProperty = DependencyProperty.Register(nameof(ApproveButtonTitle), typeof(string), typeof(ConfirmationDialog), new PropertyMetadata(string.Empty)); - - #endregion - - private bool _isApproved; - public ConfirmationDialog() - { - InitializeComponent(); - } - - public async Task ShowDialogAsync(string title, string message, string approveButtonTitle) - { - _completionSource = new TaskCompletionSource(); - - DialogTitle = title; - Message = message; - ApproveButtonTitle = approveButtonTitle; - -#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - ShowAsync(); -#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - - return await _completionSource.Task; - } - - private void DialogClosed(ContentDialog sender, ContentDialogClosedEventArgs args) - { - _completionSource.TrySetResult(_isApproved); - } - - private void ApproveClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args) - { - _isApproved = true; - - Hide(); - } - - private void CancelClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args) - { - _isApproved = false; - - Hide(); - } - } -} diff --git a/Wino.Mail/Dialogs/CustomMessageDialogInformationContainer.cs b/Wino.Mail/Dialogs/CustomMessageDialogInformationContainer.cs new file mode 100644 index 00000000..43fc2b37 --- /dev/null +++ b/Wino.Mail/Dialogs/CustomMessageDialogInformationContainer.cs @@ -0,0 +1,24 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using Wino.Core.Domain.Enums; + +namespace Wino.Dialogs +{ + public partial class CustomMessageDialogInformationContainer : ObservableObject + { + [ObservableProperty] + private bool isDontAskChecked; + + public CustomMessageDialogInformationContainer(string title, string description, WinoCustomMessageDialogIcon icon, bool isDontAskAgainEnabled) + { + Title = title; + Description = description; + Icon = icon; + IsDontAskAgainEnabled = isDontAskAgainEnabled; + } + + public string Title { get; } + public string Description { get; } + public WinoCustomMessageDialogIcon Icon { get; } + public bool IsDontAskAgainEnabled { get; } + } +} diff --git a/Wino.Mail/Dialogs/StoreRatingDialog.xaml b/Wino.Mail/Dialogs/StoreRatingDialog.xaml deleted file mode 100644 index be839312..00000000 --- a/Wino.Mail/Dialogs/StoreRatingDialog.xaml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - diff --git a/Wino.Mail/Dialogs/StoreRatingDialog.xaml.cs b/Wino.Mail/Dialogs/StoreRatingDialog.xaml.cs deleted file mode 100644 index eb6a771b..00000000 --- a/Wino.Mail/Dialogs/StoreRatingDialog.xaml.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Windows.UI.Xaml.Controls; -using Wino.Core.Domain.Interfaces; - -namespace Wino.Dialogs -{ - public sealed partial class StoreRatingDialog : ContentDialog, IStoreRatingDialog - { - public bool DontAskAgain { get; set; } - public bool RateWinoClicked { get; set; } - - public StoreRatingDialog() - { - this.InitializeComponent(); - } - - private void RateClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args) - { - RateWinoClicked = true; - } - } -} diff --git a/Wino.Mail/Dialogs/WinoMessageDialog.xaml b/Wino.Mail/Dialogs/WinoMessageDialog.xaml deleted file mode 100644 index d61fc010..00000000 --- a/Wino.Mail/Dialogs/WinoMessageDialog.xaml +++ /dev/null @@ -1,25 +0,0 @@ - - - - 250 - 900 - 200 - 756 - - - - diff --git a/Wino.Mail/Dialogs/WinoMessageDialog.xaml.cs b/Wino.Mail/Dialogs/WinoMessageDialog.xaml.cs deleted file mode 100644 index 4e30b9b9..00000000 --- a/Wino.Mail/Dialogs/WinoMessageDialog.xaml.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Threading.Tasks; -using Windows.UI.Xaml; -using Windows.UI.Xaml.Controls; - -namespace Wino.Dialogs -{ - public sealed partial class WinoMessageDialog : ContentDialog - { - private TaskCompletionSource _completionSource; - - #region Dependency Properties - - public string DialogTitle - { - get { return (string)GetValue(DialogTitleProperty); } - set { SetValue(DialogTitleProperty, value); } - } - - public static readonly DependencyProperty DialogTitleProperty = DependencyProperty.Register(nameof(DialogTitle), typeof(string), typeof(ConfirmationDialog), new PropertyMetadata(string.Empty)); - - public string Message - { - get { return (string)GetValue(MessageProperty); } - set { SetValue(MessageProperty, value); } - } - - public static readonly DependencyProperty MessageProperty = DependencyProperty.Register(nameof(Message), typeof(string), typeof(ConfirmationDialog), new PropertyMetadata(string.Empty)); - - #endregion - - public WinoMessageDialog() - { - InitializeComponent(); - } - - public async Task ShowDialogAsync(string title, string message) - { - _completionSource = new TaskCompletionSource(); - - DialogTitle = title; - Message = message; - -#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - ShowAsync(); -#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - - return await _completionSource.Task; - } - - private void ApproveClicked(object sender, RoutedEventArgs e) - { - Hide(); - } - - private void DialogClosed(ContentDialog sender, ContentDialogClosedEventArgs args) - { - _completionSource.TrySetResult(true); - } - } -} diff --git a/Wino.Mail/Selectors/CustomWinoMessageDialogIconSelector.cs b/Wino.Mail/Selectors/CustomWinoMessageDialogIconSelector.cs new file mode 100644 index 00000000..2dad8413 --- /dev/null +++ b/Wino.Mail/Selectors/CustomWinoMessageDialogIconSelector.cs @@ -0,0 +1,38 @@ +using System; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Wino.Core.Domain.Enums; + +namespace Wino.Selectors +{ + public class CustomWinoMessageDialogIconSelector : DataTemplateSelector + { + public DataTemplate InfoIconTemplate { get; set; } + public DataTemplate WarningIconTemplate { get; set; } + public DataTemplate QuestionIconTemplate { get; set; } + public DataTemplate ErrorIconTemplate { get; set; } + + protected override DataTemplate SelectTemplateCore(object item, DependencyObject container) + { + if (item == null) return null; + + if (item is WinoCustomMessageDialogIcon icon) + { + switch (icon) + { + case WinoCustomMessageDialogIcon.Information: + return InfoIconTemplate; + case WinoCustomMessageDialogIcon.Warning: + return WarningIconTemplate; + case WinoCustomMessageDialogIcon.Error: + return ErrorIconTemplate; + case WinoCustomMessageDialogIcon.Question: + return QuestionIconTemplate; + default: + throw new Exception("Unknown custom message dialog icon."); + } + } + return base.SelectTemplateCore(item, container); + } + } +} diff --git a/Wino.Mail/Services/DialogService.cs b/Wino.Mail/Services/DialogService.cs index 3788611d..829ae4ec 100644 --- a/Wino.Mail/Services/DialogService.cs +++ b/Wino.Mail/Services/DialogService.cs @@ -8,6 +8,7 @@ using Microsoft.Toolkit.Uwp.Helpers; using Serilog; using Windows.Storage; using Windows.Storage.Pickers; +using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Wino.Core.Domain; using Wino.Core.Domain.Entities; @@ -29,26 +30,21 @@ namespace Wino.Services private SemaphoreSlim _presentationSemaphore = new SemaphoreSlim(1); private readonly IThemeService _themeService; + private readonly IConfigurationService _configurationService; - public DialogService(IThemeService themeService) + public DialogService(IThemeService themeService, IConfigurationService configurationService) { _themeService = themeService; + _configurationService = configurationService; } public void ShowNotSupportedMessage() - { - InfoBarMessage(Translator.Info_UnsupportedFunctionalityTitle, Translator.Info_UnsupportedFunctionalityDescription, InfoBarMessageType.Error); - } + => InfoBarMessage(Translator.Info_UnsupportedFunctionalityTitle, + Translator.Info_UnsupportedFunctionalityDescription, + InfoBarMessageType.Error); - public async Task ShowMessageAsync(string message, string title) - { - var dialog = new WinoMessageDialog() - { - RequestedTheme = _themeService.RootTheme.ToWindowsElementTheme() - }; - - await HandleDialogPresentation(() => dialog.ShowDialogAsync(title, message)); - } + public Task ShowMessageAsync(string message, string title, WinoCustomMessageDialogIcon icon = WinoCustomMessageDialogIcon.Information) + => ShowWinoCustomMessageDialogAsync(title, message, Translator.Buttons_Close, icon); /// /// Waits for PopupRoot to be available before presenting the dialog and returns the result after presentation. @@ -75,40 +71,8 @@ namespace Wino.Services return ContentDialogResult.None; } - /// - /// Waits for PopupRoot to be available before executing the given Task that returns customized result. - /// - /// Task that presents the dialog and returns result. - /// Dialog result from the custom dialog. - private async Task HandleDialogPresentation(Func> executionTask) - { - await _presentationSemaphore.WaitAsync(); - - try - { - return await executionTask(); - } - catch (Exception ex) - { - Log.Error(ex, "Handling dialog service failed."); - } - finally - { - _presentationSemaphore.Release(); - } - - return false; - } - - public async Task ShowConfirmationDialogAsync(string question, string title, string confirmationButtonTitle) - { - var dialog = new ConfirmationDialog() - { - RequestedTheme = _themeService.RootTheme.ToWindowsElementTheme() - }; - - return await HandleDialogPresentation(() => dialog.ShowDialogAsync(title, question, confirmationButtonTitle)); - } + public Task ShowConfirmationDialogAsync(string question, string title, string confirmationButtonTitle) + => ShowWinoCustomMessageDialogAsync(title, question, confirmationButtonTitle, WinoCustomMessageDialogIcon.Question, Translator.Buttons_Cancel, string.Empty); public async Task ShowNewAccountMailProviderDialogAsync(List availableProviders) { @@ -204,18 +168,6 @@ namespace Wino.Services return editAccountDialog.IsSaved ? editAccountDialog.Account : null; } - public async Task ShowRatingDialogAsync() - { - var storeDialog = new StoreRatingDialog() - { - RequestedTheme = _themeService.RootTheme.ToWindowsElementTheme() - }; - - await HandleDialogPresentationAsync(storeDialog); - - return storeDialog; - } - public async Task ShowCreateAccountAliasDialogAsync() { var createAccountAliasDialog = new CreateAccountAliasDialog() @@ -247,6 +199,8 @@ namespace Wino.Services { var updatedAccount = await folderService.UpdateSystemFolderConfigurationAsync(accountId, configuration); + InfoBarMessage(Translator.SystemFolderConfigSetupSuccess_Title, Translator.SystemFolderConfigSetupSuccess_Message, InfoBarMessageType.Success); + // Update account menu item and force re-synchronization. WeakReferenceMessenger.Default.Send(new AccountUpdatedMessage(updatedAccount)); @@ -258,11 +212,6 @@ namespace Wino.Services WeakReferenceMessenger.Default.Send(new NewSynchronizationRequested(options, SynchronizationSource.Client)); } - - if (configuration != null) - { - InfoBarMessage(Translator.SystemFolderConfigSetupSuccess_Title, Translator.SystemFolderConfigSetupSuccess_Message, InfoBarMessageType.Success); - } } catch (Exception ex) { @@ -331,7 +280,12 @@ namespace Wino.Services return await file.ReadBytesAsync(); } - public Task ShowHardDeleteConfirmationAsync() => ShowConfirmationDialogAsync(Translator.DialogMessage_HardDeleteConfirmationMessage, Translator.DialogMessage_HardDeleteConfirmationTitle, Translator.Buttons_Yes); + public Task ShowHardDeleteConfirmationAsync() + => ShowWinoCustomMessageDialogAsync(Translator.DialogMessage_HardDeleteConfirmationMessage, + Translator.DialogMessage_HardDeleteConfirmationTitle, + Translator.Buttons_Yes, + WinoCustomMessageDialogIcon.Warning, + Translator.Buttons_No); public async Task ShowAccountPickerDialogAsync(List availableAccounts) { @@ -377,5 +331,103 @@ namespace Wino.Services await HandleDialogPresentationAsync(accountReorderDialog); } + + public async Task ShowWinoCustomMessageDialogAsync(string title, + string description, + string approveButtonText, + WinoCustomMessageDialogIcon? icon, + string cancelButtonText = "", + string dontAskAgainConfigurationKey = "") + + { + // This config key has been marked as don't ask again already. + // Return immidiate result without presenting the dialog. + + bool isDontAskEnabled = !string.IsNullOrEmpty(dontAskAgainConfigurationKey); + + if (isDontAskEnabled && _configurationService.Get(dontAskAgainConfigurationKey, false)) return false; + + var informationContainer = new CustomMessageDialogInformationContainer(title, description, icon.Value, isDontAskEnabled); + + var dialog = new ContentDialog + { + Style = (Style)App.Current.Resources["WinoDialogStyle"], + RequestedTheme = _themeService.RootTheme.ToWindowsElementTheme(), + DefaultButton = ContentDialogButton.Primary, + PrimaryButtonText = approveButtonText, + ContentTemplate = (DataTemplate)App.Current.Resources["CustomWinoContentDialogContentTemplate"], + Content = informationContainer + }; + + if (!string.IsNullOrEmpty(cancelButtonText)) + { + dialog.SecondaryButtonText = cancelButtonText; + } + + var dialogResult = await HandleDialogPresentationAsync(dialog); + + // Mark this key to not ask again if user checked the checkbox. + if (informationContainer.IsDontAskChecked) + { + _configurationService.Set(dontAskAgainConfigurationKey, true); + } + + return dialogResult == ContentDialogResult.Primary; + } + + private object GetDontAskDialogContentWithIcon(string description, WinoCustomMessageDialogIcon icon, string dontAskKey = "") + { + var iconPresenter = new ContentPresenter() + { + ContentTemplate = (DataTemplate)App.Current.Resources[$"WinoCustomMessageDialog{icon}IconTemplate"], + HorizontalAlignment = HorizontalAlignment.Center + }; + + var viewBox = new Viewbox + { + Child = iconPresenter, + Margin = new Thickness(0, 6, 0, 0) + }; + + var descriptionTextBlock = new TextBlock() + { + Text = description, + TextWrapping = TextWrapping.WrapWholeWords, + VerticalAlignment = VerticalAlignment.Center + }; + + var containerGrid = new Grid() + { + Children = + { + viewBox, + descriptionTextBlock + }, + RowSpacing = 6, + ColumnSpacing = 12 + }; + + containerGrid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(32, GridUnitType.Pixel) }); + containerGrid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Star) }); + + Grid.SetColumn(descriptionTextBlock, 1); + + // Add don't ask again checkbox if key is provided. + if (!string.IsNullOrEmpty(dontAskKey)) + { + var dontAskCheckBox = new CheckBox() { Content = Translator.Dialog_DontAskAgain }; + + dontAskCheckBox.Checked += (c, r) => { _configurationService.Set(dontAskKey, dontAskCheckBox.IsChecked.GetValueOrDefault()); }; + + containerGrid.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Auto) }); + containerGrid.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Auto) }); + + Grid.SetRow(dontAskCheckBox, 1); + Grid.SetColumnSpan(dontAskCheckBox, 2); + containerGrid.Children.Add(dontAskCheckBox); + } + + return containerGrid; + } } } diff --git a/Wino.Mail/Styles/CustomMessageDialogStyles.xaml b/Wino.Mail/Styles/CustomMessageDialogStyles.xaml new file mode 100644 index 00000000..e63faf6f --- /dev/null +++ b/Wino.Mail/Styles/CustomMessageDialogStyles.xaml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wino.Mail/Styles/CustomMessageDialogStyles.xaml.cs b/Wino.Mail/Styles/CustomMessageDialogStyles.xaml.cs new file mode 100644 index 00000000..fdb8a6e7 --- /dev/null +++ b/Wino.Mail/Styles/CustomMessageDialogStyles.xaml.cs @@ -0,0 +1,12 @@ +using Windows.UI.Xaml; + +namespace Wino.Styles +{ + partial class CustomMessageDialogStyles : ResourceDictionary + { + public CustomMessageDialogStyles() + { + InitializeComponent(); + } + } +} diff --git a/Wino.Mail/Views/SettingsPage.xaml.cs b/Wino.Mail/Views/SettingsPage.xaml.cs index 4f638828..fda36fb8 100644 --- a/Wino.Mail/Views/SettingsPage.xaml.cs +++ b/Wino.Mail/Views/SettingsPage.xaml.cs @@ -31,6 +31,16 @@ namespace Wino.Views var initialRequest = new BreadcrumbNavigationRequested(Translator.MenuSettings, WinoPage.SettingOptionsPage); PageHistory.Add(new BreadcrumbNavigationItemViewModel(initialRequest, true)); + + if (e.Parameter is WinoPage parameterPage) + { + switch (parameterPage) + { + case WinoPage.AppPreferencesPage: + WeakReferenceMessenger.Default.Send(new BreadcrumbNavigationRequested(Translator.SettingsAppPreferences_Title, WinoPage.AppPreferencesPage)); + break; + } + } } public override void OnLanguageChanged() diff --git a/Wino.Mail/Wino.Mail.csproj b/Wino.Mail/Wino.Mail.csproj index d1a9d08c..b8b23cce 100644 --- a/Wino.Mail/Wino.Mail.csproj +++ b/Wino.Mail/Wino.Mail.csproj @@ -3,9 +3,9 @@ 8.0 - PackageReference - - false + PackageReference + + false Debug @@ -89,7 +89,7 @@ true false true - true + true true @@ -119,7 +119,6 @@ true true - 1.2.2 @@ -230,6 +229,7 @@ AccountReorderDialog.xaml + CustomThemeBuilderDialog.xaml @@ -245,15 +245,9 @@ CreateAccountAliasDialog.xaml - - StoreRatingDialog.xaml - SystemFolderConfigurationDialog.xaml - - WinoMessageDialog.xaml - TextInputDialog.xaml @@ -290,9 +284,6 @@ - - ConfirmationDialog.xaml - NewAccountDialog.xaml @@ -302,6 +293,7 @@ + @@ -314,6 +306,9 @@ CommandBarItems.xaml + + CustomMessageDialogStyles.xaml + @@ -494,26 +489,14 @@ MSBuild:Compile Designer - - Designer - MSBuild:Compile - Designer MSBuild:Compile - - MSBuild:Compile - Designer - MSBuild:Compile Designer - - Designer - MSBuild:Compile - Designer MSBuild:Compile @@ -530,6 +513,10 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + MSBuild:Compile Designer @@ -814,4 +801,4 @@ --> - + \ No newline at end of file diff --git a/Wino.Messages/Client/Navigation/NavigateAppPreferencesRequested.cs b/Wino.Messages/Client/Navigation/NavigateAppPreferencesRequested.cs new file mode 100644 index 00000000..b1567945 --- /dev/null +++ b/Wino.Messages/Client/Navigation/NavigateAppPreferencesRequested.cs @@ -0,0 +1,7 @@ +namespace Wino.Messaging.Client.Navigation +{ + /// + /// Navigates user to Settings -> App Preferences. + /// + public record NavigateAppPreferencesRequested; +} diff --git a/Wino.Messages/Server/TerminateServerRequested.cs b/Wino.Messages/Server/TerminateServerRequested.cs new file mode 100644 index 00000000..42a736bb --- /dev/null +++ b/Wino.Messages/Server/TerminateServerRequested.cs @@ -0,0 +1,9 @@ +using Wino.Core.Domain.Interfaces; + +namespace Wino.Messaging.Server +{ + /// + /// This message is sent to server to kill itself when UWP app is terminating. + /// + public record TerminateServerRequested : IClientMessage; +} diff --git a/Wino.Packaging/Package.appxmanifest b/Wino.Packaging/Package.appxmanifest index ecb3a70a..77cab9a5 100644 --- a/Wino.Packaging/Package.appxmanifest +++ b/Wino.Packaging/Package.appxmanifest @@ -110,5 +110,6 @@ + diff --git a/Wino.Server/Core/ServerMessageHandlerFactory.cs b/Wino.Server/Core/ServerMessageHandlerFactory.cs index f5faff3b..f738690c 100644 --- a/Wino.Server/Core/ServerMessageHandlerFactory.cs +++ b/Wino.Server/Core/ServerMessageHandlerFactory.cs @@ -21,6 +21,7 @@ namespace Wino.Server.Core nameof(ProtocolAuthorizationCallbackReceived) => App.Current.Services.GetService(), nameof(SynchronizationExistenceCheckRequest) => App.Current.Services.GetService(), nameof(ServerTerminationModeChanged) => App.Current.Services.GetService(), + nameof(TerminateServerRequested) => App.Current.Services.GetService(), _ => throw new Exception($"Server handler for {typeName} is not registered."), }; } @@ -36,6 +37,7 @@ namespace Wino.Server.Core serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); + serviceCollection.AddTransient(); } } } diff --git a/Wino.Server/MessageHandlers/TerminateServerRequestHandler.cs b/Wino.Server/MessageHandlers/TerminateServerRequestHandler.cs new file mode 100644 index 00000000..ed73687b --- /dev/null +++ b/Wino.Server/MessageHandlers/TerminateServerRequestHandler.cs @@ -0,0 +1,26 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Serilog; +using Wino.Core.Domain.Models.Server; +using Wino.Messaging.Server; +using Wino.Server.Core; + +namespace Wino.Server.MessageHandlers +{ + public class TerminateServerRequestHandler : ServerMessageHandler + { + public override WinoServerResponse FailureDefaultResponse(Exception ex) => WinoServerResponse.CreateErrorResponse(ex.Message); + + protected override Task> HandleAsync(TerminateServerRequested message, CancellationToken cancellationToken = default) + { + // This handler is only doing the logging right now. + // Client will always expect success response. + // Server will be terminated in the server context once the client gets the response. + + Log.Information("Terminate server is requested by client. Killing server."); + + return Task.FromResult(WinoServerResponse.CreateSuccessResponse(true)); + } + } +} diff --git a/Wino.Server/ServerContext.cs b/Wino.Server/ServerContext.cs index 2334804f..97bd7f6f 100644 --- a/Wino.Server/ServerContext.cs +++ b/Wino.Server/ServerContext.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using System.Text.Json; using System.Threading.Tasks; +using System.Windows; using CommunityToolkit.Mvvm.Messaging; using Serilog; using Windows.ApplicationModel; @@ -308,12 +309,28 @@ namespace Wino.Server case nameof(ServerTerminationModeChanged): await ExecuteServerMessageSafeAsync(args, JsonSerializer.Deserialize(messageJson, _jsonSerializerOptions)); break; + + case nameof(TerminateServerRequested): + await ExecuteServerMessageSafeAsync(args, JsonSerializer.Deserialize(messageJson, _jsonSerializerOptions)); + + KillServer(); + break; default: Debug.WriteLine($"Missing handler for {typeName} in the server. Check ServerContext.cs - HandleServerMessageAsync."); break; } } + private void KillServer() + { + DisposeConnection(); + + Application.Current.Dispatcher.Invoke(() => + { + Application.Current.Shutdown(); + }); + } + /// /// Executes ServerMessage coming from the UWP. /// These requests are awaited and expected to return a response. From bd9cbe30c53883a574f3d456a11131eef6a25479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Wed, 21 Aug 2024 23:14:59 +0200 Subject: [PATCH 03/10] Fixed crashing issue with disposing composing page. --- Wino.Mail.ViewModels/ComposePageViewModel.cs | 9 ++++----- Wino.Mail/Views/ComposePage.xaml.cs | 16 ++++++++++++---- .../Client/Mails/KillChromiumRequested.cs | 7 ------- 3 files changed, 16 insertions(+), 16 deletions(-) delete mode 100644 Wino.Messages/Client/Mails/KillChromiumRequested.cs diff --git a/Wino.Mail.ViewModels/ComposePageViewModel.cs b/Wino.Mail.ViewModels/ComposePageViewModel.cs index fb84b3ae..702ef338 100644 --- a/Wino.Mail.ViewModels/ComposePageViewModel.cs +++ b/Wino.Mail.ViewModels/ComposePageViewModel.cs @@ -214,7 +214,7 @@ namespace Wino.Mail.ViewModels IncludedAttachments.Add(viewModel); } - private async Task UpdateMimeChangesAsync() + public async Task UpdateMimeChangesAsync() { if (isUpdatingMimeBlocked || CurrentMimeMessage == null || ComposingAccount == null || CurrentMailDraftItem == null) return; @@ -338,13 +338,12 @@ namespace Wino.Mail.ViewModels } } - public override async void OnNavigatedFrom(NavigationMode mode, object parameters) + public override void OnNavigatedFrom(NavigationMode mode, object parameters) { base.OnNavigatedFrom(mode, parameters); - await UpdateMimeChangesAsync().ConfigureAwait(false); - - Messenger.Send(new KillChromiumRequested()); + /// Do not put any code here. + /// Make sure to use Page's OnNavigatedTo instead. } public override async void OnNavigatedTo(NavigationMode mode, object parameters) diff --git a/Wino.Mail/Views/ComposePage.xaml.cs b/Wino.Mail/Views/ComposePage.xaml.cs index 663cebf9..9e90ef15 100644 --- a/Wino.Mail/Views/ComposePage.xaml.cs +++ b/Wino.Mail/Views/ComposePage.xaml.cs @@ -39,8 +39,7 @@ namespace Wino.Views public sealed partial class ComposePage : ComposePageAbstract, IRecipient, IRecipient, - IRecipient, - IRecipient + IRecipient { public bool IsComposerDarkMode { @@ -415,7 +414,6 @@ namespace Wino.Views return await ExecuteScriptFunctionAsync("initializeJodit", fonts, composerFont, composerFontSize, readerFont, readerFontSize); } - private void DisposeWebView2() { if (Chromium == null) return; @@ -451,6 +449,7 @@ namespace Wino.Views Disposables.Add(GetSuggestionBoxDisposable(CCBox)); Disposables.Add(GetSuggestionBoxDisposable(BccBox)); + Chromium.Unloaded += Chromium_Unloaded; Chromium.CoreWebView2Initialized -= ChromiumInitialized; Chromium.CoreWebView2Initialized += ChromiumInitialized; @@ -468,6 +467,11 @@ namespace Wino.Views IsComposerDarkMode = underlyingThemeService.IsUnderlyingThemeDark(); } + private void Chromium_Unloaded(object sender, RoutedEventArgs e) + { + + } + private async void ChromiumInitialized(Microsoft.UI.Xaml.Controls.WebView2 sender, Microsoft.UI.Xaml.Controls.CoreWebView2InitializedEventArgs args) { var editorBundlePath = (await ViewModel.NativeAppService.GetEditorBundlePathAsync()).Replace("editor.html", string.Empty); @@ -692,8 +696,12 @@ namespace Wino.Views } } - public void Receive(KillChromiumRequested message) + protected override async void OnNavigatingFrom(NavigatingCancelEventArgs e) { + base.OnNavigatingFrom(e); + + await ViewModel.UpdateMimeChangesAsync(); + DisposeDisposables(); DisposeWebView2(); } diff --git a/Wino.Messages/Client/Mails/KillChromiumRequested.cs b/Wino.Messages/Client/Mails/KillChromiumRequested.cs deleted file mode 100644 index 659adee6..00000000 --- a/Wino.Messages/Client/Mails/KillChromiumRequested.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Wino.Messaging.Client.Mails -{ - /// - /// Terminates all chromum instances. - /// - public record KillChromiumRequested; -} From e6b9d591600c3baad843efeeca603645783a3884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Wed, 21 Aug 2024 23:16:46 +0200 Subject: [PATCH 04/10] Potential crash for imap. --- Wino.Core/Synchronizers/ImapSynchronizer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Wino.Core/Synchronizers/ImapSynchronizer.cs b/Wino.Core/Synchronizers/ImapSynchronizer.cs index e0ef6810..ccb8e224 100644 --- a/Wino.Core/Synchronizers/ImapSynchronizer.cs +++ b/Wino.Core/Synchronizers/ImapSynchronizer.cs @@ -785,6 +785,7 @@ namespace Wino.Core.Synchronizers EventHandler MessageFlagsChangedHandler = async (s, e) => { if (imapFolder == null) return; + if (e.UniqueId == null) return; var localMailCopyId = MailkitClientExtensions.CreateUid(folder.Id, e.UniqueId.Value.Id); From af13e034c3e0bfdb7150ba89fbfbe54539aa7edd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Wed, 21 Aug 2024 23:22:32 +0200 Subject: [PATCH 05/10] New resource strings for app closing handlers. --- .../Translations/en_US/resources.json | 7 ++++ Wino.Core.Domain/Translator.Designer.cs | 35 +++++++++++++++++++ Wino.Mail/App.xaml.cs | 8 ++--- 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json index a81f18aa..9b12a335 100644 --- a/Wino.Core.Domain/Translations/en_US/resources.json +++ b/Wino.Core.Domain/Translations/en_US/resources.json @@ -11,6 +11,13 @@ "AccountSettingsDialog_AccountNamePlaceholder": "eg. John Doe", "AddHyperlink": "Add", "AutoDiscoveryProgressMessage": "Searching for mail settings...", + "AppCloseBackgroundSynchronizationWarningTitle": "Background Synchronization", + "AppCloseTerminateBehaviorWarningMessageFirstLine": "You are terminating Wino Mail and your app close behavior is set to 'Terminate'.", + "AppCloseTerminateBehaviorWarningMessageSecondLine": "This will stop all background synchronizations and notifications.", + "AppCloseTerminateBehaviorWarningMessageThirdLine": "Do you want to go to App Preferences to set Wino Mail to run minimized or in the background?", + "AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Application has not been set to launch on Windows startup.", + "AppCloseStartupLaunchDisabledWarningMessageSecondLine": "This will cause you to miss notifications when you restart your computer.", + "AppCloseStartupLaunchDisabledWarningMessageThirdLine": "Do you want to go to App Preferences page to enable it?", "BasicIMAPSetupDialog_AdvancedConfiguration": "Advanced Configuration", "BasicIMAPSetupDialog_CredentialLocalMessage": "Your credentials will only be stored locally on your computer.", "BasicIMAPSetupDialog_Description": "Some accounts require additional steps to sign in", diff --git a/Wino.Core.Domain/Translator.Designer.cs b/Wino.Core.Domain/Translator.Designer.cs index 4c055048..2b64bff8 100644 --- a/Wino.Core.Domain/Translator.Designer.cs +++ b/Wino.Core.Domain/Translator.Designer.cs @@ -78,6 +78,41 @@ namespace Wino.Core.Domain /// public static string AutoDiscoveryProgressMessage => Resources.GetTranslatedString(@"AutoDiscoveryProgressMessage"); + /// + /// Background Synchronization + /// + public static string AppCloseBackgroundSynchronizationWarningTitle => Resources.GetTranslatedString(@"AppCloseBackgroundSynchronizationWarningTitle"); + + /// + /// You are terminating Wino Mail and your app close behavior is set to 'Terminate'. + /// + public static string AppCloseTerminateBehaviorWarningMessageFirstLine => Resources.GetTranslatedString(@"AppCloseTerminateBehaviorWarningMessageFirstLine"); + + /// + /// This will stop all background synchronizations and notifications. + /// + public static string AppCloseTerminateBehaviorWarningMessageSecondLine => Resources.GetTranslatedString(@"AppCloseTerminateBehaviorWarningMessageSecondLine"); + + /// + /// Do you want to go to App Preferences to set Wino Mail to run minimized or in the background? + /// + public static string AppCloseTerminateBehaviorWarningMessageThirdLine => Resources.GetTranslatedString(@"AppCloseTerminateBehaviorWarningMessageThirdLine"); + + /// + /// Application has not been set to launch on Windows startup. + /// + public static string AppCloseStartupLaunchDisabledWarningMessageFirstLine => Resources.GetTranslatedString(@"AppCloseStartupLaunchDisabledWarningMessageFirstLine"); + + /// + /// This will cause you to miss notifications when you restart your computer. + /// + public static string AppCloseStartupLaunchDisabledWarningMessageSecondLine => Resources.GetTranslatedString(@"AppCloseStartupLaunchDisabledWarningMessageSecondLine"); + + /// + /// Do you want to go to App Preferences page to enable it? + /// + public static string AppCloseStartupLaunchDisabledWarningMessageThirdLine => Resources.GetTranslatedString(@"AppCloseStartupLaunchDisabledWarningMessageThirdLine"); + /// /// Advanced Configuration /// diff --git a/Wino.Mail/App.xaml.cs b/Wino.Mail/App.xaml.cs index 8fc1a810..2a2a2626 100644 --- a/Wino.Mail/App.xaml.cs +++ b/Wino.Mail/App.xaml.cs @@ -139,8 +139,8 @@ namespace Wino // Starting the server is fine, but check if server termination behavior is set to terminate. // This state will kill the server once the app is terminated. - isGoToAppPreferencesRequested = await _dialogService.ShowWinoCustomMessageDialogAsync("Background Synchronization", - "You are terminating Wino Mail and your app close behavior is set to 'Terminate'.\nThis will stop all background synchronizations and notifications.\nDo you want to go to App Preferences to set Wino Mail to run minimized or in the background?", + isGoToAppPreferencesRequested = await _dialogService.ShowWinoCustomMessageDialogAsync(Translator.AppCloseBackgroundSynchronizationWarningTitle, + $"{Translator.AppCloseTerminateBehaviorWarningMessageFirstLine}\n{Translator.AppCloseTerminateBehaviorWarningMessageSecondLine}\n\n{Translator.AppCloseTerminateBehaviorWarningMessageThirdLine}", Translator.Buttons_Yes, WinoCustomMessageDialogIcon.Warning, Translator.Buttons_No, @@ -151,8 +151,8 @@ namespace Wino { // Startup behavior is not enabled. - isGoToAppPreferencesRequested = await dialogService.ShowWinoCustomMessageDialogAsync("Background Synchronization", - "Application has not been set to launch on Windows startup.\nThis may impact notifications and background synchronization.\nDo you want to go to App Preferences page to enable it?", + isGoToAppPreferencesRequested = await dialogService.ShowWinoCustomMessageDialogAsync(Translator.AppCloseBackgroundSynchronizationWarningTitle, + $"{Translator.AppCloseStartupLaunchDisabledWarningMessageFirstLine}\n{Translator.AppCloseStartupLaunchDisabledWarningMessageSecondLine}\n\n{Translator.AppCloseStartupLaunchDisabledWarningMessageThirdLine}", Translator.Buttons_Yes, WinoCustomMessageDialogIcon.Warning, Translator.Buttons_No, From c304517fc26f8e238a6f533f153df89d8f27f8a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Wed, 21 Aug 2024 23:30:57 +0200 Subject: [PATCH 06/10] Revert debug code. --- Wino.Core.UWP/Services/StoreRatingService.cs | 2 +- Wino.Core/Services/DatabaseService.cs | 2 +- Wino.Mail/Services/DialogService.cs | 55 -------------------- Wino.Mail/Views/ComposePage.xaml.cs | 6 --- 4 files changed, 2 insertions(+), 63 deletions(-) diff --git a/Wino.Core.UWP/Services/StoreRatingService.cs b/Wino.Core.UWP/Services/StoreRatingService.cs index d6852459..aed6a8bc 100644 --- a/Wino.Core.UWP/Services/StoreRatingService.cs +++ b/Wino.Core.UWP/Services/StoreRatingService.cs @@ -11,7 +11,7 @@ namespace Wino.Core.UWP.Services { public class StoreRatingService : IStoreRatingService { - private const string RatedStorageKey = "a"; // nameof(RatedStorageKey); + private const string RatedStorageKey = nameof(RatedStorageKey); private const string LatestAskedKey = nameof(LatestAskedKey); private readonly IConfigurationService _configurationService; diff --git a/Wino.Core/Services/DatabaseService.cs b/Wino.Core/Services/DatabaseService.cs index e21a88eb..f848cd5f 100644 --- a/Wino.Core/Services/DatabaseService.cs +++ b/Wino.Core/Services/DatabaseService.cs @@ -14,7 +14,7 @@ namespace Wino.Core.Services public class DatabaseService : IDatabaseService { - private const string DatabaseName = "Wino172.db"; + private const string DatabaseName = "Wino180.db"; private bool _isInitialized = false; private readonly IApplicationConfiguration _folderConfiguration; diff --git a/Wino.Mail/Services/DialogService.cs b/Wino.Mail/Services/DialogService.cs index 829ae4ec..ad842ff7 100644 --- a/Wino.Mail/Services/DialogService.cs +++ b/Wino.Mail/Services/DialogService.cs @@ -374,60 +374,5 @@ namespace Wino.Services return dialogResult == ContentDialogResult.Primary; } - - private object GetDontAskDialogContentWithIcon(string description, WinoCustomMessageDialogIcon icon, string dontAskKey = "") - { - var iconPresenter = new ContentPresenter() - { - ContentTemplate = (DataTemplate)App.Current.Resources[$"WinoCustomMessageDialog{icon}IconTemplate"], - HorizontalAlignment = HorizontalAlignment.Center - }; - - var viewBox = new Viewbox - { - Child = iconPresenter, - Margin = new Thickness(0, 6, 0, 0) - }; - - var descriptionTextBlock = new TextBlock() - { - Text = description, - TextWrapping = TextWrapping.WrapWholeWords, - VerticalAlignment = VerticalAlignment.Center - }; - - var containerGrid = new Grid() - { - Children = - { - viewBox, - descriptionTextBlock - }, - RowSpacing = 6, - ColumnSpacing = 12 - }; - - containerGrid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(32, GridUnitType.Pixel) }); - containerGrid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Star) }); - - Grid.SetColumn(descriptionTextBlock, 1); - - // Add don't ask again checkbox if key is provided. - if (!string.IsNullOrEmpty(dontAskKey)) - { - var dontAskCheckBox = new CheckBox() { Content = Translator.Dialog_DontAskAgain }; - - dontAskCheckBox.Checked += (c, r) => { _configurationService.Set(dontAskKey, dontAskCheckBox.IsChecked.GetValueOrDefault()); }; - - containerGrid.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Auto) }); - containerGrid.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Auto) }); - - Grid.SetRow(dontAskCheckBox, 1); - Grid.SetColumnSpan(dontAskCheckBox, 2); - containerGrid.Children.Add(dontAskCheckBox); - } - - return containerGrid; - } } } diff --git a/Wino.Mail/Views/ComposePage.xaml.cs b/Wino.Mail/Views/ComposePage.xaml.cs index 9e90ef15..935bc373 100644 --- a/Wino.Mail/Views/ComposePage.xaml.cs +++ b/Wino.Mail/Views/ComposePage.xaml.cs @@ -449,7 +449,6 @@ namespace Wino.Views Disposables.Add(GetSuggestionBoxDisposable(CCBox)); Disposables.Add(GetSuggestionBoxDisposable(BccBox)); - Chromium.Unloaded += Chromium_Unloaded; Chromium.CoreWebView2Initialized -= ChromiumInitialized; Chromium.CoreWebView2Initialized += ChromiumInitialized; @@ -467,11 +466,6 @@ namespace Wino.Views IsComposerDarkMode = underlyingThemeService.IsUnderlyingThemeDark(); } - private void Chromium_Unloaded(object sender, RoutedEventArgs e) - { - - } - private async void ChromiumInitialized(Microsoft.UI.Xaml.Controls.WebView2 sender, Microsoft.UI.Xaml.Controls.CoreWebView2InitializedEventArgs args) { var editorBundlePath = (await ViewModel.NativeAppService.GetEditorBundlePathAsync()).Replace("editor.html", string.Empty); From 93087d7aa7209f89c55916792edffff4751f73e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Thu, 22 Aug 2024 00:51:10 +0200 Subject: [PATCH 07/10] Ask for enable startup on first launch. --- .../Translations/en_US/resources.json | 3 ++ Wino.Core.Domain/Translator.Designer.cs | 15 ++++++ Wino.Mail.ViewModels/AppShellViewModel.cs | 46 ++++++++++++++++--- 3 files changed, 57 insertions(+), 7 deletions(-) diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json index 9b12a335..a439fc5a 100644 --- a/Wino.Core.Domain/Translations/en_US/resources.json +++ b/Wino.Core.Domain/Translations/en_US/resources.json @@ -112,6 +112,9 @@ "DialogMessage_UnsubscribeConfirmationGoToWebsiteMessage": "To stop getting messages from {0}, go to their website to unsubscribe.", "DialogMessage_UnsubscribeConfirmationGoToWebsiteConfirmButton": "Go to website", "DialogMessage_UnsubscribeConfirmationMailtoMessage": "Do you want to stop getting messages from {0}? Wino will unsubscribe for you by sending an email from your email account to {1}.", + "DialogMessage_EnableStartupLaunchTitle": "Enable Startup Launch", + "DialogMessage_EnableStartupLaunchMessage": "Let Wino Mail automatically launch minimized on Windows startup to not miss any notifications.\n\nDo you want to enable startup launch?", + "DialogMessage_EnableStartupLaunchDeniedMessage": "You can enable startup launch from Settings -> App Preferences.", "Dialog_DontAskAgain": "Don't ask again", "CreateAccountAliasDialog_Title": "Create Account Alias", "CreateAccountAliasDialog_Description": "Make sure your outgoing server allows sending mails from this alias.", diff --git a/Wino.Core.Domain/Translator.Designer.cs b/Wino.Core.Domain/Translator.Designer.cs index 2b64bff8..72056b31 100644 --- a/Wino.Core.Domain/Translator.Designer.cs +++ b/Wino.Core.Domain/Translator.Designer.cs @@ -583,6 +583,21 @@ namespace Wino.Core.Domain /// public static string DialogMessage_UnsubscribeConfirmationMailtoMessage => Resources.GetTranslatedString(@"DialogMessage_UnsubscribeConfirmationMailtoMessage"); + /// + /// Enable Startup Launch + /// + public static string DialogMessage_EnableStartupLaunchTitle => Resources.GetTranslatedString(@"DialogMessage_EnableStartupLaunchTitle"); + + /// + /// Let Wino Mail automatically launch minimized on Windows startup to not miss any notifications. Do you want to enable startup launch? + /// + public static string DialogMessage_EnableStartupLaunchMessage => Resources.GetTranslatedString(@"DialogMessage_EnableStartupLaunchMessage"); + + /// + /// You can enable startup launch from Settings -> App Preferences. + /// + public static string DialogMessage_EnableStartupLaunchDeniedMessage => Resources.GetTranslatedString(@"DialogMessage_EnableStartupLaunchDeniedMessage"); + /// /// Don't ask again /// diff --git a/Wino.Mail.ViewModels/AppShellViewModel.cs b/Wino.Mail.ViewModels/AppShellViewModel.cs index b87bd135..342786e1 100644 --- a/Wino.Mail.ViewModels/AppShellViewModel.cs +++ b/Wino.Mail.ViewModels/AppShellViewModel.cs @@ -60,6 +60,8 @@ namespace Wino.Mail.ViewModels #endregion + private const string IsActivateStartupLaunchAskedKey = nameof(IsActivateStartupLaunchAskedKey); + public IStatePersistanceService StatePersistenceService { get; } public IWinoServerConnectionManager ServerConnectionManager { get; } public IPreferencesService PreferencesService { get; } @@ -67,6 +69,7 @@ namespace Wino.Mail.ViewModels private readonly IFolderService _folderService; private readonly IConfigurationService _configurationService; + private readonly IStartupBehaviorService _startupBehaviorService; private readonly IAccountService _accountService; private readonly IContextMenuItemService _contextMenuItemService; private readonly IStoreRatingService _storeRatingService; @@ -101,7 +104,8 @@ namespace Wino.Mail.ViewModels IFolderService folderService, IStatePersistanceService statePersistanceService, IWinoServerConnectionManager serverConnectionManager, - IConfigurationService configurationService) : base(dialogService) + IConfigurationService configurationService, + IStartupBehaviorService startupBehaviorService) : base(dialogService) { StatePersistenceService = statePersistanceService; ServerConnectionManager = serverConnectionManager; @@ -119,6 +123,7 @@ namespace Wino.Mail.ViewModels NavigationService = navigationService; _configurationService = configurationService; + _startupBehaviorService = startupBehaviorService; _backgroundTaskService = backgroundTaskService; _mimeFileService = mimeFileService; _nativeAppService = nativeAppService; @@ -239,9 +244,41 @@ namespace Wino.Mail.ViewModels await ProcessLaunchOptionsAsync(); await ForceAllAccountSynchronizationsAsync(); + await MakeSureEnableStartupLaunchAsync(); ConfigureBackgroundTasks(); } + private async Task MakeSureEnableStartupLaunchAsync() + { + if (!_configurationService.Get(IsActivateStartupLaunchAskedKey, false)) + { + bool isAccepted = await DialogService.ShowWinoCustomMessageDialogAsync(Translator.DialogMessage_EnableStartupLaunchTitle, + Translator.DialogMessage_EnableStartupLaunchMessage, + Translator.Buttons_Yes, + WinoCustomMessageDialogIcon.Information, + Translator.Buttons_No); + + bool shouldDisplayLaterOnMessage = !isAccepted; + + if (isAccepted) + { + var behavior = await _startupBehaviorService.ToggleStartupBehavior(true); + + shouldDisplayLaterOnMessage = behavior != StartupBehaviorResult.Enabled; + } + + if (shouldDisplayLaterOnMessage) + { + await DialogService.ShowWinoCustomMessageDialogAsync(Translator.DialogMessage_EnableStartupLaunchTitle, + Translator.DialogMessage_EnableStartupLaunchDeniedMessage, + Translator.Buttons_Close, + WinoCustomMessageDialogIcon.Information); + } + + _configurationService.Set(IsActivateStartupLaunchAskedKey, true); + } + } + private void ConfigureBackgroundTasks() { try @@ -809,16 +846,11 @@ namespace Wino.Mail.ViewModels protected override async void OnAccountUpdated(MailAccount updatedAccount) { - await ExecuteUIThread(async () => + await ExecuteUIThread(() => { if (MenuItems.TryGetAccountMenuItem(updatedAccount.Id, out IAccountMenuItem foundAccountMenuItem)) { foundAccountMenuItem.UpdateAccount(updatedAccount); - - if (latestSelectedAccountMenuItem == foundAccountMenuItem) - { - await ChangeLoadedAccountAsync(foundAccountMenuItem, false); - } } }); } From 53dbeadabb190ac11f520534042b15e5cef009ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Thu, 22 Aug 2024 00:52:41 +0200 Subject: [PATCH 08/10] Prevent asking users for startup launch if they already did that. --- Wino.Mail.ViewModels/AppShellViewModel.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Wino.Mail.ViewModels/AppShellViewModel.cs b/Wino.Mail.ViewModels/AppShellViewModel.cs index 342786e1..46a39f8d 100644 --- a/Wino.Mail.ViewModels/AppShellViewModel.cs +++ b/Wino.Mail.ViewModels/AppShellViewModel.cs @@ -252,6 +252,15 @@ namespace Wino.Mail.ViewModels { if (!_configurationService.Get(IsActivateStartupLaunchAskedKey, false)) { + var currentBehavior = await _startupBehaviorService.GetCurrentStartupBehaviorAsync(); + + // User somehow already enabled Wino before the first launch. + if (currentBehavior == StartupBehaviorResult.Enabled) + { + _configurationService.Set(IsActivateStartupLaunchAskedKey, true); + return; + } + bool isAccepted = await DialogService.ShowWinoCustomMessageDialogAsync(Translator.DialogMessage_EnableStartupLaunchTitle, Translator.DialogMessage_EnableStartupLaunchMessage, Translator.Buttons_Yes, From 298344c2ab16ea47dbebf7b5b06e6b67a969b779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Thu, 22 Aug 2024 00:57:46 +0200 Subject: [PATCH 09/10] Remove async from adding attachments. --- Wino.Mail.ViewModels/ComposePageViewModel.cs | 10 ---------- Wino.Mail/Views/ComposePage.xaml.cs | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/Wino.Mail.ViewModels/ComposePageViewModel.cs b/Wino.Mail.ViewModels/ComposePageViewModel.cs index 702ef338..13643abf 100644 --- a/Wino.Mail.ViewModels/ComposePageViewModel.cs +++ b/Wino.Mail.ViewModels/ComposePageViewModel.cs @@ -204,16 +204,6 @@ namespace Wino.Mail.ViewModels await _worker.ExecuteAsync(draftSendPreparationRequest); } - public async Task IncludeAttachmentAsync(MailAttachmentViewModel viewModel) - { - //if (bodyBuilder == null) return; - - //bodyBuilder.Attachments.Add(viewModel.FileName, new MemoryStream(viewModel.Content)); - - //LoadAttachments(); - IncludedAttachments.Add(viewModel); - } - public async Task UpdateMimeChangesAsync() { if (isUpdatingMimeBlocked || CurrentMimeMessage == null || ComposingAccount == null || CurrentMailDraftItem == null) return; diff --git a/Wino.Mail/Views/ComposePage.xaml.cs b/Wino.Mail/Views/ComposePage.xaml.cs index 935bc373..166b1722 100644 --- a/Wino.Mail/Views/ComposePage.xaml.cs +++ b/Wino.Mail/Views/ComposePage.xaml.cs @@ -239,7 +239,7 @@ namespace Wino.Views { var attachmentViewModel = await file.ToAttachmentViewModelAsync(); - await ViewModel.IncludeAttachmentAsync(attachmentViewModel); + ViewModel.IncludedAttachments.Add(attachmentViewModel); } } From d060db3c9680f75fca6f93289ffe70ca81dbf8de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Thu, 22 Aug 2024 01:20:08 +0200 Subject: [PATCH 10/10] Fixing an issue where doing folder config on account does not refresh the folder list. --- Wino.Core/MenuItems/MenuItemCollection.cs | 15 ++++++------- Wino.Core/Services/FolderService.cs | 15 +------------ Wino.Mail.ViewModels/AppShellViewModel.cs | 22 ++++++++++++++----- Wino.Mail/Services/DialogService.cs | 9 +++----- .../AccountFolderConfigurationUpdated.cs | 9 ++++++++ 5 files changed, 37 insertions(+), 33 deletions(-) create mode 100644 Wino.Messages/Client/Accounts/AccountFolderConfigurationUpdated.cs diff --git a/Wino.Core/MenuItems/MenuItemCollection.cs b/Wino.Core/MenuItems/MenuItemCollection.cs index f1357d35..fe04185f 100644 --- a/Wino.Core/MenuItems/MenuItemCollection.cs +++ b/Wino.Core/MenuItems/MenuItemCollection.cs @@ -151,15 +151,12 @@ namespace Wino.Core.MenuItems return accountMenuItem; } - public async Task ReplaceFoldersAsync(IEnumerable folders) + public void ReplaceFolders(IEnumerable folders) { - await _dispatcher.ExecuteOnUIThread(() => - { - ClearFolderAreaMenuItems(); + ClearFolderAreaMenuItems(); - Items.Add(new SeperatorItem()); - AddRange(folders); - }); + Items.Add(new SeperatorItem()); + AddRange(folders); } /// @@ -194,9 +191,11 @@ namespace Wino.Core.MenuItems { item.IsExpanded = false; item.IsSelected = false; + + Remove(item); }); - RemoveRange(itemsToRemove); + // RemoveRange(itemsToRemove); } } } diff --git a/Wino.Core/Services/FolderService.cs b/Wino.Core/Services/FolderService.cs index 2573f0a2..c9cd1332 100644 --- a/Wino.Core/Services/FolderService.cs +++ b/Wino.Core/Services/FolderService.cs @@ -387,11 +387,6 @@ namespace Wino.Core.Services if (configuration == null) throw new ArgumentNullException(nameof(configuration)); - var account = await _accountService.GetAccountAsync(accountId); - - if (account == null) - throw new ArgumentNullException(nameof(account)); - // Update system folders for this account. await Task.WhenAll(UpdateSystemFolderInternalAsync(configuration.SentFolder, SpecialFolderType.Sent), @@ -400,9 +395,8 @@ namespace Wino.Core.Services UpdateSystemFolderInternalAsync(configuration.TrashFolder, SpecialFolderType.Deleted), UpdateSystemFolderInternalAsync(configuration.ArchiveFolder, SpecialFolderType.Archive)); - await _accountService.UpdateAccountAsync(account); - return account; + return await _accountService.GetAccountAsync(accountId).ConfigureAwait(false); } private Task UpdateSystemFolderInternalAsync(MailItemFolder folder, SpecialFolderType assignedSpecialFolderType) @@ -492,13 +486,6 @@ namespace Wino.Core.Services return; } - var account = await _accountService.GetAccountAsync(folder.MailAccountId).ConfigureAwait(false); - if (account == null) - { - _logger.Warning("Account with id {MailAccountId} does not exist. Cannot update folder.", folder.MailAccountId); - return; - } - _logger.Debug("Updating folder {FolderName}", folder.Id, folder.FolderName); await Connection.UpdateAsync(folder).ConfigureAwait(false); diff --git a/Wino.Mail.ViewModels/AppShellViewModel.cs b/Wino.Mail.ViewModels/AppShellViewModel.cs index 46a39f8d..eab4fa06 100644 --- a/Wino.Mail.ViewModels/AppShellViewModel.cs +++ b/Wino.Mail.ViewModels/AppShellViewModel.cs @@ -38,7 +38,8 @@ namespace Wino.Mail.ViewModels IRecipient, IRecipient, IRecipient, - IRecipient + IRecipient, + IRecipient { #region Menu Items @@ -631,6 +632,9 @@ namespace Wino.Mail.ViewModels await MenuItems.SetAccountMenuItemEnabledStatusAsync(false); + // Load account folder structure and replace the visible folders. + var folders = await _folderService.GetAccountFoldersForDisplayAsync(clickedBaseAccountMenuItem); + await ExecuteUIThread(() => { clickedBaseAccountMenuItem.IsEnabled = false; @@ -643,12 +647,10 @@ namespace Wino.Mail.ViewModels clickedBaseAccountMenuItem.IsSelected = true; latestSelectedAccountMenuItem = clickedBaseAccountMenuItem; + + MenuItems.ReplaceFolders(folders); }); - // Load account folder structure and replace the visible folders. - var folders = await _folderService.GetAccountFoldersForDisplayAsync(clickedBaseAccountMenuItem); - - await MenuItems.ReplaceFoldersAsync(folders); await UpdateUnreadItemCountAsync(); await MenuItems.SetAccountMenuItemEnabledStatusAsync(true); @@ -955,6 +957,16 @@ namespace Wino.Mail.ViewModels } } + public async void Receive(AccountFolderConfigurationUpdated message) + { + // Reloading of folders is needed to re-create folder tree if the account is loaded. + + if (MenuItems.TryGetAccountMenuItem(message.AccountId, out IAccountMenuItem accountMenuItem)) + { + await ChangeLoadedAccountAsync(accountMenuItem, true); + } + } + public async void Receive(MergedInboxRenamed message) { var mergedInboxMenuItem = MenuItems.FirstOrDefault(a => a.EntityId == message.MergedInboxId); diff --git a/Wino.Mail/Services/DialogService.cs b/Wino.Mail/Services/DialogService.cs index ad842ff7..a858be9b 100644 --- a/Wino.Mail/Services/DialogService.cs +++ b/Wino.Mail/Services/DialogService.cs @@ -19,9 +19,9 @@ using Wino.Core.Domain.Models.Folders; using Wino.Core.Domain.Models.Synchronization; using Wino.Core.UWP.Extensions; using Wino.Dialogs; +using Wino.Messaging.Client.Accounts; using Wino.Messaging.Client.Shell; using Wino.Messaging.Server; -using Wino.Messaging.UI; namespace Wino.Services { @@ -197,16 +197,13 @@ namespace Wino.Services if (configuration != null) { - var updatedAccount = await folderService.UpdateSystemFolderConfigurationAsync(accountId, configuration); - InfoBarMessage(Translator.SystemFolderConfigSetupSuccess_Title, Translator.SystemFolderConfigSetupSuccess_Message, InfoBarMessageType.Success); - // Update account menu item and force re-synchronization. - WeakReferenceMessenger.Default.Send(new AccountUpdatedMessage(updatedAccount)); + WeakReferenceMessenger.Default.Send(new AccountFolderConfigurationUpdated(accountId)); var options = new SynchronizationOptions() { - AccountId = updatedAccount.Id, + AccountId = accountId, Type = SynchronizationType.Full, }; diff --git a/Wino.Messages/Client/Accounts/AccountFolderConfigurationUpdated.cs b/Wino.Messages/Client/Accounts/AccountFolderConfigurationUpdated.cs new file mode 100644 index 00000000..f0f7bf25 --- /dev/null +++ b/Wino.Messages/Client/Accounts/AccountFolderConfigurationUpdated.cs @@ -0,0 +1,9 @@ +using System; + +namespace Wino.Messaging.Client.Accounts +{ + /// + /// When account's special folder configuration is updated. + /// + public record AccountFolderConfigurationUpdated(Guid AccountId); +}