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.