diff --git a/Wino.Core.Domain/Entities/MailAccount.cs b/Wino.Core.Domain/Entities/MailAccount.cs index c8f39c1d..7d87128b 100644 --- a/Wino.Core.Domain/Entities/MailAccount.cs +++ b/Wino.Core.Domain/Entities/MailAccount.cs @@ -50,6 +50,11 @@ namespace Wino.Core.Domain.Entities /// public Guid? SignatureId { get; set; } + /// + /// Gets or sets the listing order of the account in the accounts list. + /// + public int Order { get; set; } + /// /// Gets or sets whether the account has any reason for an interactive user action to fix continue operating. /// diff --git a/Wino.Core.Domain/Interfaces/IAccountProviderDetailViewModel.cs b/Wino.Core.Domain/Interfaces/IAccountProviderDetailViewModel.cs index b535fea1..2778acd2 100644 --- a/Wino.Core.Domain/Interfaces/IAccountProviderDetailViewModel.cs +++ b/Wino.Core.Domain/Interfaces/IAccountProviderDetailViewModel.cs @@ -13,5 +13,21 @@ namespace Wino.Core.Domain.Interfaces /// Name representation of the view model that will be used to identify the startup entity on launch. /// string StartupEntityTitle { get; } + + /// + /// E-mail addresses that this account holds. + /// + + string StartupEntityAddresses { get; } + + /// + /// Represents the account order in the accounts list. + /// + int Order { get; } + + /// + /// Provider details of the account. + /// + IProviderDetail ProviderDetail { get; set; } } } diff --git a/Wino.Core.Domain/Interfaces/IAccountService.cs b/Wino.Core.Domain/Interfaces/IAccountService.cs index 99190af3..78682cda 100644 --- a/Wino.Core.Domain/Interfaces/IAccountService.cs +++ b/Wino.Core.Domain/Interfaces/IAccountService.cs @@ -68,12 +68,37 @@ namespace Wino.Core.Domain.Interfaces /// Current account synchronization modifier. Task UpdateSynchronizationIdentifierAsync(Guid accountId, string newIdentifier); + /// + /// Renames the merged inbox with the given id. + /// + /// Merged Inbox id + /// New name for the merged/linked inbox. Task RenameMergedAccountAsync(Guid mergedInboxId, string newName); + /// + /// Creates a new merged inbox with the given accounts. + /// + /// Merged inbox properties. + /// List of accounts to merge together. Task CreateMergeAccountsAsync(MergedInbox mergedInbox, IEnumerable accountsToMerge); + /// + /// Updates the merged inbox with the given id with the new linked accounts. + /// + /// Updating merged inbox id. + /// List of linked account ids. Task UpdateMergedInboxAsync(Guid mergedInboxId, IEnumerable linkedAccountIds); + /// + /// Destroys the merged inbox with the given id. + /// + /// Merged inbox id to destroy. Task UnlinkMergedInboxAsync(Guid mergedInboxId); + + /// + /// Updates the account listing orders. + /// + /// AccountId-OrderNumber pair for all accounts. + Task UpdateAccountOrdersAsync(Dictionary accountIdOrderPair); } } diff --git a/Wino.Core.Domain/Interfaces/IDialogService.cs b/Wino.Core.Domain/Interfaces/IDialogService.cs index af54f79f..6493fcb7 100644 --- a/Wino.Core.Domain/Interfaces/IDialogService.cs +++ b/Wino.Core.Domain/Interfaces/IDialogService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Threading.Tasks; using Wino.Core.Domain.Entities; using Wino.Core.Domain.Enums; @@ -32,6 +33,13 @@ namespace Wino.Core.Domain.Interfaces Task ShowEditAccountDialogAsync(MailAccount account); Task ShowAccountPickerDialogAsync(List availableAccounts); + /// + /// Displays a dialog to the user for reordering accounts. + /// + /// Available accounts in order. + /// Result model that has dict of AccountId-AccountOrder. + Task ShowAccountReorderDialogAsync(ObservableCollection availableAccounts); + /// /// Presents a dialog to the user for selecting folder. /// diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json index ee59438a..88b8b57f 100644 --- a/Wino.Core.Domain/Translations/en_US/resources.json +++ b/Wino.Core.Domain/Translations/en_US/resources.json @@ -407,6 +407,8 @@ "SettingsFolderMenuStyle_Description": "Change whether account folders should be nested inside an account menu item or not. Toggle this off if you like the old menu system in Windows Mail", "SettingsManageAccountSettings_Description": "Notifications, signatures, synchronization and other settings per account.", "SettingsManageAccountSettings_Title": "Manage Account Settings", + "SettingsReorderAccounts_Title": "Reorder Accounts", + "SettingsReorderAccounts_Description": "Change the order of accounts in the account list.", "SettingsManageLink_Description": "Move items to add new link or remove existing link.", "SettingsManageLink_Title": "Manage Link", "SettingsMarkAsRead_Description": "Change what should happen to the selected item.", diff --git a/Wino.Core.Domain/Translator.Designer.cs b/Wino.Core.Domain/Translator.Designer.cs index ee2312c1..9c83ca16 100644 --- a/Wino.Core.Domain/Translator.Designer.cs +++ b/Wino.Core.Domain/Translator.Designer.cs @@ -2481,6 +2481,18 @@ namespace Wino.Core.Domain public static string SettingsManageAccountSettings_Title => Resources.GetTranslatedString(@"SettingsManageAccountSettings_Title"); + /// + /// Reorder Accounts + /// + public static string SettingsReorderAccounts_Title => Resources.GetTranslatedString(@"SettingsReorderAccounts_Title"); + + + /// + /// Change the order of accounts in the account list. + /// + public static string SettingsReorderAccounts_Description => Resources.GetTranslatedString(@"SettingsReorderAccounts_Description"); + + /// /// Move items to add new link or remove existing link. /// diff --git a/Wino.Core/Messages/Accounts/AccountMenuItemsReordered.cs b/Wino.Core/Messages/Accounts/AccountMenuItemsReordered.cs new file mode 100644 index 00000000..d9a9d74b --- /dev/null +++ b/Wino.Core/Messages/Accounts/AccountMenuItemsReordered.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace Wino.Core.Messages.Accounts +{ + /// + /// Emitted when account menu items are reordered. + /// + /// New order info. + public record AccountMenuItemsReordered(Dictionary newOrderDictionary); +} diff --git a/Wino.Core/Services/AccountService.cs b/Wino.Core/Services/AccountService.cs index 850139f2..80660e87 100644 --- a/Wino.Core/Services/AccountService.cs +++ b/Wino.Core/Services/AccountService.cs @@ -215,7 +215,7 @@ namespace Wino.Core.Services public async Task> GetAccountsAsync() { - var accounts = await Connection.Table().ToListAsync(); + var accounts = await Connection.Table().OrderBy(a => a.Order).ToListAsync(); foreach (var account in accounts) { @@ -301,12 +301,21 @@ namespace Wino.Core.Services { var account = await Connection.Table().FirstOrDefaultAsync(a => a.Id == accountId); - if (account?.ProviderType == MailProviderType.IMAP4) - account.ServerInformation = await GetAccountCustomServerInformationAsync(account.Id); + if (account == null) + { + _logger.Error("Could not find account with id {AccountId}", accountId); + } + else + { + if (account.ProviderType == MailProviderType.IMAP4) + account.ServerInformation = await GetAccountCustomServerInformationAsync(account.Id); - account.Preferences = await GetAccountPreferencesAsync(account.Id); + account.Preferences = await GetAccountPreferencesAsync(account.Id); - return account; + return account; + } + + return null; } public Task GetAccountCustomServerInformationAsync(Guid accountId) @@ -336,6 +345,12 @@ namespace Wino.Core.Services { _preferencesService.StartupEntityId = account.Id; } + else + { + // Set the order of the account. + // This can be changed by the user later in manage accounts page. + account.Order = accountCount; + } await Connection.InsertAsync(account); @@ -352,6 +367,8 @@ namespace Wino.Core.Services // Outlook & Office 365 supports Focused inbox. Enabled by default. bool isMicrosoftProvider = account.ProviderType == MailProviderType.Outlook || account.ProviderType == MailProviderType.Office365; + // TODO: This should come from account settings API. + // Wino doesn't have MailboxSettings yet. if (isMicrosoftProvider) account.Preferences.IsFocusedInboxEnabled = true; @@ -398,6 +415,24 @@ namespace Wino.Core.Services return account.SynchronizationDeltaIdentifier; } + public async Task UpdateAccountOrdersAsync(Dictionary accountIdOrderPair) + { + foreach (var pair in accountIdOrderPair) + { + var account = await GetAccountAsync(pair.Key); + if (account == null) + { + _logger.Information("Could not find account with id {Key} for reordering. It may be a linked account.", pair.Key); + continue; + } + + account.Order = pair.Value; + + await Connection.UpdateAsync(account); + } + + Messenger.Send(new AccountMenuItemsReordered(accountIdOrderPair)); + } } } diff --git a/Wino.Mail.ViewModels/AccountManagementViewModel.cs b/Wino.Mail.ViewModels/AccountManagementViewModel.cs index e370f190..712ac35a 100644 --- a/Wino.Mail.ViewModels/AccountManagementViewModel.cs +++ b/Wino.Mail.ViewModels/AccountManagementViewModel.cs @@ -42,6 +42,7 @@ namespace Wino.Mail.ViewModels public bool IsPurchasePanelVisible => !HasUnlimitedAccountProduct; public bool IsAccountCreationAlmostOnLimit => Accounts != null && Accounts.Count == FREE_ACCOUNT_COUNT - 1; public bool HasAccountsDefined => Accounts != null && Accounts.Any(); + public bool CanReorderAccounts => Accounts?.Count > 1; public string UsedAccountsString => string.Format(Translator.WinoUpgradeRemainingAccountsMessage, Accounts.Count, FREE_ACCOUNT_COUNT); @@ -263,6 +264,9 @@ namespace Wino.Mail.ViewModels mergedAccountProviderDetailViewModel)); } + [RelayCommand(CanExecute = nameof(CanReorderAccounts))] + private Task ReorderAccountsAsync() => DialogService.ShowAccountReorderDialogAsync(availableAccounts: Accounts); + public override void OnNavigatedFrom(NavigationMode mode, object parameters) { base.OnNavigatedFrom(mode, parameters); @@ -276,6 +280,9 @@ namespace Wino.Mail.ViewModels { OnPropertyChanged(nameof(HasAccountsDefined)); OnPropertyChanged(nameof(UsedAccountsString)); + OnPropertyChanged(nameof(IsAccountCreationAlmostOnLimit)); + + ReorderAccountsCommand.NotifyCanExecuteChanged(); } private void PagePropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) diff --git a/Wino.Mail.ViewModels/AppShellViewModel.cs b/Wino.Mail.ViewModels/AppShellViewModel.cs index 33fb15cf..5b0a4cb5 100644 --- a/Wino.Mail.ViewModels/AppShellViewModel.cs +++ b/Wino.Mail.ViewModels/AppShellViewModel.cs @@ -39,7 +39,8 @@ namespace Wino.Mail.ViewModels IRecipient, IRecipient, IRecipient, - IRecipient + IRecipient, + IRecipient { #region Menu Items @@ -1059,5 +1060,19 @@ namespace Wino.Mail.ViewModels ChangeLoadedAccount(latestSelectedAccountMenuItem, navigateInbox: false); } + + private void ReorderAccountMenuItems(Dictionary newAccountOrder) + { + foreach (var item in newAccountOrder) + { + var menuItem = MenuItems.GetAccountMenuItem(item.Key); + + if (menuItem == null) continue; + + MenuItems.Move(MenuItems.IndexOf(menuItem), item.Value); + } + } + + public void Receive(AccountMenuItemsReordered message) => ReorderAccountMenuItems(message.newOrderDictionary); } } diff --git a/Wino.Mail.ViewModels/Data/AccountProviderDetailViewModel.cs b/Wino.Mail.ViewModels/Data/AccountProviderDetailViewModel.cs index 4f629e99..cd2d2a89 100644 --- a/Wino.Mail.ViewModels/Data/AccountProviderDetailViewModel.cs +++ b/Wino.Mail.ViewModels/Data/AccountProviderDetailViewModel.cs @@ -17,6 +17,10 @@ namespace Wino.Mail.ViewModels.Data public string StartupEntityTitle => Account.Name; + public int Order => Account.Order; + + public string StartupEntityAddresses => Account.Address; + public AccountProviderDetailViewModel(IProviderDetail providerDetail, MailAccount account) { ProviderDetail = providerDetail; diff --git a/Wino.Mail.ViewModels/Data/MergedAccountProviderDetailViewModel.cs b/Wino.Mail.ViewModels/Data/MergedAccountProviderDetailViewModel.cs index 6dda7ac3..1bd7de5b 100644 --- a/Wino.Mail.ViewModels/Data/MergedAccountProviderDetailViewModel.cs +++ b/Wino.Mail.ViewModels/Data/MergedAccountProviderDetailViewModel.cs @@ -18,6 +18,12 @@ namespace Wino.Mail.ViewModels.Data public string StartupEntityTitle => MergedInbox.Name; + public int Order => 0; + + public IProviderDetail ProviderDetail { get; set; } + + public string StartupEntityAddresses => AccountAddresses; + public MergedAccountProviderDetailViewModel(MergedInbox mergedInbox, List holdingAccounts) { MergedInbox = mergedInbox; diff --git a/Wino.Mail/Dialogs/AccountReorderDialog.xaml b/Wino.Mail/Dialogs/AccountReorderDialog.xaml new file mode 100644 index 00000000..4f733a3a --- /dev/null +++ b/Wino.Mail/Dialogs/AccountReorderDialog.xaml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wino.Mail/Dialogs/AccountReorderDialog.xaml.cs b/Wino.Mail/Dialogs/AccountReorderDialog.xaml.cs new file mode 100644 index 00000000..c5433c7e --- /dev/null +++ b/Wino.Mail/Dialogs/AccountReorderDialog.xaml.cs @@ -0,0 +1,52 @@ +using System.Collections.ObjectModel; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Windows.UI.Xaml.Controls; +using Wino.Core.Domain.Interfaces; + +namespace Wino.Dialogs +{ + public sealed partial class AccountReorderDialog : ContentDialog + { + public ObservableCollection Accounts { get; } + + private int count; + private bool isOrdering = false; + + private readonly IAccountService _accountService = App.Current.Services.GetService(); + + public AccountReorderDialog(ObservableCollection accounts) + { + Accounts = accounts; + + count = accounts.Count; + + InitializeComponent(); + } + + private void DialogOpened(ContentDialog sender, ContentDialogOpenedEventArgs args) + { + Accounts.CollectionChanged -= AccountsChanged; + Accounts.CollectionChanged += AccountsChanged; + } + + private void DialogClosed(ContentDialog sender, ContentDialogClosedEventArgs args) => Accounts.CollectionChanged -= AccountsChanged; + + private async void AccountsChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) + { + if (count - 1 == Accounts.Count) + isOrdering = true; + + if (count == Accounts.Count && isOrdering) + { + // Order is completed. Apply changes. + + var dict = Accounts.ToDictionary(a => a.StartupEntityId, a => Accounts.IndexOf(a)); + + await _accountService.UpdateAccountOrdersAsync(dict); + + isOrdering = false; + } + } + } +} diff --git a/Wino.Mail/Selectors/AccountReorderTemplateSelector.cs b/Wino.Mail/Selectors/AccountReorderTemplateSelector.cs new file mode 100644 index 00000000..07574892 --- /dev/null +++ b/Wino.Mail/Selectors/AccountReorderTemplateSelector.cs @@ -0,0 +1,22 @@ +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Wino.Mail.ViewModels.Data; + +namespace Wino.Selectors +{ + public class AccountReorderTemplateSelector : DataTemplateSelector + { + public DataTemplate MergedAccountReorderTemplate { get; set; } + public DataTemplate RootAccountReorderTemplate { get; set; } + + protected override DataTemplate SelectTemplateCore(object item, DependencyObject container) + { + if (item is MergedAccountProviderDetailViewModel) + { + return MergedAccountReorderTemplate; + } + + return RootAccountReorderTemplate; + } + } +} diff --git a/Wino.Mail/Services/DialogService.cs b/Wino.Mail/Services/DialogService.cs index e38ba569..2d751080 100644 --- a/Wino.Mail/Services/DialogService.cs +++ b/Wino.Mail/Services/DialogService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.Messaging; @@ -331,6 +332,14 @@ namespace Wino.Services return accountPicker.PickedAccount; } + public async Task ShowAccountReorderDialogAsync(ObservableCollection availableAccounts) + { + var accountReorderDialog = new AccountReorderDialog(availableAccounts) + { + RequestedTheme = _themeService.RootTheme.ToWindowsElementTheme() + }; + await HandleDialogPresentationAsync(accountReorderDialog); + } } } diff --git a/Wino.Mail/Views/Account/AccountManagementPage.xaml b/Wino.Mail/Views/Account/AccountManagementPage.xaml index 266d2c04..1f79d9cd 100644 --- a/Wino.Mail/Views/Account/AccountManagementPage.xaml +++ b/Wino.Mail/Views/Account/AccountManagementPage.xaml @@ -222,6 +222,17 @@ + + + + + + diff --git a/Wino.Mail/Wino.Mail.csproj b/Wino.Mail/Wino.Mail.csproj index c27cf3d5..a0e55b3b 100644 --- a/Wino.Mail/Wino.Mail.csproj +++ b/Wino.Mail/Wino.Mail.csproj @@ -252,6 +252,9 @@ AccountPickerDialog.xaml + + AccountReorderDialog.xaml + CustomThemeBuilderDialog.xaml @@ -319,6 +322,7 @@ + @@ -484,6 +488,10 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + Designer MSBuild:Compile