Add capability-first account and calendar setup flow

This commit is contained in:
Burak Kaan Köse
2026-04-20 19:38:30 +02:00
parent 54148716bb
commit d85812ed7b
41 changed files with 1369 additions and 333 deletions
@@ -17,6 +17,7 @@ using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Misc;
using Wino.Core.Services;
using Wino.Core.ViewModels.Data;
@@ -96,8 +97,14 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
[ObservableProperty]
private bool isTaskbarBadgeEnabled;
[ObservableProperty]
public partial AccountCapabilityOption SelectedCapabilityOption { get; set; }
public bool IsFocusedInboxSupportedForAccount => Account != null && Account.Preferences.IsFocusedInboxEnabled != null;
public bool IsImapServer => ServerInformation != null;
public bool HasMailAccess => Account?.IsMailAccessGranted == true;
public bool HasCalendarAccess => Account?.IsCalendarAccessGranted == true;
public bool IsOAuthCapabilityEditable => Account?.ProviderType is MailProviderType.Outlook or MailProviderType.Gmail;
public string ProviderIconPath => Account?.SpecialImapProvider != SpecialImapProvider.None
? $"ms-appx:///Assets/Providers/{Account.SpecialImapProvider}.png"
: $"ms-appx:///Assets/Providers/{Account?.ProviderType}.png";
@@ -130,6 +137,13 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
new ImapConnectionSecurityModel(Core.Domain.Enums.ImapConnectionSecurity.None, Translator.ImapConnectionSecurity_None)
];
public List<AccountCapabilityOption> CapabilityOptions { get; } =
[
new(true, false, Translator.AccountCapability_MailOnly),
new(false, true, Translator.AccountCapability_CalendarOnly),
new(true, true, Translator.AccountCapability_MailAndCalendar)
];
public AccountDetailsPageViewModel(IMailDialogService dialogService,
IAccountService accountService,
@@ -262,6 +276,7 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
AccountName = Account.Name;
SenderName = Account.SenderName;
ServerInformation = Account.ServerInformation;
SelectedCapabilityOption = ResolveCapabilityOption(Account.IsMailAccessGranted, Account.IsCalendarAccessGranted);
IsFocusedInboxEnabled = Account.Preferences.IsFocusedInboxEnabled.GetValueOrDefault();
AreNotificationsEnabled = Account.Preferences.IsNotificationsEnabled;
@@ -288,7 +303,11 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
SelectedOutgoingServerConnectionSecurityIndex = AvailableConnectionSecurities.FindIndex(a => a.ImapConnectionSecurity == ServerInformation.OutgoingServerSocketOption);
}
SelectedTabIndex = _statePersistanceService.ApplicationMode == WinoApplicationMode.Calendar ? 2 : 1;
SelectedTabIndex = _statePersistanceService.ApplicationMode == WinoApplicationMode.Calendar && HasCalendarAccess
? 2
: HasMailAccess
? 1
: 0;
var folderStructures = (await _folderService.GetFolderStructureForAccountAsync(Account.Id, true)).Folders;
@@ -382,11 +401,15 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
partial void OnAccountChanged(MailAccount value)
{
SelectedCapabilityOption = ResolveCapabilityOption(value?.IsMailAccessGranted == true, value?.IsCalendarAccessGranted == true);
OnPropertyChanged(nameof(IsFocusedInboxSupportedForAccount));
OnPropertyChanged(nameof(ProviderIconPath));
OnPropertyChanged(nameof(Address));
OnPropertyChanged(nameof(IsInitialSynchronizationSummaryVisible));
OnPropertyChanged(nameof(InitialSynchronizationSummary));
OnPropertyChanged(nameof(HasMailAccess));
OnPropertyChanged(nameof(HasCalendarAccess));
OnPropertyChanged(nameof(IsOAuthCapabilityEditable));
}
protected override async void OnPropertyChanged(PropertyChangedEventArgs e)
@@ -417,6 +440,21 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
Account.Preferences.IsTaskbarBadgeEnabled = IsTaskbarBadgeEnabled;
await _accountService.UpdateAccountAsync(Account);
break;
case nameof(SelectedCapabilityOption) when IsOAuthCapabilityEditable && SelectedCapabilityOption != null:
if (Account.IsMailAccessGranted == SelectedCapabilityOption.IsMailAccessGranted &&
Account.IsCalendarAccessGranted == SelectedCapabilityOption.IsCalendarAccessGranted)
break;
try
{
await UpdateOAuthCapabilityAsync(SelectedCapabilityOption);
}
catch (Exception ex)
{
await ExecuteUIThread(() => SelectedCapabilityOption = ResolveCapabilityOption(Account.IsMailAccessGranted, Account.IsCalendarAccessGranted));
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error, ex.Message, InfoBarMessageType.Error);
}
break;
case nameof(SelectedPrimaryCalendar) when SelectedPrimaryCalendar != null:
foreach (var calendar in AccountCalendars)
{
@@ -427,6 +465,111 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
break;
}
}
private AccountCapabilityOption ResolveCapabilityOption(bool isMailAccessGranted, bool isCalendarAccessGranted)
=> CapabilityOptions.First(option =>
option.IsMailAccessGranted == isMailAccessGranted &&
option.IsCalendarAccessGranted == isCalendarAccessGranted);
private async Task UpdateOAuthCapabilityAsync(AccountCapabilityOption selectedOption)
{
var previousMailAccess = Account.IsMailAccessGranted;
var previousCalendarAccess = Account.IsCalendarAccessGranted;
var requiresReauthorization = (selectedOption.IsMailAccessGranted && !previousMailAccess) ||
(selectedOption.IsCalendarAccessGranted && !previousCalendarAccess);
try
{
if (requiresReauthorization)
{
Account.IsMailAccessGranted = selectedOption.IsMailAccessGranted;
Account.IsCalendarAccessGranted = selectedOption.IsCalendarAccessGranted;
await SynchronizationManager.Instance.HandleAuthorizationAsync(
Account.ProviderType,
Account,
Account.ProviderType == MailProviderType.Gmail);
}
}
catch
{
Account.IsMailAccessGranted = previousMailAccess;
Account.IsCalendarAccessGranted = previousCalendarAccess;
throw;
}
Account.IsMailAccessGranted = selectedOption.IsMailAccessGranted;
Account.IsCalendarAccessGranted = selectedOption.IsCalendarAccessGranted;
await _accountService.UpdateAccountAsync(Account);
if (selectedOption.IsMailAccessGranted && !previousMailAccess)
{
await SynchronizationManager.Instance.SynchronizeProfileAsync(Account.Id);
await SynchronizationManager.Instance.SynchronizeMailAsync(new MailSynchronizationOptions
{
AccountId = Account.Id,
Type = MailSynchronizationType.FullFolders
});
if (Account.ProviderType == MailProviderType.Outlook)
{
await SynchronizationManager.Instance.SynchronizeMailAsync(new MailSynchronizationOptions
{
AccountId = Account.Id,
Type = MailSynchronizationType.Categories
});
}
if (!string.IsNullOrWhiteSpace(Account.Address))
{
var aliases = await _accountService.GetAccountAliasesAsync(Account.Id);
var hasRootAlias = aliases.Any(alias => alias.IsRootAlias);
if (!hasRootAlias)
{
await _accountService.CreateRootAliasAsync(Account.Id, Account.Address);
}
}
await SynchronizationManager.Instance.SynchronizeMailAsync(new MailSynchronizationOptions
{
AccountId = Account.Id,
Type = MailSynchronizationType.Alias
});
}
if (selectedOption.IsCalendarAccessGranted && !previousCalendarAccess)
{
await SynchronizationManager.Instance.SynchronizeCalendarAsync(new CalendarSynchronizationOptions
{
AccountId = Account.Id,
Type = CalendarSynchronizationType.CalendarMetadata
});
}
var refreshedAccount = await _accountService.GetAccountAsync(Account.Id);
await ExecuteUIThread(() =>
{
Account = refreshedAccount;
AccountName = refreshedAccount.Name;
SenderName = refreshedAccount.SenderName;
EnsureSelectedTabForCapabilities();
});
}
private void EnsureSelectedTabForCapabilities()
{
if (SelectedTabIndex == 1 && !HasMailAccess)
{
SelectedTabIndex = HasCalendarAccess ? 2 : 0;
}
else if (SelectedTabIndex == 2 && !HasCalendarAccess)
{
SelectedTabIndex = HasMailAccess ? 1 : 0;
}
}
}
public sealed class AccountCalendarShowAsOption
@@ -441,6 +584,20 @@ public sealed class AccountCalendarShowAsOption
}
}
public sealed class AccountCapabilityOption
{
public bool IsMailAccessGranted { get; }
public bool IsCalendarAccessGranted { get; }
public string DisplayText { get; }
public AccountCapabilityOption(bool isMailAccessGranted, bool isCalendarAccessGranted, string displayText)
{
IsMailAccessGranted = isMailAccessGranted;
IsCalendarAccessGranted = isCalendarAccessGranted;
DisplayText = displayText;
}
}
public partial class AccountCalendarSettingsItemViewModel : ObservableObject
{
public AccountCalendar Calendar { get; }
@@ -71,6 +71,8 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
private void BuildSteps()
{
Steps.Clear();
var shouldSetupMail = WizardContext.IsMailAccessEnabled;
var shouldSetupCalendar = WizardContext.IsCalendarAccessEnabled;
if (WizardContext.IsOAuthProvider)
{
@@ -78,31 +80,47 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
{
Title = string.Format(Translator.AccountSetup_Step_Authenticating, WizardContext.SelectedProvider.Name)
});
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_FetchingProfile });
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SavingAccount });
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingFolders });
if (WizardContext.SelectedProvider.Type == MailProviderType.Outlook)
if (shouldSetupMail)
{
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingCategories });
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_FetchingProfile });
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingFolders });
if (WizardContext.SelectedProvider.Type == MailProviderType.Outlook)
{
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingCategories });
}
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingAliases });
}
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_FetchingCalendarMetadata });
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingAliases });
if (shouldSetupCalendar)
{
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_FetchingCalendarMetadata });
}
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_Finalizing });
}
else if (WizardContext.IsSpecialImapProvider)
{
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_TestingMailAuth });
if (shouldSetupMail)
{
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_TestingMailAuth });
}
if (WizardContext.CalendarSupportMode == ImapCalendarSupportMode.CalDav)
if (shouldSetupCalendar && WizardContext.CalendarSupportMode == ImapCalendarSupportMode.CalDav)
{
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_DiscoveringCalDav });
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_TestingCalendarAuth });
}
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SavingAccount });
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingFolders });
if (shouldSetupMail)
{
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingFolders });
}
if (WizardContext.CalendarSupportMode != ImapCalendarSupportMode.Disabled)
if (shouldSetupCalendar && WizardContext.CalendarSupportMode != ImapCalendarSupportMode.Disabled)
{
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_FetchingCalendarMetadata });
}
@@ -112,7 +130,10 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
else // Generic IMAP
{
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SavingAccount });
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingFolders });
if (shouldSetupMail)
{
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingFolders });
}
var setupResult = WizardContext.ImapCalDavSetupResult;
if (setupResult?.IsCalendarAccessGranted == true &&
@@ -186,7 +207,8 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
AccountColorHex = WizardContext.AccountColorHex,
CreatedAt = accountCreatedAt,
InitialSynchronizationRange = WizardContext.SelectedInitialSynchronizationRange,
IsCalendarAccessGranted = true
IsMailAccessGranted = WizardContext.IsMailAccessEnabled,
IsCalendarAccessGranted = WizardContext.IsCalendarAccessEnabled
};
if (WizardContext.IsOAuthProvider)
@@ -208,50 +230,53 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
_dbWritten = true;
SetCurrentStepSucceeded();
// Step: Profile
SetStepInProgress(Translator.AccountSetup_Step_FetchingProfile);
var profileResult = await SynchronizationManager.Instance.SynchronizeProfileAsync(_createdAccount.Id);
if (profileResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeProfileInformation);
if (profileResult.ProfileInformation != null)
if (_createdAccount.IsMailAccessGranted)
{
_createdAccount.SenderName = profileResult.ProfileInformation.SenderName;
_createdAccount.Base64ProfilePictureData = profileResult.ProfileInformation.Base64ProfilePictureData;
// Step: Profile
SetStepInProgress(Translator.AccountSetup_Step_FetchingProfile);
var profileResult = await SynchronizationManager.Instance.SynchronizeProfileAsync(_createdAccount.Id);
if (profileResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeProfileInformation);
if (!string.IsNullOrEmpty(profileResult.ProfileInformation.AccountAddress))
if (profileResult.ProfileInformation != null)
{
if (await _accountService.AccountAddressExistsAsync(profileResult.ProfileInformation.AccountAddress, _createdAccount.Id))
throw new InvalidOperationException(Translator.DialogMessage_AccountAddressExistsMessage);
_createdAccount.SenderName = profileResult.ProfileInformation.SenderName;
_createdAccount.Base64ProfilePictureData = profileResult.ProfileInformation.Base64ProfilePictureData;
_createdAccount.Address = profileResult.ProfileInformation.AccountAddress;
if (!string.IsNullOrEmpty(profileResult.ProfileInformation.AccountAddress))
{
if (await _accountService.AccountAddressExistsAsync(profileResult.ProfileInformation.AccountAddress, _createdAccount.Id))
throw new InvalidOperationException(Translator.DialogMessage_AccountAddressExistsMessage);
_createdAccount.Address = profileResult.ProfileInformation.AccountAddress;
}
await _accountService.UpdateProfileInformationAsync(_createdAccount.Id, profileResult.ProfileInformation);
}
await _accountService.UpdateProfileInformationAsync(_createdAccount.Id, profileResult.ProfileInformation);
}
SetCurrentStepSucceeded();
// Step: Folders
SetStepInProgress(Translator.AccountSetup_Step_SyncingFolders);
var folderResult = await SynchronizationManager.Instance.SynchronizeFoldersAsync(_createdAccount.Id);
if (folderResult == null || folderResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeFolders);
SetCurrentStepSucceeded();
// Step: Categories
if (_createdAccount.IsCategorySyncSupported)
{
SetStepInProgress(Translator.AccountSetup_Step_SyncingCategories);
var categoryResult = await SynchronizationManager.Instance.SynchronizeCategoriesAsync(_createdAccount.Id);
if (categoryResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeCategories);
SetCurrentStepSucceeded();
// Step: Folders
SetStepInProgress(Translator.AccountSetup_Step_SyncingFolders);
var folderResult = await SynchronizationManager.Instance.SynchronizeFoldersAsync(_createdAccount.Id);
if (folderResult == null || folderResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeFolders);
SetCurrentStepSucceeded();
// Step: Categories
if (_createdAccount.IsCategorySyncSupported)
{
SetStepInProgress(Translator.AccountSetup_Step_SyncingCategories);
var categoryResult = await SynchronizationManager.Instance.SynchronizeCategoriesAsync(_createdAccount.Id);
if (categoryResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeCategories);
SetCurrentStepSucceeded();
}
}
// Step: Calendar metadata
SetStepInProgress(Translator.AccountSetup_Step_FetchingCalendarMetadata);
if (_createdAccount.IsCalendarAccessGranted)
{
SetStepInProgress(Translator.AccountSetup_Step_FetchingCalendarMetadata);
var calResult = await SynchronizationManager.Instance.SynchronizeCalendarAsync(new CalendarSynchronizationOptions
{
AccountId = _createdAccount.Id,
@@ -259,22 +284,25 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
});
if (calResult == null || calResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeCalendarMetadata);
SetCurrentStepSucceeded();
}
SetCurrentStepSucceeded();
// Step: Aliases
SetStepInProgress(Translator.AccountSetup_Step_SyncingAliases);
if (_createdAccount.IsAliasSyncSupported)
if (_createdAccount.IsMailAccessGranted)
{
var aliasResult = await SynchronizationManager.Instance.SynchronizeAliasesAsync(_createdAccount.Id);
if (aliasResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeAliases);
SetStepInProgress(Translator.AccountSetup_Step_SyncingAliases);
if (_createdAccount.IsAliasSyncSupported)
{
var aliasResult = await SynchronizationManager.Instance.SynchronizeAliasesAsync(_createdAccount.Id);
if (aliasResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeAliases);
}
else
{
await _accountService.CreateRootAliasAsync(_createdAccount.Id, _createdAccount.Address);
}
SetCurrentStepSucceeded();
}
else
{
await _accountService.CreateRootAliasAsync(_createdAccount.Id, _createdAccount.Address);
}
SetCurrentStepSucceeded();
}
else if (WizardContext.IsSpecialImapProvider)
{
@@ -288,13 +316,17 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
_createdAccount.Address = WizardContext.EmailAddress;
_createdAccount.SenderName = WizardContext.DisplayName;
_createdAccount.IsMailAccessGranted = dialogResult.IsMailAccessGranted;
_createdAccount.IsCalendarAccessGranted = customServerInformation.CalendarSupportMode != ImapCalendarSupportMode.Disabled;
_createdAccount.ServerInformation = customServerInformation;
// Step: Test IMAP
SetStepInProgress(Translator.AccountSetup_Step_TestingMailAuth);
await ValidateImapConnectivityAsync(customServerInformation);
SetCurrentStepSucceeded();
if (_createdAccount.IsMailAccessGranted)
{
// Step: Test IMAP
SetStepInProgress(Translator.AccountSetup_Step_TestingMailAuth);
await ValidateImapConnectivityAsync(customServerInformation);
SetCurrentStepSucceeded();
}
// Step: CalDAV discovery and testing (if applicable)
if (customServerInformation.CalendarSupportMode == ImapCalendarSupportMode.CalDav)
@@ -313,12 +345,15 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
_dbWritten = true;
SetCurrentStepSucceeded();
// Step: Folders
SetStepInProgress(Translator.AccountSetup_Step_SyncingFolders);
var folderResult = await SynchronizationManager.Instance.SynchronizeFoldersAsync(_createdAccount.Id);
if (folderResult == null || folderResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeFolders);
SetCurrentStepSucceeded();
if (_createdAccount.IsMailAccessGranted)
{
// Step: Folders
SetStepInProgress(Translator.AccountSetup_Step_SyncingFolders);
var folderResult = await SynchronizationManager.Instance.SynchronizeFoldersAsync(_createdAccount.Id);
if (folderResult == null || folderResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeFolders);
SetCurrentStepSucceeded();
}
// Step: Calendar metadata (if not disabled)
if (_createdAccount.IsCalendarAccessGranted)
@@ -334,8 +369,10 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
SetCurrentStepSucceeded();
}
// Aliases for IMAP
await _accountService.CreateRootAliasAsync(_createdAccount.Id, _createdAccount.Address);
if (_createdAccount.IsMailAccessGranted)
{
await _accountService.CreateRootAliasAsync(_createdAccount.Id, _createdAccount.Address);
}
}
else // Generic IMAP
{
@@ -350,6 +387,7 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
_createdAccount.Address = setupResult.EmailAddress;
_createdAccount.SenderName = setupResult.DisplayName;
_createdAccount.IsMailAccessGranted = setupResult.IsMailAccessGranted;
_createdAccount.IsCalendarAccessGranted = setupResult.IsCalendarAccessGranted;
_createdAccount.ServerInformation = customServerInformation;
@@ -359,12 +397,15 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
_dbWritten = true;
SetCurrentStepSucceeded();
// Step: Folders
SetStepInProgress(Translator.AccountSetup_Step_SyncingFolders);
var folderResult = await SynchronizationManager.Instance.SynchronizeFoldersAsync(_createdAccount.Id);
if (folderResult == null || folderResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeFolders);
SetCurrentStepSucceeded();
if (_createdAccount.IsMailAccessGranted)
{
// Step: Folders
SetStepInProgress(Translator.AccountSetup_Step_SyncingFolders);
var folderResult = await SynchronizationManager.Instance.SynchronizeFoldersAsync(_createdAccount.Id);
if (folderResult == null || folderResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeFolders);
SetCurrentStepSucceeded();
}
// Step: CalDAV (if applicable)
if (setupResult.IsCalendarAccessGranted &&
@@ -392,8 +433,10 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
SetCurrentStepSucceeded();
}
// Aliases for IMAP
await _accountService.CreateRootAliasAsync(_createdAccount.Id, _createdAccount.Address);
if (_createdAccount.IsMailAccessGranted)
{
await _accountService.CreateRootAliasAsync(_createdAccount.Id, _createdAccount.Address);
}
}
// Step: Finalizing
@@ -61,6 +61,7 @@ public sealed class ImapCalDavSetupResult
{
public string DisplayName { get; init; }
public string EmailAddress { get; init; }
public bool IsMailAccessGranted { get; init; }
public bool IsCalendarAccessGranted { get; init; }
public CustomServerInformation ServerInformation { get; init; }
}
@@ -21,6 +21,12 @@ public partial class WelcomeWizardContext : ObservableObject
[ObservableProperty]
public partial InitialSynchronizationRange SelectedInitialSynchronizationRange { get; set; } = InitialSynchronizationRange.SixMonths;
[ObservableProperty]
public partial bool IsMailAccessEnabled { get; set; } = true;
[ObservableProperty]
public partial bool IsCalendarAccessEnabled { get; set; } = true;
// Special IMAP fields (iCloud/Yahoo)
[ObservableProperty]
public partial string DisplayName { get; set; }
@@ -66,7 +72,9 @@ public partial class WelcomeWizardContext : ObservableObject
AccountName,
BuildSpecialImapProviderDetails(),
AccountColorHex,
SelectedInitialSynchronizationRange);
SelectedInitialSynchronizationRange,
IsMailAccessEnabled,
IsCalendarAccessEnabled);
}
public void Reset()
@@ -75,6 +83,8 @@ public partial class WelcomeWizardContext : ObservableObject
AccountName = null;
AccountColorHex = null;
SelectedInitialSynchronizationRange = InitialSynchronizationRange.SixMonths;
IsMailAccessEnabled = true;
IsCalendarAccessEnabled = true;
DisplayName = null;
EmailAddress = null;
AppSpecificPassword = null;
+43 -2
View File
@@ -1,9 +1,50 @@
using Wino.Core.Domain.Interfaces;
using System;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Wino.Core.Domain;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.ViewModels;
namespace Wino.Mail.ViewModels;
public partial class IdlePageViewModel : CoreBaseViewModel
{
public IdlePageViewModel(IMailDialogService dialogService) { }
public const string MailEmptyStateParameter = "mail-empty-state";
private readonly INavigationService _navigationService;
[ObservableProperty]
public partial bool IsMailEmptyStateVisible { get; set; }
public string MailEmptyStateTitle => Translator.MailEmptyState_Title;
public string MailEmptyStateMessage => Translator.MailEmptyState_Message;
public string AddAccountText => Translator.MailEmptyState_AddAccount;
public string ManageAccountsText => Translator.MailEmptyState_ManageAccounts;
public IdlePageViewModel(IMailDialogService dialogService, INavigationService navigationService)
{
_navigationService = navigationService;
}
public override void OnNavigatedTo(NavigationMode mode, object parameters)
{
base.OnNavigatedTo(mode, parameters);
IsMailEmptyStateVisible = string.Equals(parameters as string, MailEmptyStateParameter, StringComparison.Ordinal);
}
[RelayCommand]
private void AddAccount()
=> _navigationService.Navigate(
WinoPage.SettingsPage,
ProviderSelectionNavigationContext.CreateForSettingsAddAccount(),
NavigationReferenceFrame.ShellFrame);
[RelayCommand]
private void ManageAccounts()
=> _navigationService.Navigate(
WinoPage.SettingsPage,
WinoPage.ManageAccountsPage,
NavigationReferenceFrame.ShellFrame);
}
@@ -57,6 +57,12 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
[ObservableProperty]
private string password = string.Empty;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsMailSettingsVisible))]
[NotifyPropertyChangedFor(nameof(IsMailPasswordInputVisible))]
[NotifyPropertyChangedFor(nameof(IsMailActionsVisible))]
private bool isMailSupportEnabled = true;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsCalendarModeSelectionVisible))]
[NotifyPropertyChangedFor(nameof(IsCalDavSettingsVisible))]
@@ -141,6 +147,9 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
public bool HasProviderHint => !string.IsNullOrWhiteSpace(ProviderHint);
public bool IsBasicSetupSelected => SelectedSetupTabIndex == 0;
public bool IsAdvancedSetupSelected => SelectedSetupTabIndex == 1;
public bool IsMailSettingsVisible => IsMailSupportEnabled;
public bool IsMailPasswordInputVisible => IsMailSupportEnabled;
public bool IsMailActionsVisible => IsMailSupportEnabled;
public bool IsCalendarModeSelectionVisible => IsCalendarSupportEnabled;
public bool IsCalDavSettingsVisible => IsCalendarSupportEnabled && SelectedCalendarSupportMode == ImapCalendarSupportMode.CalDav;
public bool IsLocalCalendarModeSelected => IsCalendarSupportEnabled && SelectedCalendarSupportMode == ImapCalendarSupportMode.LocalOnly;
@@ -152,6 +161,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
public string EmailAddressHeaderText => Translator.IMAPSetupDialog_MailAddress;
public string EmailAddressPlaceholderText => Translator.IMAPSetupDialog_MailAddressPlaceholder;
public string PasswordHeaderText => Translator.IMAPSetupDialog_Password;
public string EnableMailSupportText => Translator.ProviderSelection_UseForMail;
public string EnableCalendarSupportText => Translator.ImapCalDavSettingsPage_EnableCalendarSupport;
public string AutoDiscoverButtonText => Translator.ImapCalDavSettingsPage_AutoDiscoverButton;
public string BasicTabText => Translator.ImapCalDavSettingsPage_BasicTab;
@@ -342,6 +352,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
{
try
{
ValidateCapabilitySelection();
await EnsureImapSettingsPreparedAsync().ConfigureAwait(false);
var serverInformation = BuildServerInformation();
@@ -401,6 +412,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
{
try
{
ValidateCapabilitySelection();
await EnsureImapSettingsPreparedAsync();
var serverInformation = BuildServerInformation();
@@ -416,8 +428,15 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
if (!await ValidateAccountUniquenessAsync(excludedAccountId))
return;
await ValidateImapConnectivityAsync(serverInformation);
IsImapValidationSucceeded = true;
if (IsMailSupportEnabled)
{
await ValidateImapConnectivityAsync(serverInformation);
IsImapValidationSucceeded = true;
}
else
{
IsImapValidationSucceeded = false;
}
if (serverInformation.CalendarSupportMode == ImapCalendarSupportMode.CalDav)
{
@@ -485,6 +504,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
{
DisplayName = DisplayName.Trim(),
EmailAddress = EmailAddress.Trim(),
IsMailAccessGranted = IsMailSupportEnabled,
IsCalendarAccessGranted = serverInformation.CalendarSupportMode != ImapCalendarSupportMode.Disabled,
ServerInformation = serverInformation
};
@@ -511,6 +531,14 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
}
}
partial void OnIsMailSupportEnabledChanged(bool value)
{
if (!value)
{
IsImapValidationSucceeded = false;
}
}
partial void OnSelectedCalendarSupportModeChanged(ImapCalendarSupportMode value)
{
if (value == ImapCalendarSupportMode.LocalOnly && !_localOnlyInfoShown)
@@ -562,6 +590,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
ApplyProviderHint(_editingSpecialImapProvider);
ApplyServerInformation(account.ServerInformation);
IsMailSupportEnabled = account.IsMailAccessGranted;
if (account.ServerInformation != null)
{
@@ -588,8 +617,10 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
if (!string.IsNullOrWhiteSpace(accountCreationDialogResult?.SpecialImapProviderDetails?.SenderName))
DisplayName = accountCreationDialogResult.SpecialImapProviderDetails.SenderName;
IsCalendarSupportEnabled = true;
SelectedCalendarSupportMode = ImapCalendarSupportMode.CalDav;
IsMailSupportEnabled = accountCreationDialogResult?.IsMailAccessGranted != false;
IsCalendarSupportEnabled = accountCreationDialogResult?.IsCalendarAccessGranted == true;
SelectedCalendarSupportMode = accountCreationDialogResult?.SpecialImapProviderDetails?.CalendarSupportMode
?? (IsCalendarSupportEnabled ? ImapCalendarSupportMode.CalDav : ImapCalendarSupportMode.Disabled);
var specialProvider = accountCreationDialogResult?.SpecialImapProviderDetails?.SpecialImapProvider ?? SpecialImapProvider.None;
_editingSpecialImapProvider = specialProvider;
@@ -737,6 +768,9 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
private async Task EnsureImapSettingsPreparedAsync()
{
if (!IsMailSupportEnabled)
return;
if (HasCompleteImapSettings())
return;
@@ -847,6 +881,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
{
DisplayName = DisplayName.Trim(),
EmailAddress = EmailAddress.Trim(),
IsMailAccessGranted = IsMailSupportEnabled,
IsCalendarAccessGranted = serverInformation.CalendarSupportMode != ImapCalendarSupportMode.Disabled,
ServerInformation = serverInformation
});
@@ -863,7 +898,9 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
private async Task<bool> ValidateAccountUniquenessAsync(Guid? excludedAccountId)
{
var accountName = (_pageMode == ImapCalDavSettingsPageMode.Create || _pageMode == ImapCalDavSettingsPageMode.Wizard)
var accountName = (_pageMode == ImapCalDavSettingsPageMode.Create
|| _pageMode == ImapCalDavSettingsPageMode.Wizard
|| _pageMode == ImapCalDavSettingsPageMode.AddAccount)
? _accountCreationContext?.AccountName
: null;
@@ -889,6 +926,15 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
return true;
}
private static void ValidateCapabilitySelection(bool isMailEnabled, bool isCalendarEnabled)
{
if (!isMailEnabled && !isCalendarEnabled)
throw new InvalidOperationException(Translator.ProviderSelection_CapabilityValidationMessage);
}
private void ValidateCapabilitySelection()
=> ValidateCapabilitySelection(IsMailSupportEnabled, IsCalendarSupportEnabled);
private async Task SaveEditFlowAsync(CustomServerInformation serverInformation)
{
var account = await _accountService.GetAccountAsync(_editingAccountId);
@@ -897,6 +943,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
account.SenderName = DisplayName.Trim();
account.Address = EmailAddress.Trim();
account.IsMailAccessGranted = IsMailSupportEnabled;
account.IsCalendarAccessGranted = serverInformation.CalendarSupportMode != ImapCalendarSupportMode.Disabled;
serverInformation.Id = account.ServerInformation?.Id ?? Guid.NewGuid();
@@ -908,11 +955,14 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
await _accountService.UpdateAccountCustomServerInformationAsync(serverInformation);
await _accountService.UpdateAccountAsync(account);
Messenger.Send(new NewMailSynchronizationRequested(new MailSynchronizationOptions
if (account.IsMailAccessGranted)
{
AccountId = account.Id,
Type = MailSynchronizationType.FullFolders
}));
Messenger.Send(new NewMailSynchronizationRequested(new MailSynchronizationOptions
{
AccountId = account.Id,
Type = MailSynchronizationType.FullFolders
}));
}
if (account.IsCalendarAccessGranted)
{
@@ -1007,6 +1057,9 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
private void ValidateImapSettings(CustomServerInformation serverInformation)
{
if (!IsMailSupportEnabled)
return;
ValidateIdentitySettings();
if (string.IsNullOrWhiteSpace(serverInformation.IncomingServer))
@@ -1075,7 +1128,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
private bool TryApplyKnownProviderSettingsIfNeeded(bool requireCompleteImapSettings, bool requireCompleteCalDavSettings)
{
var needsImapSettings = requireCompleteImapSettings && !HasCompleteImapSettings();
var needsImapSettings = IsMailSupportEnabled && requireCompleteImapSettings && !HasCompleteImapSettings();
var needsCalDavSettings = requireCompleteCalDavSettings
&& IsCalendarSupportEnabled
&& SelectedCalendarSupportMode == ImapCalendarSupportMode.CalDav
@@ -1114,6 +1167,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
SenderName = DisplayName.Trim(),
ProviderType = MailProviderType.IMAP4,
SpecialImapProvider = _editingSpecialImapProvider,
IsMailAccessGranted = IsMailSupportEnabled,
IsCalendarAccessGranted = mode != ImapCalendarSupportMode.Disabled
},
new AccountCreationDialogResult(
@@ -1121,7 +1175,9 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
DisplayName.Trim(),
providerDetails,
string.Empty,
_wizardContext.SelectedInitialSynchronizationRange));
_wizardContext.SelectedInitialSynchronizationRange,
IsMailSupportEnabled,
mode != ImapCalendarSupportMode.Disabled));
if (serverInformation == null)
return false;
@@ -1158,7 +1214,8 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
&& !string.IsNullOrWhiteSpace(CalDavPassword));
private bool HasCompleteImapSettings()
=> !string.IsNullOrWhiteSpace(IncomingServer)
=> !IsMailSupportEnabled
|| (!string.IsNullOrWhiteSpace(IncomingServer)
&& !string.IsNullOrWhiteSpace(IncomingServerPort)
&& !string.IsNullOrWhiteSpace(IncomingServerUsername)
&& !string.IsNullOrWhiteSpace(IncomingServerPassword)
@@ -1167,7 +1224,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
&& !string.IsNullOrWhiteSpace(OutgoingServerUsername)
&& !string.IsNullOrWhiteSpace(OutgoingServerPassword)
&& IsValidPort(IncomingServerPort)
&& IsValidPort(OutgoingServerPort);
&& IsValidPort(OutgoingServerPort));
private int FindAuthenticationMethodIndex(ImapAuthenticationMethod method)
{
+94 -15
View File
@@ -46,6 +46,8 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
IRecipient<AccountRemovedMessage>,
IRecipient<AccountUpdatedMessage>
{
private const string MailEmptyStateNavigationParameter = IdlePageViewModel.MailEmptyStateParameter;
#region Menu Items
[ObservableProperty]
@@ -188,7 +190,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
// First clear all account menu items.
MenuItems.RemoveRange(MenuItems.Where(a => a is IAccountMenuItem));
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
var accounts = await GetMailEnabledAccountsAsync().ConfigureAwait(false);
List<Guid> initializedAccountIds = new();
@@ -373,7 +375,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
private async Task ForceAllAccountSynchronizationsAsync()
{
// Run Inbox synchronization for all accounts on startup.
var accounts = await _accountService.GetAccountsAsync();
var accounts = await GetMailEnabledAccountsAsync().ConfigureAwait(false);
foreach (var account in accounts)
{
@@ -442,6 +444,9 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
private async Task ProcessLaunchDefaultAsync()
{
if (await NavigateToMailEmptyStateIfNeededAsync().ConfigureAwait(false))
return;
if (PreferencesService.StartupEntityId == null)
{
NavigateToWelcomeWizard();
@@ -468,8 +473,16 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
}
else
{
// Fallback to the welcome wizard if startup entity is not found.
NavigateToWelcomeWizard();
var firstMailAccountMenuItem = MenuItems.FirstOrDefault(a => a is IAccountMenuItem) as IAccountMenuItem;
if (firstMailAccountMenuItem != null)
{
firstMailAccountMenuItem.Expand();
await ChangeLoadedAccountAsync(firstMailAccountMenuItem);
}
else
{
NavigateToWelcomeWizard();
}
}
}
}
@@ -947,6 +960,11 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
operationAccount = selectedFolderMenuItem.ParentAccount;
}
if (operationAccount?.IsMailAccessGranted == false)
{
operationAccount = null;
}
// We couldn't find any account so far.
// If there is only 1 account to use, use it. If not,
// send a message for flyout so user can pick from it.
@@ -956,7 +974,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
// No selected account.
// List all accounts and let user pick one.
var accounts = await _accountService.GetAccountsAsync();
var accounts = await GetMailEnabledAccountsAsync().ConfigureAwait(false);
if (!accounts.Any())
{
@@ -1081,7 +1099,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
public async void Receive(MailtoProtocolMessageRequested message)
{
var accounts = await _accountService.GetAccountsAsync();
var accounts = await GetMailEnabledAccountsAsync().ConfigureAwait(false);
MailAccount targetAccount = null;
@@ -1114,7 +1132,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
if (shareRequest?.Files == null || shareRequest.Files.Count == 0)
return;
var accounts = await _accountService.GetAccountsAsync();
var accounts = await GetMailEnabledAccountsAsync().ConfigureAwait(false);
if (!accounts.Any())
return;
@@ -1188,7 +1206,10 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
else
{
await ExecuteUIThread(() => SelectedMenuItem = null);
NavigateToWelcomeWizard();
if (!await NavigateToMailEmptyStateIfNeededAsync().ConfigureAwait(false))
{
NavigateToWelcomeWizard();
}
}
}
@@ -1199,6 +1220,37 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
NavigationReferenceFrame.ShellFrame,
NavigationTransitionType.None);
private Task<List<MailAccount>> GetMailEnabledAccountsAsync()
=> GetAccountsByCapabilityAsync(requireMail: true);
private async Task<List<MailAccount>> GetAccountsByCapabilityAsync(bool requireMail = false, bool requireCalendar = false)
{
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
return accounts
.Where(account => (!requireMail || account.IsMailAccessGranted) &&
(!requireCalendar || account.IsCalendarAccessGranted))
.ToList();
}
private async Task<bool> NavigateToMailEmptyStateIfNeededAsync()
{
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
if (!accounts.Any() || accounts.Any(account => account.IsMailAccessGranted))
return false;
latestSelectedAccountMenuItem = null;
await ExecuteUIThread(() => SelectedMenuItem = null);
NavigationService.Navigate(
WinoPage.IdlePage,
MailEmptyStateNavigationParameter,
NavigationReferenceFrame.InnerShellFrame,
NavigationTransitionType.None);
return true;
}
private bool IsAccountCurrentlyLoaded(Guid accountId)
{
return latestSelectedAccountMenuItem?.HoldingAccounts?.Any(a => a.Id == accountId) == true;
@@ -1385,7 +1437,9 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
public async void Receive(AccountRemovedMessage message)
{
var remainingAccounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
if (!remainingAccounts.Any())
var remainingMailAccounts = remainingAccounts.Where(account => account.IsMailAccessGranted).ToList();
if (!remainingMailAccounts.Any())
{
latestSelectedAccountMenuItem = null;
await ExecuteUIThread(() =>
@@ -1394,6 +1448,12 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
MenuItems?.Clear();
MenuItems?.Add(CreateMailMenuItem);
});
if (remainingAccounts.Any())
{
await NavigateToMailEmptyStateIfNeededAsync().ConfigureAwait(false);
}
return;
}
@@ -1413,22 +1473,34 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
await RecreateMenuItemsAsync();
if (!createdAccount.IsMailAccessGranted)
{
await NavigateToMailEmptyStateIfNeededAsync().ConfigureAwait(false);
}
if (!MenuItems.TryGetAccountMenuItem(createdAccount.Id, out IAccountMenuItem createdMenuItem))
{
Log.Warning("Created account {AccountId} could not be found in menu items after refresh.", createdAccount.Id);
if (!createdAccount.IsMailAccessGranted)
return;
return;
}
await ChangeLoadedAccountAsync(createdMenuItem);
// Each created account should start a new synchronization automatically.
var options = new MailSynchronizationOptions()
if (createdAccount.IsMailAccessGranted)
{
AccountId = createdAccount.Id,
Type = MailSynchronizationType.FullFolders,
};
// Each created account should start a new synchronization automatically.
var options = new MailSynchronizationOptions()
{
AccountId = createdAccount.Id,
Type = MailSynchronizationType.FullFolders,
};
Messenger.Send(new NewMailSynchronizationRequested(options));
Messenger.Send(new NewMailSynchronizationRequested(options));
}
if (createdAccount.IsCalendarAccessGranted)
{
@@ -1455,6 +1527,13 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
{
var updatedAccount = message.Account;
if (!updatedAccount.IsMailAccessGranted || !MenuItems.TryGetAccountMenuItem(updatedAccount.Id, out _))
{
await RecreateMenuItemsAsync();
await RestoreSelectedAccountAfterMenuRefreshAsync(false);
return;
}
await ExecuteUIThread(() =>
{
if (MenuItems.TryGetAccountMenuItem(updatedAccount.Id, out IAccountMenuItem foundAccountMenuItem))
@@ -10,9 +10,7 @@ using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Requests.Category;
using Wino.Core.Services;
@@ -53,11 +51,11 @@ public partial class MailCategoryManagementPageViewModel : MailBaseViewModel
if (parameters is not Guid accountId)
return;
Account = await _accountService.GetAccountAsync(accountId).ConfigureAwait(false);
Account = await _accountService.GetAccountAsync(accountId);
if (Account != null)
{
await LoadCategoriesAsync().ConfigureAwait(false);
await LoadCategoriesAsync();
}
}
@@ -15,6 +15,13 @@ using Wino.Messaging.Client.Navigation;
namespace Wino.Mail.ViewModels;
public enum ProviderSelectionWizardStep
{
Provider = 0,
Identity = 1,
Capabilities = 2
}
public partial class ProviderSelectionPageViewModel : MailBaseViewModel
{
private readonly IAccountService _accountService;
@@ -45,16 +52,50 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsInitialSynchronizationWarningVisible))]
[NotifyPropertyChangedFor(nameof(IsMailSynchronizationRangeVisible))]
public partial InitialSynchronizationRangeOption SelectedInitialSynchronizationRange { get; set; }
[ObservableProperty]
public partial string AccountName { get; set; }
[ObservableProperty]
public partial bool CanProceed { get; set; }
[NotifyPropertyChangedFor(nameof(IsMailSynchronizationRangeVisible))]
public partial bool IsMailAccessEnabled { get; set; } = true;
[ObservableProperty]
public partial bool IsCalendarAccessEnabled { get; set; } = true;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CurrentStepNumber))]
[NotifyPropertyChangedFor(nameof(StepProgressValue))]
[NotifyPropertyChangedFor(nameof(StepProgressText))]
[NotifyPropertyChangedFor(nameof(IsProviderStepVisible))]
[NotifyPropertyChangedFor(nameof(IsIdentityStepVisible))]
[NotifyPropertyChangedFor(nameof(IsCapabilityStepVisible))]
[NotifyPropertyChangedFor(nameof(CanGoBack))]
[NotifyCanExecuteChangedFor(nameof(ContinueCommand))]
[NotifyCanExecuteChangedFor(nameof(GoBackCommand))]
public partial ProviderSelectionWizardStep CurrentStep { get; set; } = ProviderSelectionWizardStep.Provider;
public bool IsColorSelected => SelectedColor != null;
public bool IsInitialSynchronizationWarningVisible => SelectedInitialSynchronizationRange?.IsEverything == true;
public bool IsInitialSynchronizationWarningVisible => IsMailSynchronizationRangeVisible && SelectedInitialSynchronizationRange?.IsEverything == true;
public bool IsMailSynchronizationRangeVisible => IsMailAccessEnabled;
public int CurrentStepNumber => (int)CurrentStep + 1;
public double StepProgressValue => CurrentStepNumber;
public string StepProgressText => string.Format(Translator.ProviderSelection_StepProgress, CurrentStepNumber);
public bool IsProviderStepVisible => CurrentStep == ProviderSelectionWizardStep.Provider;
public bool IsIdentityStepVisible => CurrentStep == ProviderSelectionWizardStep.Identity;
public bool IsCapabilityStepVisible => CurrentStep == ProviderSelectionWizardStep.Capabilities;
public bool CanGoBack => CurrentStep != ProviderSelectionWizardStep.Provider;
public string SelectedProviderName => SelectedProvider?.Name ?? string.Empty;
public string SelectedProviderDescription => SelectedProvider?.Description ?? string.Empty;
public string SelectedProviderImage => SelectedProvider?.ProviderImage ?? string.Empty;
public string SelectedProviderCapabilityDescription => GetSelectedProviderCapabilityDescription();
public bool IsCapabilitySelectionMissing => !IsMailAccessEnabled && !IsCalendarAccessEnabled;
public bool IsCalendarOnlyServerHintVisible =>
SelectedProvider?.Type == MailProviderType.IMAP4 &&
!IsMailAccessEnabled &&
IsCalendarAccessEnabled;
public ProviderSelectionPageViewModel(
IAccountService accountService,
@@ -101,48 +142,110 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
p.Type == WizardContext.SelectedProvider.Type &&
p.SpecialImapProvider == WizardContext.SelectedProvider.SpecialImapProvider);
AccountName = WizardContext.AccountName;
IsMailAccessEnabled = WizardContext.IsMailAccessEnabled;
IsCalendarAccessEnabled = WizardContext.IsCalendarAccessEnabled;
if (WizardContext.AccountColorHex != null)
SelectedColor = AvailableColors.FirstOrDefault(c => c.Hex == WizardContext.AccountColorHex);
}
else
{
IsMailAccessEnabled = true;
IsCalendarAccessEnabled = true;
}
Validate();
CurrentStep = mode == NavigationMode.Back && SelectedProvider != null
? ProviderSelectionWizardStep.Capabilities
: ProviderSelectionWizardStep.Provider;
}
partial void OnSelectedProviderChanged(IProviderDetail value)
{
Validate();
OnPropertyChanged(nameof(SelectedProviderName));
OnPropertyChanged(nameof(SelectedProviderDescription));
OnPropertyChanged(nameof(SelectedProviderImage));
OnPropertyChanged(nameof(SelectedProviderCapabilityDescription));
OnPropertyChanged(nameof(IsCapabilitySelectionMissing));
OnPropertyChanged(nameof(IsCalendarOnlyServerHintVisible));
ContinueCommand.NotifyCanExecuteChanged();
}
partial void OnAccountNameChanged(string value) => Validate();
partial void OnAccountNameChanged(string value) => ContinueCommand.NotifyCanExecuteChanged();
partial void OnIsMailAccessEnabledChanged(bool value)
{
OnPropertyChanged(nameof(IsCapabilitySelectionMissing));
OnPropertyChanged(nameof(IsCalendarOnlyServerHintVisible));
ContinueCommand.NotifyCanExecuteChanged();
}
partial void OnIsCalendarAccessEnabledChanged(bool value)
{
OnPropertyChanged(nameof(IsCapabilitySelectionMissing));
OnPropertyChanged(nameof(IsCalendarOnlyServerHintVisible));
ContinueCommand.NotifyCanExecuteChanged();
}
[RelayCommand]
private void ClearColor() => SelectedColor = null;
private void Validate()
private bool CanContinue()
{
CanProceed = SelectedProvider != null && !string.IsNullOrWhiteSpace(AccountName);
return CurrentStep switch
{
ProviderSelectionWizardStep.Provider => SelectedProvider != null,
ProviderSelectionWizardStep.Identity => !string.IsNullOrWhiteSpace(AccountName),
ProviderSelectionWizardStep.Capabilities => IsMailAccessEnabled || IsCalendarAccessEnabled,
_ => false
};
}
[RelayCommand]
private async Task ProceedAsync()
[RelayCommand(CanExecute = nameof(CanGoBack))]
private void GoBack()
{
if (!CanProceed) return;
if (await _accountService.AccountNameExistsAsync(AccountName))
{
await _dialogService.ShowMessageAsync(
Translator.DialogMessage_AccountNameExistsMessage,
Translator.DialogMessage_AccountExistsTitle,
WinoCustomMessageDialogIcon.Warning);
if (!CanGoBack)
return;
CurrentStep--;
}
[RelayCommand(CanExecute = nameof(CanContinue))]
private async Task ContinueAsync()
{
switch (CurrentStep)
{
case ProviderSelectionWizardStep.Provider:
CurrentStep = ProviderSelectionWizardStep.Identity;
return;
case ProviderSelectionWizardStep.Identity:
if (await _accountService.AccountNameExistsAsync(AccountName?.Trim()))
{
await _dialogService.ShowMessageAsync(
Translator.DialogMessage_AccountNameExistsMessage,
Translator.DialogMessage_AccountExistsTitle,
WinoCustomMessageDialogIcon.Warning);
return;
}
CurrentStep = ProviderSelectionWizardStep.Capabilities;
return;
case ProviderSelectionWizardStep.Capabilities:
await CompleteWizardAsync();
return;
}
}
private async Task CompleteWizardAsync()
{
if (!CanContinue())
return;
}
// Persist to wizard context
WizardContext.SelectedProvider = SelectedProvider;
WizardContext.AccountName = AccountName?.Trim();
WizardContext.AccountColorHex = SelectedColor?.Hex ?? string.Empty;
WizardContext.SelectedInitialSynchronizationRange = SelectedInitialSynchronizationRange?.Range ?? InitialSynchronizationRange.SixMonths;
WizardContext.IsMailAccessEnabled = IsMailAccessEnabled;
WizardContext.IsCalendarAccessEnabled = IsCalendarAccessEnabled;
if (WizardContext.IsGenericImap)
{
@@ -172,4 +275,23 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
WinoPage.AccountSetupProgressPage));
}
}
partial void OnSelectedProviderChanging(IProviderDetail value)
{
}
private string GetSelectedProviderCapabilityDescription()
{
if (SelectedProvider == null)
return string.Empty;
if (SelectedProvider.Type is MailProviderType.Outlook or MailProviderType.Gmail)
return Translator.ProviderSelection_CapabilityProviderDescription_OAuth;
if (SelectedProvider.SpecialImapProvider is SpecialImapProvider.iCloud or SpecialImapProvider.Yahoo)
return Translator.ProviderSelection_CapabilityProviderDescription_SpecialImap;
return Translator.ProviderSelection_CapabilityProviderDescription_CustomServer;
}
}
@@ -38,11 +38,15 @@ public partial class SpecialImapCredentialsPageViewModel : MailBaseViewModel
public partial string AppSpecificPassword { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(RequiresAppSpecificPassword))]
public partial int SelectedCalendarModeIndex { get; set; }
[ObservableProperty]
public partial bool CanProceed { get; set; }
public bool IsCalendarModeSelectionVisible => WizardContext.IsCalendarAccessEnabled;
public bool RequiresAppSpecificPassword => WizardContext.IsMailAccessEnabled || SelectedCalendarModeIndex == 1;
public string AppPasswordHelpUrl
{
get
@@ -86,8 +90,15 @@ public partial class SpecialImapCredentialsPageViewModel : MailBaseViewModel
_ => 0
};
if (!WizardContext.IsCalendarAccessEnabled)
{
SelectedCalendarModeIndex = 0;
}
OnPropertyChanged(nameof(AppPasswordHelpUrl));
OnPropertyChanged(nameof(CalendarModeCalDavDescription));
OnPropertyChanged(nameof(IsCalendarModeSelectionVisible));
OnPropertyChanged(nameof(RequiresAppSpecificPassword));
Validate();
}
@@ -95,13 +106,18 @@ public partial class SpecialImapCredentialsPageViewModel : MailBaseViewModel
partial void OnDisplayNameChanged(string value) => Validate();
partial void OnEmailAddressChanged(string value) => Validate();
partial void OnAppSpecificPasswordChanged(string value) => Validate();
partial void OnSelectedCalendarModeIndexChanged(int value)
{
OnPropertyChanged(nameof(RequiresAppSpecificPassword));
Validate();
}
private void Validate()
{
CanProceed = !string.IsNullOrWhiteSpace(DisplayName)
&& !string.IsNullOrWhiteSpace(EmailAddress)
&& MailAccountAddressValidator.IsValid(EmailAddress)
&& !string.IsNullOrWhiteSpace(AppSpecificPassword);
&& (!RequiresAppSpecificPassword || !string.IsNullOrWhiteSpace(AppSpecificPassword));
}
[RelayCommand]
@@ -121,12 +137,14 @@ public partial class SpecialImapCredentialsPageViewModel : MailBaseViewModel
WizardContext.DisplayName = DisplayName?.Trim();
WizardContext.EmailAddress = EmailAddress?.Trim();
WizardContext.AppSpecificPassword = AppSpecificPassword?.Trim();
WizardContext.CalendarSupportMode = SelectedCalendarModeIndex switch
{
1 => ImapCalendarSupportMode.CalDav,
2 => ImapCalendarSupportMode.LocalOnly,
_ => ImapCalendarSupportMode.Disabled
};
WizardContext.CalendarSupportMode = WizardContext.IsCalendarAccessEnabled
? SelectedCalendarModeIndex switch
{
1 => ImapCalendarSupportMode.CalDav,
2 => ImapCalendarSupportMode.LocalOnly,
_ => ImapCalendarSupportMode.Disabled
}
: ImapCalendarSupportMode.Disabled;
Messenger.Send(new BreadcrumbNavigationRequested(
Translator.WelcomeWizard_Step3Title,