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.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json index a81f18aa..a439fc5a 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", @@ -105,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 4c055048..72056b31 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 /// @@ -548,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.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."); } } diff --git a/Wino.Core.UWP/Services/StoreRatingService.cs b/Wino.Core.UWP/Services/StoreRatingService.cs index df33dcab..aed6a8bc 100644 --- a/Wino.Core.UWP/Services/StoreRatingService.cs +++ b/Wino.Core.UWP/Services/StoreRatingService.cs @@ -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.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/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.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.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); 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..eab4fa06 100644 --- a/Wino.Mail.ViewModels/AppShellViewModel.cs +++ b/Wino.Mail.ViewModels/AppShellViewModel.cs @@ -37,7 +37,9 @@ namespace Wino.Mail.ViewModels IRecipient, IRecipient, IRecipient, - IRecipient + IRecipient, + IRecipient, + IRecipient { #region Menu Items @@ -59,12 +61,16 @@ namespace Wino.Mail.ViewModels #endregion + private const string IsActivateStartupLaunchAskedKey = nameof(IsActivateStartupLaunchAskedKey); + public IStatePersistanceService StatePersistenceService { get; } public IWinoServerConnectionManager ServerConnectionManager { get; } public IPreferencesService PreferencesService { get; } public IWinoNavigationService NavigationService { get; } private readonly IFolderService _folderService; + private readonly IConfigurationService _configurationService; + private readonly IStartupBehaviorService _startupBehaviorService; private readonly IAccountService _accountService; private readonly IContextMenuItemService _contextMenuItemService; private readonly IStoreRatingService _storeRatingService; @@ -98,7 +104,9 @@ namespace Wino.Mail.ViewModels IWinoRequestDelegator winoRequestDelegator, IFolderService folderService, IStatePersistanceService statePersistanceService, - IWinoServerConnectionManager serverConnectionManager) : base(dialogService) + IWinoServerConnectionManager serverConnectionManager, + IConfigurationService configurationService, + IStartupBehaviorService startupBehaviorService) : base(dialogService) { StatePersistenceService = statePersistanceService; ServerConnectionManager = serverConnectionManager; @@ -115,6 +123,8 @@ namespace Wino.Mail.ViewModels PreferencesService = preferencesService; NavigationService = navigationService; + _configurationService = configurationService; + _startupBehaviorService = startupBehaviorService; _backgroundTaskService = backgroundTaskService; _mimeFileService = mimeFileService; _nativeAppService = nativeAppService; @@ -229,16 +239,56 @@ namespace Wino.Mail.ViewModels public override async void OnNavigatedTo(NavigationMode mode, object parameters) { base.OnNavigatedTo(mode, parameters); - await CreateFooterItemsAsync(); await RecreateMenuItemsAsync(); await ProcessLaunchOptionsAsync(); await ForceAllAccountSynchronizationsAsync(); + await MakeSureEnableStartupLaunchAsync(); ConfigureBackgroundTasks(); } + private async Task MakeSureEnableStartupLaunchAsync() + { + 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, + 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 @@ -527,7 +577,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 +611,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) { @@ -582,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; @@ -594,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); @@ -721,7 +772,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; } @@ -850,7 +913,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) { @@ -892,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); @@ -961,5 +1036,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..13643abf 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; } @@ -202,17 +204,7 @@ 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); - } - - private async Task UpdateMimeChangesAsync() + public async Task UpdateMimeChangesAsync() { if (isUpdatingMimeBlocked || CurrentMimeMessage == null || ComposingAccount == null || CurrentMailDraftItem == null) return; @@ -336,13 +328,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.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..2a2a2626 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(Translator.AppCloseBackgroundSynchronizationWarningTitle, + $"{Translator.AppCloseTerminateBehaviorWarningMessageFirstLine}\n{Translator.AppCloseTerminateBehaviorWarningMessageSecondLine}\n\n{Translator.AppCloseTerminateBehaviorWarningMessageThirdLine}", + Translator.Buttons_Yes, + WinoCustomMessageDialogIcon.Warning, + Translator.Buttons_No, + "DontAskTerminateServerBehavior"); + } + + if (isGoToAppPreferencesRequested == null && currentStartupBehavior != StartupBehaviorResult.Enabled) + { + // Startup behavior is not enabled. + + isGoToAppPreferencesRequested = await dialogService.ShowWinoCustomMessageDialogAsync(Translator.AppCloseBackgroundSynchronizationWarningTitle, + $"{Translator.AppCloseStartupLaunchDisabledWarningMessageFirstLine}\n{Translator.AppCloseStartupLaunchDisabledWarningMessageSecondLine}\n\n{Translator.AppCloseStartupLaunchDisabledWarningMessageThirdLine}", + 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..a858be9b 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; @@ -18,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 { @@ -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() @@ -245,24 +197,18 @@ 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, }; 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 +277,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 +328,48 @@ 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; + } } } 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/ComposePage.xaml.cs b/Wino.Mail/Views/ComposePage.xaml.cs index 663cebf9..166b1722 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 { @@ -240,7 +239,7 @@ namespace Wino.Views { var attachmentViewModel = await file.ToAttachmentViewModelAsync(); - await ViewModel.IncludeAttachmentAsync(attachmentViewModel); + ViewModel.IncludedAttachments.Add(attachmentViewModel); } } @@ -415,7 +414,6 @@ namespace Wino.Views return await ExecuteScriptFunctionAsync("initializeJodit", fonts, composerFont, composerFontSize, readerFont, readerFontSize); } - private void DisposeWebView2() { if (Chromium == null) return; @@ -692,8 +690,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.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/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); +} 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; -} 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.