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
+1 -1
View File
@@ -45,6 +45,6 @@ public class GmailAuthenticator : BaseAuthenticator, IGmailAuthenticator
return GoogleWebAuthorizationBroker.AuthorizeAsync(new ClientSecrets() return GoogleWebAuthorizationBroker.AuthorizeAsync(new ClientSecrets()
{ {
ClientId = ClientId ClientId = ClientId
}, AuthenticatorConfig.GmailScope, account.Id.ToString(), CancellationToken.None, new FileDataStore(AuthenticatorConfig.GmailTokenStoreIdentifier)); }, AuthenticatorConfig.GetGmailScope(account?.IsMailAccessGranted != false, account?.IsCalendarAccessGranted == true), account.Id.ToString(), CancellationToken.None, new FileDataStore(AuthenticatorConfig.GmailTokenStoreIdentifier));
} }
} }
+6 -3
View File
@@ -65,7 +65,10 @@ public class OutlookAuthenticator : BaseAuthenticator, IOutlookAuthenticator
_publicClientApplication = outlookAppBuilder.Build(); _publicClientApplication = outlookAppBuilder.Build();
} }
public string[] Scope => AuthenticatorConfig.OutlookScope; private string[] GetScope(MailAccount account)
=> AuthenticatorConfig.GetOutlookScope(
account?.IsMailAccessGranted != false,
account?.IsCalendarAccessGranted == true);
private async Task EnsureTokenCacheAttachedAsync() private async Task EnsureTokenCacheAttachedAsync()
{ {
@@ -91,7 +94,7 @@ public class OutlookAuthenticator : BaseAuthenticator, IOutlookAuthenticator
try try
{ {
var authResult = await _publicClientApplication.AcquireTokenSilent(Scope, storedAccount).ExecuteAsync(); var authResult = await _publicClientApplication.AcquireTokenSilent(GetScope(account), storedAccount).ExecuteAsync();
return new TokenInformationEx(authResult.AccessToken, authResult.Account.Username); return new TokenInformationEx(authResult.AccessToken, authResult.Account.Username);
} }
@@ -122,7 +125,7 @@ public class OutlookAuthenticator : BaseAuthenticator, IOutlookAuthenticator
if (_nativeAppService.GetCoreWindowHwnd == null) throw new AuthenticationAttentionException(account); if (_nativeAppService.GetCoreWindowHwnd == null) throw new AuthenticationAttentionException(account);
AuthenticationResult authResult = await _publicClientApplication AuthenticationResult authResult = await _publicClientApplication
.AcquireTokenInteractive(Scope) .AcquireTokenInteractive(GetScope(account))
.ExecuteAsync(); .ExecuteAsync();
// If the account is null, it means it's the initial creation of it. // If the account is null, it means it's the initial creation of it.
@@ -299,6 +299,9 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
foreach (var account in accounts) foreach (var account in accounts)
{ {
if (!GroupedAccountCalendarViewModel.SupportsCalendar(account))
continue;
var accountCalendars = await _calendarService.GetAccountCalendarsAsync(account.Id).ConfigureAwait(false); var accountCalendars = await _calendarService.GetAccountCalendarsAsync(account.Id).ConfigureAwait(false);
var calendarViewModels = accountCalendars.Select(calendar => new AccountCalendarViewModel(account, calendar)).ToList(); var calendarViewModels = accountCalendars.Select(calendar => new AccountCalendarViewModel(account, calendar)).ToList();
var groupedAccountCalendarViewModel = new GroupedAccountCalendarViewModel(account, calendarViewModels); var groupedAccountCalendarViewModel = new GroupedAccountCalendarViewModel(account, calendarViewModels);
@@ -408,6 +408,9 @@ public partial class CalendarEventComposePageViewModel : CalendarBaseViewModel
foreach (var account in accounts) foreach (var account in accounts)
{ {
if (!GroupedAccountCalendarViewModel.SupportsCalendar(account))
continue;
var calendars = await _calendarService.GetAccountCalendarsAsync(account.Id).ConfigureAwait(false); var calendars = await _calendarService.GetAccountCalendarsAsync(account.Id).ConfigureAwait(false);
var viewModels = calendars var viewModels = calendars
.Select(calendar => new AccountCalendarViewModel(account, calendar)) .Select(calendar => new AccountCalendarViewModel(account, calendar))
@@ -17,6 +17,9 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject
public MailAccount Account { get; } public MailAccount Account { get; }
public ObservableCollection<AccountCalendarViewModel> AccountCalendars { get; } public ObservableCollection<AccountCalendarViewModel> AccountCalendars { get; }
public static bool SupportsCalendar(MailAccount account)
=> account?.IsCalendarAccessGranted == true;
public GroupedAccountCalendarViewModel(MailAccount account, IEnumerable<AccountCalendarViewModel> calendarViewModels) public GroupedAccountCalendarViewModel(MailAccount account, IEnumerable<AccountCalendarViewModel> calendarViewModels)
{ {
Account = account; Account = account;
@@ -78,6 +78,13 @@ public class MailAccount
/// </summary> /// </summary>
public SpecialImapProvider SpecialImapProvider { get; set; } public SpecialImapProvider SpecialImapProvider { get; set; }
/// <summary>
/// Gets or sets whether mail access is granted for this account.
/// When false, mail folders, aliases, compose flows, and mail synchronization are unavailable.
/// Default is true for legacy accounts to preserve existing behavior.
/// </summary>
public bool IsMailAccessGranted { get; set; } = true;
/// <summary> /// <summary>
/// Gets or sets whether calendar access is granted for this account. /// Gets or sets whether calendar access is granted for this account.
/// When false, synchronizers will not process EventMessages or calendar invitations. /// When false, synchronizers will not process EventMessages or calendar invitations.
@@ -3,8 +3,8 @@
public interface IAuthenticatorConfig public interface IAuthenticatorConfig
{ {
string OutlookAuthenticatorClientId { get; } string OutlookAuthenticatorClientId { get; }
string[] OutlookScope { get; } string[] GetOutlookScope(bool isMailAccessGranted, bool isCalendarAccessGranted);
string GmailAuthenticatorClientId { get; } string GmailAuthenticatorClientId { get; }
string[] GmailScope { get; } string[] GetGmailScope(bool isMailAccessGranted, bool isCalendarAccessGranted);
string GmailTokenStoreIdentifier { get; } string GmailTokenStoreIdentifier { get; }
} }
@@ -7,4 +7,6 @@ public record AccountCreationDialogResult(
string AccountName, string AccountName,
SpecialImapProviderDetails SpecialImapProviderDetails, SpecialImapProviderDetails SpecialImapProviderDetails,
string AccountColorHex, string AccountColorHex,
InitialSynchronizationRange InitialSynchronizationRange); InitialSynchronizationRange InitialSynchronizationRange,
bool IsMailAccessGranted,
bool IsCalendarAccessGranted);
@@ -47,6 +47,11 @@
"AccountDetailsPage_CalendarListDescription": "Select a calendar to configure its settings", "AccountDetailsPage_CalendarListDescription": "Select a calendar to configure its settings",
"AccountDetailsPage_InitialSynchronization_Title": "Initial synchronization", "AccountDetailsPage_InitialSynchronization_Title": "Initial synchronization",
"AccountDetailsPage_InitialSynchronization_Description": "Wino synchronized your mails until {0} going back.", "AccountDetailsPage_InitialSynchronization_Description": "Wino synchronized your mails until {0} going back.",
"AccountDetailsPage_CapabilityTitle": "Connected features",
"AccountDetailsPage_CapabilityDescription": "Choose whether this account is used for mail, calendar, or both. Enabling a new feature may ask you to sign in again.",
"AccountCapability_MailOnly": "Mail only",
"AccountCapability_CalendarOnly": "Calendar only",
"AccountCapability_MailAndCalendar": "Mail + Calendar",
"AddHyperlink": "Add", "AddHyperlink": "Add",
"AppCloseBackgroundSynchronizationWarningTitle": "Background Synchronization", "AppCloseBackgroundSynchronizationWarningTitle": "Background Synchronization",
"AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Application has not been set to launch on Windows startup.", "AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Application has not been set to launch on Windows startup.",
@@ -76,6 +81,7 @@
"Buttons_ApplyTheme": "Apply Theme", "Buttons_ApplyTheme": "Apply Theme",
"Buttons_PopOut": "Pop out", "Buttons_PopOut": "Pop out",
"Buttons_Browse": "Browse", "Buttons_Browse": "Browse",
"Buttons_Back": "Back",
"Buttons_Cancel": "Cancel", "Buttons_Cancel": "Cancel",
"Buttons_Close": "Close", "Buttons_Close": "Close",
"Buttons_Copy": "Copy", "Buttons_Copy": "Copy",
@@ -681,6 +687,10 @@
"NoMailSelected": "No message selected", "NoMailSelected": "No message selected",
"NoMessageCrieteria": "No messages match your search criteria", "NoMessageCrieteria": "No messages match your search criteria",
"NoMessageEmptyFolder": "This folder is empty", "NoMessageEmptyFolder": "This folder is empty",
"MailEmptyState_Title": "No mail-enabled accounts",
"MailEmptyState_Message": "You have accounts connected for calendar, but none of them are enabled for mail. Add a mail account or update an existing account to use mail.",
"MailEmptyState_AddAccount": "Add account",
"MailEmptyState_ManageAccounts": "Manage accounts",
"Notifications_MultipleNotificationsMessage": "You have {0} new messages.", "Notifications_MultipleNotificationsMessage": "You have {0} new messages.",
"Notifications_MultipleNotificationsTitle": "New Mail", "Notifications_MultipleNotificationsTitle": "New Mail",
"Notifications_WinoUpdatedMessage": "Checkout new version {0}", "Notifications_WinoUpdatedMessage": "Checkout new version {0}",
@@ -702,8 +712,8 @@
"ProviderDetail_Gmail_Description": "Google Account", "ProviderDetail_Gmail_Description": "Google Account",
"ProviderDetail_iCloud_Description": "Apple iCloud Account", "ProviderDetail_iCloud_Description": "Apple iCloud Account",
"ProviderDetail_iCloud_Title": "iCloud", "ProviderDetail_iCloud_Title": "iCloud",
"ProviderDetail_IMAP_Description": "Custom IMAP/SMTP server", "ProviderDetail_IMAP_Description": "IMAP/SMTP mail with CalDAV or local calendar",
"ProviderDetail_IMAP_Title": "IMAP Server", "ProviderDetail_IMAP_Title": "Custom server",
"ProviderDetail_Yahoo_Description": "Yahoo Account", "ProviderDetail_Yahoo_Description": "Yahoo Account",
"ProviderDetail_Yahoo_Title": "Yahoo Mail", "ProviderDetail_Yahoo_Title": "Yahoo Mail",
"QuickEventDialog_EventName": "Event name", "QuickEventDialog_EventName": "Event name",
@@ -1427,8 +1437,8 @@
"WinoAccount_Management_ExportDialog_InProgress": "Exporting your selected Wino data...", "WinoAccount_Management_ExportDialog_InProgress": "Exporting your selected Wino data...",
"WinoAccount_Management_LocalDataSectionTitle": "Transfer with a JSON file", "WinoAccount_Management_LocalDataSectionTitle": "Transfer with a JSON file",
"WinoAccount_Management_LocalDataSectionDescription": "Import from or export to a local JSON file. Passwords, tokens, and other sensitive information are not included.", "WinoAccount_Management_LocalDataSectionDescription": "Import from or export to a local JSON file. Passwords, tokens, and other sensitive information are not included.",
"WinoAccount_Management_LocalDataImportAction": "Import JSON", "WinoAccount_Management_LocalDataImportAction": "Import",
"WinoAccount_Management_LocalDataExportAction": "Export JSON", "WinoAccount_Management_LocalDataExportAction": "Export",
"WinoAccount_Management_LocalDataSaved": "Saved your exported Wino data to {0}.", "WinoAccount_Management_LocalDataSaved": "Saved your exported Wino data to {0}.",
"WinoAccount_Management_LocalDataInvalidFile": "The selected JSON file doesn't contain a valid Wino export.", "WinoAccount_Management_LocalDataInvalidFile": "The selected JSON file doesn't contain a valid Wino export.",
"WinoAccount_Management_LoadFailed": "Wino could not load the latest Wino Account information.", "WinoAccount_Management_LoadFailed": "Wino could not load the latest Wino Account information.",
@@ -1527,8 +1537,22 @@
"WelcomeWizard_Step3Title": "Finish Setup", "WelcomeWizard_Step3Title": "Finish Setup",
"ProviderSelection_Title": "Choose your email provider", "ProviderSelection_Title": "Choose your email provider",
"ProviderSelection_Subtitle": "Select a provider below to add your email account to Wino Mail.", "ProviderSelection_Subtitle": "Select a provider below to add your email account to Wino Mail.",
"ProviderSelection_StepProgress": "Step {0} of 3",
"ProviderSelection_IdentityTitle": "Account identity",
"ProviderSelection_IdentityDescription": "Choose how this account appears inside Wino.",
"ProviderSelection_ProviderSectionTitle": "Provider",
"ProviderSelection_ProviderSectionDescription": "Select the service you want to connect.",
"ProviderSelection_CapabilitySectionTitle": "Use this account for",
"ProviderSelection_CapabilitySectionDescription": "Choose whether you want mail, calendar, or both.",
"ProviderSelection_CapabilityProviderDescription_OAuth": "On the next step, secure sign-in will connect your account. If you enable calendar, Wino will also connect Outlook Calendar or Google Calendar automatically.",
"ProviderSelection_CapabilityProviderDescription_SpecialImap": "On the next step, you'll enter your provider credentials. Mail uses IMAP/SMTP, and calendar can use CalDAV or stay local on this device.",
"ProviderSelection_CapabilityProviderDescription_CustomServer": "On the next step, you'll enter your server details. Mail uses IMAP/SMTP, and calendar can use CalDAV or stay local on this device.",
"ProviderSelection_AccountNameHeader": "Account Name", "ProviderSelection_AccountNameHeader": "Account Name",
"ProviderSelection_AccountNamePlaceholder": "e.g. Personal, Work", "ProviderSelection_AccountNamePlaceholder": "e.g. Personal, Work",
"ProviderSelection_UseForMail": "Mail",
"ProviderSelection_UseForCalendar": "Calendar",
"ProviderSelection_CapabilityValidationMessage": "Choose at least one capability before continuing.",
"ProviderSelection_CalendarOnlyServerHint": "If you continue with calendar only, the next page will not require an email address.",
"ProviderSelection_DisplayNameHeader": "Display Name", "ProviderSelection_DisplayNameHeader": "Display Name",
"ProviderSelection_DisplayNamePlaceholder": "e.g. John Doe", "ProviderSelection_DisplayNamePlaceholder": "e.g. John Doe",
"ProviderSelection_EmailHeader": "E-mail Address", "ProviderSelection_EmailHeader": "E-mail Address",
+3
View File
@@ -0,0 +1,3 @@
using Xunit;
[assembly: CollectionBehavior(DisableTestParallelization = true)]
@@ -83,7 +83,7 @@ public class AccountServiceTests : IAsyncLifetime
var secondAccountId = Guid.NewGuid(); var secondAccountId = Guid.NewGuid();
await _accountService.CreateAccountAsync( await _accountService.CreateAccountAsync(
CreateImapAccount(firstAccountId), CreateImapAccount(firstAccountId, "IMAP Test Account 1", "imap1@test.local"),
new CustomServerInformation new CustomServerInformation
{ {
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
@@ -92,7 +92,7 @@ public class AccountServiceTests : IAsyncLifetime
}); });
await _accountService.CreateAccountAsync( await _accountService.CreateAccountAsync(
CreateImapAccount(secondAccountId), CreateImapAccount(secondAccountId, "IMAP Test Account 2", "imap2@test.local"),
new CustomServerInformation new CustomServerInformation
{ {
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
@@ -119,13 +119,13 @@ public class AccountServiceTests : IAsyncLifetime
.BeGreaterThanOrEqualTo(50); .BeGreaterThanOrEqualTo(50);
} }
private static MailAccount CreateImapAccount(Guid accountId) private static MailAccount CreateImapAccount(Guid accountId, string name = "IMAP Test Account", string address = "imap@test.local")
{ {
return new MailAccount return new MailAccount
{ {
Id = accountId, Id = accountId,
Name = "IMAP Test Account", Name = name,
Address = "imap@test.local", Address = address,
SenderName = "IMAP Test", SenderName = "IMAP Test",
ProviderType = MailProviderType.IMAP4 ProviderType = MailProviderType.IMAP4
}; };
@@ -13,6 +13,8 @@ public sealed class MailRequestStateTests
[Fact] [Fact]
public void MarkReadRequest_RevertUiChanges_RestoresOriginalReadState() public void MarkReadRequest_RevertUiChanges_RestoresOriginalReadState()
{ {
WeakReferenceMessenger.Default.Reset();
var mailCopy = CreateMailCopy(isRead: false, isFlagged: false); var mailCopy = CreateMailCopy(isRead: false, isFlagged: false);
var request = new MarkReadRequest(mailCopy, IsRead: true); var request = new MarkReadRequest(mailCopy, IsRead: true);
var recipient = new MailRequestRecipient(); var recipient = new MailRequestRecipient();
@@ -35,12 +37,15 @@ public sealed class MailRequestStateTests
finally finally
{ {
WeakReferenceMessenger.Default.UnregisterAll(recipient); WeakReferenceMessenger.Default.UnregisterAll(recipient);
WeakReferenceMessenger.Default.Reset();
} }
} }
[Fact] [Fact]
public void ChangeFlagRequest_RevertUiChanges_RestoresOriginalFlagState() public void ChangeFlagRequest_RevertUiChanges_RestoresOriginalFlagState()
{ {
WeakReferenceMessenger.Default.Reset();
var mailCopy = CreateMailCopy(isRead: true, isFlagged: false); var mailCopy = CreateMailCopy(isRead: true, isFlagged: false);
var request = new ChangeFlagRequest(mailCopy, IsFlagged: true); var request = new ChangeFlagRequest(mailCopy, IsFlagged: true);
var recipient = new MailRequestRecipient(); var recipient = new MailRequestRecipient();
@@ -63,6 +68,7 @@ public sealed class MailRequestStateTests
finally finally
{ {
WeakReferenceMessenger.Default.UnregisterAll(recipient); WeakReferenceMessenger.Default.UnregisterAll(recipient);
WeakReferenceMessenger.Default.Reset();
} }
} }
@@ -21,6 +21,8 @@ public sealed class BaseSynchronizerUiChangeTests
[Fact] [Fact]
public void ApplyOptimisticUiChanges_UsesBundleUiChangeRequest_ForBatchBundle() public void ApplyOptimisticUiChanges_UsesBundleUiChangeRequest_ForBatchBundle()
{ {
WeakReferenceMessenger.Default.Reset();
var folderId = Guid.NewGuid(); var folderId = Guid.NewGuid();
var account = new MailAccount { Id = Guid.NewGuid(), Name = "Test account" }; var account = new MailAccount { Id = Guid.NewGuid(), Name = "Test account" };
var synchronizer = new TestSynchronizer(account); var synchronizer = new TestSynchronizer(account);
@@ -48,6 +50,7 @@ public sealed class BaseSynchronizerUiChangeTests
{ {
WeakReferenceMessenger.Default.Unregister<MailStateUpdatedMessage>(recipient); WeakReferenceMessenger.Default.Unregister<MailStateUpdatedMessage>(recipient);
WeakReferenceMessenger.Default.Unregister<BulkMailStateUpdatedMessage>(recipient); WeakReferenceMessenger.Default.Unregister<BulkMailStateUpdatedMessage>(recipient);
WeakReferenceMessenger.Default.Reset();
} }
} }
@@ -1,6 +1,7 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Collections.Generic;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
@@ -9,6 +10,7 @@ using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Navigation;
using Wino.Core.ViewModels.Data;
using Wino.Mail.ViewModels.Data; using Wino.Mail.ViewModels.Data;
using Wino.Messaging.Client.Navigation; using Wino.Messaging.Client.Navigation;
@@ -17,6 +19,7 @@ namespace Wino.Core.ViewModels;
public abstract partial class AccountManagementPageViewModelBase : CoreBaseViewModel public abstract partial class AccountManagementPageViewModelBase : CoreBaseViewModel
{ {
public ObservableCollection<IAccountProviderDetailViewModel> Accounts { get; set; } = []; public ObservableCollection<IAccountProviderDetailViewModel> Accounts { get; set; } = [];
public IEnumerable<IAccountProviderDetailViewModel> StartupAccounts => Accounts.Where(IsStartupEligible);
public bool IsPurchasePanelVisible => !HasUnlimitedAccountProduct; public bool IsPurchasePanelVisible => !HasUnlimitedAccountProduct;
public bool IsAccountCreationAlmostOnLimit => Accounts != null && Accounts.Count == FREE_ACCOUNT_COUNT - 1; public bool IsAccountCreationAlmostOnLimit => Accounts != null && Accounts.Count == FREE_ACCOUNT_COUNT - 1;
@@ -130,10 +133,21 @@ public abstract partial class AccountManagementPageViewModelBase : CoreBaseViewM
private void AccountsChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) private void AccountsChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{ {
OnPropertyChanged(nameof(HasAccountsDefined)); OnPropertyChanged(nameof(HasAccountsDefined));
OnPropertyChanged(nameof(StartupAccounts));
} }
private static string GetAccountDetailsTitle(MailAccount account) private static string GetAccountDetailsTitle(MailAccount account)
=> !string.IsNullOrWhiteSpace(account?.Address) => !string.IsNullOrWhiteSpace(account?.Address)
? string.Format(Translator.SettingsAccountDetails_NavigationTitle, account.Address) ? string.Format(Translator.SettingsAccountDetails_NavigationTitle, account.Address)
: account?.Name ?? Translator.AccountDetailsPage_Title; : account?.Name ?? Translator.AccountDetailsPage_Title;
private static bool IsStartupEligible(IAccountProviderDetailViewModel account)
{
return account switch
{
AccountProviderDetailViewModel accountViewModel => accountViewModel.Account.IsMailAccessGranted,
MergedAccountProviderDetailViewModel mergedAccountViewModel => mergedAccountViewModel.HoldingAccounts.Any(a => a.Account.IsMailAccessGranted),
_ => true
};
}
} }
@@ -1,5 +1,6 @@
using System; using System;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
@@ -9,6 +10,8 @@ public partial class AccountProviderDetailViewModel : ObservableObject, IAccount
{ {
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(CapabilitySummary))]
[NotifyPropertyChangedFor(nameof(DescriptionText))]
private MailAccount account; private MailAccount account;
public IProviderDetail ProviderDetail { get; set; } public IProviderDetail ProviderDetail { get; set; }
@@ -20,6 +23,10 @@ public partial class AccountProviderDetailViewModel : ObservableObject, IAccount
public int Order => Account.Order; public int Order => Account.Order;
public string StartupEntityAddresses => Account.Address; public string StartupEntityAddresses => Account.Address;
public string CapabilitySummary => BuildCapabilitySummary(Account);
public string DescriptionText => string.IsNullOrWhiteSpace(Account.Address)
? CapabilitySummary
: $"{CapabilitySummary} | {Account.Address}";
public int HoldingAccountCount => 1; public int HoldingAccountCount => 1;
@@ -30,4 +37,15 @@ public partial class AccountProviderDetailViewModel : ObservableObject, IAccount
ProviderDetail = providerDetail; ProviderDetail = providerDetail;
Account = account; Account = account;
} }
private static string BuildCapabilitySummary(MailAccount account)
{
if (account?.IsMailAccessGranted == true && account.IsCalendarAccessGranted)
return Translator.AccountCapability_MailAndCalendar;
if (account?.IsMailAccessGranted == true)
return Translator.AccountCapability_MailOnly;
return Translator.AccountCapability_CalendarOnly;
}
} }
@@ -17,6 +17,7 @@ using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts; using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.Folders; using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Misc; using Wino.Core.Misc;
using Wino.Core.Services; using Wino.Core.Services;
using Wino.Core.ViewModels.Data; using Wino.Core.ViewModels.Data;
@@ -96,8 +97,14 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
[ObservableProperty] [ObservableProperty]
private bool isTaskbarBadgeEnabled; private bool isTaskbarBadgeEnabled;
[ObservableProperty]
public partial AccountCapabilityOption SelectedCapabilityOption { get; set; }
public bool IsFocusedInboxSupportedForAccount => Account != null && Account.Preferences.IsFocusedInboxEnabled != null; public bool IsFocusedInboxSupportedForAccount => Account != null && Account.Preferences.IsFocusedInboxEnabled != null;
public bool IsImapServer => ServerInformation != 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 public string ProviderIconPath => Account?.SpecialImapProvider != SpecialImapProvider.None
? $"ms-appx:///Assets/Providers/{Account.SpecialImapProvider}.png" ? $"ms-appx:///Assets/Providers/{Account.SpecialImapProvider}.png"
: $"ms-appx:///Assets/Providers/{Account?.ProviderType}.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) 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, public AccountDetailsPageViewModel(IMailDialogService dialogService,
IAccountService accountService, IAccountService accountService,
@@ -262,6 +276,7 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
AccountName = Account.Name; AccountName = Account.Name;
SenderName = Account.SenderName; SenderName = Account.SenderName;
ServerInformation = Account.ServerInformation; ServerInformation = Account.ServerInformation;
SelectedCapabilityOption = ResolveCapabilityOption(Account.IsMailAccessGranted, Account.IsCalendarAccessGranted);
IsFocusedInboxEnabled = Account.Preferences.IsFocusedInboxEnabled.GetValueOrDefault(); IsFocusedInboxEnabled = Account.Preferences.IsFocusedInboxEnabled.GetValueOrDefault();
AreNotificationsEnabled = Account.Preferences.IsNotificationsEnabled; AreNotificationsEnabled = Account.Preferences.IsNotificationsEnabled;
@@ -288,7 +303,11 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
SelectedOutgoingServerConnectionSecurityIndex = AvailableConnectionSecurities.FindIndex(a => a.ImapConnectionSecurity == ServerInformation.OutgoingServerSocketOption); 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; var folderStructures = (await _folderService.GetFolderStructureForAccountAsync(Account.Id, true)).Folders;
@@ -382,11 +401,15 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
partial void OnAccountChanged(MailAccount value) partial void OnAccountChanged(MailAccount value)
{ {
SelectedCapabilityOption = ResolveCapabilityOption(value?.IsMailAccessGranted == true, value?.IsCalendarAccessGranted == true);
OnPropertyChanged(nameof(IsFocusedInboxSupportedForAccount)); OnPropertyChanged(nameof(IsFocusedInboxSupportedForAccount));
OnPropertyChanged(nameof(ProviderIconPath)); OnPropertyChanged(nameof(ProviderIconPath));
OnPropertyChanged(nameof(Address)); OnPropertyChanged(nameof(Address));
OnPropertyChanged(nameof(IsInitialSynchronizationSummaryVisible)); OnPropertyChanged(nameof(IsInitialSynchronizationSummaryVisible));
OnPropertyChanged(nameof(InitialSynchronizationSummary)); OnPropertyChanged(nameof(InitialSynchronizationSummary));
OnPropertyChanged(nameof(HasMailAccess));
OnPropertyChanged(nameof(HasCalendarAccess));
OnPropertyChanged(nameof(IsOAuthCapabilityEditable));
} }
protected override async void OnPropertyChanged(PropertyChangedEventArgs e) protected override async void OnPropertyChanged(PropertyChangedEventArgs e)
@@ -417,6 +440,21 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
Account.Preferences.IsTaskbarBadgeEnabled = IsTaskbarBadgeEnabled; Account.Preferences.IsTaskbarBadgeEnabled = IsTaskbarBadgeEnabled;
await _accountService.UpdateAccountAsync(Account); await _accountService.UpdateAccountAsync(Account);
break; 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: case nameof(SelectedPrimaryCalendar) when SelectedPrimaryCalendar != null:
foreach (var calendar in AccountCalendars) foreach (var calendar in AccountCalendars)
{ {
@@ -427,6 +465,111 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
break; 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 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 partial class AccountCalendarSettingsItemViewModel : ObservableObject
{ {
public AccountCalendar Calendar { get; } public AccountCalendar Calendar { get; }
@@ -71,6 +71,8 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
private void BuildSteps() private void BuildSteps()
{ {
Steps.Clear(); Steps.Clear();
var shouldSetupMail = WizardContext.IsMailAccessEnabled;
var shouldSetupCalendar = WizardContext.IsCalendarAccessEnabled;
if (WizardContext.IsOAuthProvider) if (WizardContext.IsOAuthProvider)
{ {
@@ -78,31 +80,47 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
{ {
Title = string.Format(Translator.AccountSetup_Step_Authenticating, WizardContext.SelectedProvider.Name) 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_SavingAccount });
if (shouldSetupMail)
{
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_FetchingProfile });
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingFolders }); Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingFolders });
if (WizardContext.SelectedProvider.Type == MailProviderType.Outlook) if (WizardContext.SelectedProvider.Type == MailProviderType.Outlook)
{ {
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingCategories }); Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingCategories });
} }
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_FetchingCalendarMetadata });
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingAliases }); 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 }); Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_Finalizing });
} }
else if (WizardContext.IsSpecialImapProvider) else if (WizardContext.IsSpecialImapProvider)
{
if (shouldSetupMail)
{ {
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_TestingMailAuth }); 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_DiscoveringCalDav });
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_TestingCalendarAuth }); 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_SavingAccount });
if (shouldSetupMail)
{
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingFolders }); 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 }); Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_FetchingCalendarMetadata });
} }
@@ -112,7 +130,10 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
else // Generic IMAP else // Generic IMAP
{ {
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SavingAccount }); Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SavingAccount });
if (shouldSetupMail)
{
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingFolders }); Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingFolders });
}
var setupResult = WizardContext.ImapCalDavSetupResult; var setupResult = WizardContext.ImapCalDavSetupResult;
if (setupResult?.IsCalendarAccessGranted == true && if (setupResult?.IsCalendarAccessGranted == true &&
@@ -186,7 +207,8 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
AccountColorHex = WizardContext.AccountColorHex, AccountColorHex = WizardContext.AccountColorHex,
CreatedAt = accountCreatedAt, CreatedAt = accountCreatedAt,
InitialSynchronizationRange = WizardContext.SelectedInitialSynchronizationRange, InitialSynchronizationRange = WizardContext.SelectedInitialSynchronizationRange,
IsCalendarAccessGranted = true IsMailAccessGranted = WizardContext.IsMailAccessEnabled,
IsCalendarAccessGranted = WizardContext.IsCalendarAccessEnabled
}; };
if (WizardContext.IsOAuthProvider) if (WizardContext.IsOAuthProvider)
@@ -208,6 +230,8 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
_dbWritten = true; _dbWritten = true;
SetCurrentStepSucceeded(); SetCurrentStepSucceeded();
if (_createdAccount.IsMailAccessGranted)
{
// Step: Profile // Step: Profile
SetStepInProgress(Translator.AccountSetup_Step_FetchingProfile); SetStepInProgress(Translator.AccountSetup_Step_FetchingProfile);
var profileResult = await SynchronizationManager.Instance.SynchronizeProfileAsync(_createdAccount.Id); var profileResult = await SynchronizationManager.Instance.SynchronizeProfileAsync(_createdAccount.Id);
@@ -247,11 +271,12 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
throw new Exception(Translator.Exception_FailedToSynchronizeCategories); throw new Exception(Translator.Exception_FailedToSynchronizeCategories);
SetCurrentStepSucceeded(); SetCurrentStepSucceeded();
} }
}
// Step: Calendar metadata // Step: Calendar metadata
SetStepInProgress(Translator.AccountSetup_Step_FetchingCalendarMetadata);
if (_createdAccount.IsCalendarAccessGranted) if (_createdAccount.IsCalendarAccessGranted)
{ {
SetStepInProgress(Translator.AccountSetup_Step_FetchingCalendarMetadata);
var calResult = await SynchronizationManager.Instance.SynchronizeCalendarAsync(new CalendarSynchronizationOptions var calResult = await SynchronizationManager.Instance.SynchronizeCalendarAsync(new CalendarSynchronizationOptions
{ {
AccountId = _createdAccount.Id, AccountId = _createdAccount.Id,
@@ -259,10 +284,12 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
}); });
if (calResult == null || calResult.CompletedState != SynchronizationCompletedState.Success) if (calResult == null || calResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeCalendarMetadata); throw new Exception(Translator.Exception_FailedToSynchronizeCalendarMetadata);
}
SetCurrentStepSucceeded(); SetCurrentStepSucceeded();
}
// Step: Aliases // Step: Aliases
if (_createdAccount.IsMailAccessGranted)
{
SetStepInProgress(Translator.AccountSetup_Step_SyncingAliases); SetStepInProgress(Translator.AccountSetup_Step_SyncingAliases);
if (_createdAccount.IsAliasSyncSupported) if (_createdAccount.IsAliasSyncSupported)
{ {
@@ -276,6 +303,7 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
} }
SetCurrentStepSucceeded(); SetCurrentStepSucceeded();
} }
}
else if (WizardContext.IsSpecialImapProvider) else if (WizardContext.IsSpecialImapProvider)
{ {
var dialogResult = WizardContext.BuildAccountCreationDialogResult(); var dialogResult = WizardContext.BuildAccountCreationDialogResult();
@@ -288,13 +316,17 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
_createdAccount.Address = WizardContext.EmailAddress; _createdAccount.Address = WizardContext.EmailAddress;
_createdAccount.SenderName = WizardContext.DisplayName; _createdAccount.SenderName = WizardContext.DisplayName;
_createdAccount.IsMailAccessGranted = dialogResult.IsMailAccessGranted;
_createdAccount.IsCalendarAccessGranted = customServerInformation.CalendarSupportMode != ImapCalendarSupportMode.Disabled; _createdAccount.IsCalendarAccessGranted = customServerInformation.CalendarSupportMode != ImapCalendarSupportMode.Disabled;
_createdAccount.ServerInformation = customServerInformation; _createdAccount.ServerInformation = customServerInformation;
if (_createdAccount.IsMailAccessGranted)
{
// Step: Test IMAP // Step: Test IMAP
SetStepInProgress(Translator.AccountSetup_Step_TestingMailAuth); SetStepInProgress(Translator.AccountSetup_Step_TestingMailAuth);
await ValidateImapConnectivityAsync(customServerInformation); await ValidateImapConnectivityAsync(customServerInformation);
SetCurrentStepSucceeded(); SetCurrentStepSucceeded();
}
// Step: CalDAV discovery and testing (if applicable) // Step: CalDAV discovery and testing (if applicable)
if (customServerInformation.CalendarSupportMode == ImapCalendarSupportMode.CalDav) if (customServerInformation.CalendarSupportMode == ImapCalendarSupportMode.CalDav)
@@ -313,12 +345,15 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
_dbWritten = true; _dbWritten = true;
SetCurrentStepSucceeded(); SetCurrentStepSucceeded();
if (_createdAccount.IsMailAccessGranted)
{
// Step: Folders // Step: Folders
SetStepInProgress(Translator.AccountSetup_Step_SyncingFolders); SetStepInProgress(Translator.AccountSetup_Step_SyncingFolders);
var folderResult = await SynchronizationManager.Instance.SynchronizeFoldersAsync(_createdAccount.Id); var folderResult = await SynchronizationManager.Instance.SynchronizeFoldersAsync(_createdAccount.Id);
if (folderResult == null || folderResult.CompletedState != SynchronizationCompletedState.Success) if (folderResult == null || folderResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeFolders); throw new Exception(Translator.Exception_FailedToSynchronizeFolders);
SetCurrentStepSucceeded(); SetCurrentStepSucceeded();
}
// Step: Calendar metadata (if not disabled) // Step: Calendar metadata (if not disabled)
if (_createdAccount.IsCalendarAccessGranted) if (_createdAccount.IsCalendarAccessGranted)
@@ -334,9 +369,11 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
SetCurrentStepSucceeded(); SetCurrentStepSucceeded();
} }
// Aliases for IMAP if (_createdAccount.IsMailAccessGranted)
{
await _accountService.CreateRootAliasAsync(_createdAccount.Id, _createdAccount.Address); await _accountService.CreateRootAliasAsync(_createdAccount.Id, _createdAccount.Address);
} }
}
else // Generic IMAP else // Generic IMAP
{ {
var setupResult = WizardContext.ImapCalDavSetupResult var setupResult = WizardContext.ImapCalDavSetupResult
@@ -350,6 +387,7 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
_createdAccount.Address = setupResult.EmailAddress; _createdAccount.Address = setupResult.EmailAddress;
_createdAccount.SenderName = setupResult.DisplayName; _createdAccount.SenderName = setupResult.DisplayName;
_createdAccount.IsMailAccessGranted = setupResult.IsMailAccessGranted;
_createdAccount.IsCalendarAccessGranted = setupResult.IsCalendarAccessGranted; _createdAccount.IsCalendarAccessGranted = setupResult.IsCalendarAccessGranted;
_createdAccount.ServerInformation = customServerInformation; _createdAccount.ServerInformation = customServerInformation;
@@ -359,12 +397,15 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
_dbWritten = true; _dbWritten = true;
SetCurrentStepSucceeded(); SetCurrentStepSucceeded();
if (_createdAccount.IsMailAccessGranted)
{
// Step: Folders // Step: Folders
SetStepInProgress(Translator.AccountSetup_Step_SyncingFolders); SetStepInProgress(Translator.AccountSetup_Step_SyncingFolders);
var folderResult = await SynchronizationManager.Instance.SynchronizeFoldersAsync(_createdAccount.Id); var folderResult = await SynchronizationManager.Instance.SynchronizeFoldersAsync(_createdAccount.Id);
if (folderResult == null || folderResult.CompletedState != SynchronizationCompletedState.Success) if (folderResult == null || folderResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeFolders); throw new Exception(Translator.Exception_FailedToSynchronizeFolders);
SetCurrentStepSucceeded(); SetCurrentStepSucceeded();
}
// Step: CalDAV (if applicable) // Step: CalDAV (if applicable)
if (setupResult.IsCalendarAccessGranted && if (setupResult.IsCalendarAccessGranted &&
@@ -392,9 +433,11 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
SetCurrentStepSucceeded(); SetCurrentStepSucceeded();
} }
// Aliases for IMAP if (_createdAccount.IsMailAccessGranted)
{
await _accountService.CreateRootAliasAsync(_createdAccount.Id, _createdAccount.Address); await _accountService.CreateRootAliasAsync(_createdAccount.Id, _createdAccount.Address);
} }
}
// Step: Finalizing // Step: Finalizing
SetStepInProgress(Translator.AccountSetup_Step_Finalizing); SetStepInProgress(Translator.AccountSetup_Step_Finalizing);
@@ -61,6 +61,7 @@ public sealed class ImapCalDavSetupResult
{ {
public string DisplayName { get; init; } public string DisplayName { get; init; }
public string EmailAddress { get; init; } public string EmailAddress { get; init; }
public bool IsMailAccessGranted { get; init; }
public bool IsCalendarAccessGranted { get; init; } public bool IsCalendarAccessGranted { get; init; }
public CustomServerInformation ServerInformation { get; init; } public CustomServerInformation ServerInformation { get; init; }
} }
@@ -21,6 +21,12 @@ public partial class WelcomeWizardContext : ObservableObject
[ObservableProperty] [ObservableProperty]
public partial InitialSynchronizationRange SelectedInitialSynchronizationRange { get; set; } = InitialSynchronizationRange.SixMonths; 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) // Special IMAP fields (iCloud/Yahoo)
[ObservableProperty] [ObservableProperty]
public partial string DisplayName { get; set; } public partial string DisplayName { get; set; }
@@ -66,7 +72,9 @@ public partial class WelcomeWizardContext : ObservableObject
AccountName, AccountName,
BuildSpecialImapProviderDetails(), BuildSpecialImapProviderDetails(),
AccountColorHex, AccountColorHex,
SelectedInitialSynchronizationRange); SelectedInitialSynchronizationRange,
IsMailAccessEnabled,
IsCalendarAccessEnabled);
} }
public void Reset() public void Reset()
@@ -75,6 +83,8 @@ public partial class WelcomeWizardContext : ObservableObject
AccountName = null; AccountName = null;
AccountColorHex = null; AccountColorHex = null;
SelectedInitialSynchronizationRange = InitialSynchronizationRange.SixMonths; SelectedInitialSynchronizationRange = InitialSynchronizationRange.SixMonths;
IsMailAccessEnabled = true;
IsCalendarAccessEnabled = true;
DisplayName = null; DisplayName = null;
EmailAddress = null; EmailAddress = null;
AppSpecificPassword = 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; using Wino.Core.ViewModels;
namespace Wino.Mail.ViewModels; namespace Wino.Mail.ViewModels;
public partial class IdlePageViewModel : CoreBaseViewModel 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] [ObservableProperty]
private string password = string.Empty; private string password = string.Empty;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsMailSettingsVisible))]
[NotifyPropertyChangedFor(nameof(IsMailPasswordInputVisible))]
[NotifyPropertyChangedFor(nameof(IsMailActionsVisible))]
private bool isMailSupportEnabled = true;
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsCalendarModeSelectionVisible))] [NotifyPropertyChangedFor(nameof(IsCalendarModeSelectionVisible))]
[NotifyPropertyChangedFor(nameof(IsCalDavSettingsVisible))] [NotifyPropertyChangedFor(nameof(IsCalDavSettingsVisible))]
@@ -141,6 +147,9 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
public bool HasProviderHint => !string.IsNullOrWhiteSpace(ProviderHint); public bool HasProviderHint => !string.IsNullOrWhiteSpace(ProviderHint);
public bool IsBasicSetupSelected => SelectedSetupTabIndex == 0; public bool IsBasicSetupSelected => SelectedSetupTabIndex == 0;
public bool IsAdvancedSetupSelected => SelectedSetupTabIndex == 1; public bool IsAdvancedSetupSelected => SelectedSetupTabIndex == 1;
public bool IsMailSettingsVisible => IsMailSupportEnabled;
public bool IsMailPasswordInputVisible => IsMailSupportEnabled;
public bool IsMailActionsVisible => IsMailSupportEnabled;
public bool IsCalendarModeSelectionVisible => IsCalendarSupportEnabled; public bool IsCalendarModeSelectionVisible => IsCalendarSupportEnabled;
public bool IsCalDavSettingsVisible => IsCalendarSupportEnabled && SelectedCalendarSupportMode == ImapCalendarSupportMode.CalDav; public bool IsCalDavSettingsVisible => IsCalendarSupportEnabled && SelectedCalendarSupportMode == ImapCalendarSupportMode.CalDav;
public bool IsLocalCalendarModeSelected => IsCalendarSupportEnabled && SelectedCalendarSupportMode == ImapCalendarSupportMode.LocalOnly; public bool IsLocalCalendarModeSelected => IsCalendarSupportEnabled && SelectedCalendarSupportMode == ImapCalendarSupportMode.LocalOnly;
@@ -152,6 +161,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
public string EmailAddressHeaderText => Translator.IMAPSetupDialog_MailAddress; public string EmailAddressHeaderText => Translator.IMAPSetupDialog_MailAddress;
public string EmailAddressPlaceholderText => Translator.IMAPSetupDialog_MailAddressPlaceholder; public string EmailAddressPlaceholderText => Translator.IMAPSetupDialog_MailAddressPlaceholder;
public string PasswordHeaderText => Translator.IMAPSetupDialog_Password; public string PasswordHeaderText => Translator.IMAPSetupDialog_Password;
public string EnableMailSupportText => Translator.ProviderSelection_UseForMail;
public string EnableCalendarSupportText => Translator.ImapCalDavSettingsPage_EnableCalendarSupport; public string EnableCalendarSupportText => Translator.ImapCalDavSettingsPage_EnableCalendarSupport;
public string AutoDiscoverButtonText => Translator.ImapCalDavSettingsPage_AutoDiscoverButton; public string AutoDiscoverButtonText => Translator.ImapCalDavSettingsPage_AutoDiscoverButton;
public string BasicTabText => Translator.ImapCalDavSettingsPage_BasicTab; public string BasicTabText => Translator.ImapCalDavSettingsPage_BasicTab;
@@ -342,6 +352,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
{ {
try try
{ {
ValidateCapabilitySelection();
await EnsureImapSettingsPreparedAsync().ConfigureAwait(false); await EnsureImapSettingsPreparedAsync().ConfigureAwait(false);
var serverInformation = BuildServerInformation(); var serverInformation = BuildServerInformation();
@@ -401,6 +412,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
{ {
try try
{ {
ValidateCapabilitySelection();
await EnsureImapSettingsPreparedAsync(); await EnsureImapSettingsPreparedAsync();
var serverInformation = BuildServerInformation(); var serverInformation = BuildServerInformation();
@@ -416,8 +428,15 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
if (!await ValidateAccountUniquenessAsync(excludedAccountId)) if (!await ValidateAccountUniquenessAsync(excludedAccountId))
return; return;
if (IsMailSupportEnabled)
{
await ValidateImapConnectivityAsync(serverInformation); await ValidateImapConnectivityAsync(serverInformation);
IsImapValidationSucceeded = true; IsImapValidationSucceeded = true;
}
else
{
IsImapValidationSucceeded = false;
}
if (serverInformation.CalendarSupportMode == ImapCalendarSupportMode.CalDav) if (serverInformation.CalendarSupportMode == ImapCalendarSupportMode.CalDav)
{ {
@@ -485,6 +504,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
{ {
DisplayName = DisplayName.Trim(), DisplayName = DisplayName.Trim(),
EmailAddress = EmailAddress.Trim(), EmailAddress = EmailAddress.Trim(),
IsMailAccessGranted = IsMailSupportEnabled,
IsCalendarAccessGranted = serverInformation.CalendarSupportMode != ImapCalendarSupportMode.Disabled, IsCalendarAccessGranted = serverInformation.CalendarSupportMode != ImapCalendarSupportMode.Disabled,
ServerInformation = serverInformation 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) partial void OnSelectedCalendarSupportModeChanged(ImapCalendarSupportMode value)
{ {
if (value == ImapCalendarSupportMode.LocalOnly && !_localOnlyInfoShown) if (value == ImapCalendarSupportMode.LocalOnly && !_localOnlyInfoShown)
@@ -562,6 +590,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
ApplyProviderHint(_editingSpecialImapProvider); ApplyProviderHint(_editingSpecialImapProvider);
ApplyServerInformation(account.ServerInformation); ApplyServerInformation(account.ServerInformation);
IsMailSupportEnabled = account.IsMailAccessGranted;
if (account.ServerInformation != null) if (account.ServerInformation != null)
{ {
@@ -588,8 +617,10 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
if (!string.IsNullOrWhiteSpace(accountCreationDialogResult?.SpecialImapProviderDetails?.SenderName)) if (!string.IsNullOrWhiteSpace(accountCreationDialogResult?.SpecialImapProviderDetails?.SenderName))
DisplayName = accountCreationDialogResult.SpecialImapProviderDetails.SenderName; DisplayName = accountCreationDialogResult.SpecialImapProviderDetails.SenderName;
IsCalendarSupportEnabled = true; IsMailSupportEnabled = accountCreationDialogResult?.IsMailAccessGranted != false;
SelectedCalendarSupportMode = ImapCalendarSupportMode.CalDav; IsCalendarSupportEnabled = accountCreationDialogResult?.IsCalendarAccessGranted == true;
SelectedCalendarSupportMode = accountCreationDialogResult?.SpecialImapProviderDetails?.CalendarSupportMode
?? (IsCalendarSupportEnabled ? ImapCalendarSupportMode.CalDav : ImapCalendarSupportMode.Disabled);
var specialProvider = accountCreationDialogResult?.SpecialImapProviderDetails?.SpecialImapProvider ?? SpecialImapProvider.None; var specialProvider = accountCreationDialogResult?.SpecialImapProviderDetails?.SpecialImapProvider ?? SpecialImapProvider.None;
_editingSpecialImapProvider = specialProvider; _editingSpecialImapProvider = specialProvider;
@@ -737,6 +768,9 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
private async Task EnsureImapSettingsPreparedAsync() private async Task EnsureImapSettingsPreparedAsync()
{ {
if (!IsMailSupportEnabled)
return;
if (HasCompleteImapSettings()) if (HasCompleteImapSettings())
return; return;
@@ -847,6 +881,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
{ {
DisplayName = DisplayName.Trim(), DisplayName = DisplayName.Trim(),
EmailAddress = EmailAddress.Trim(), EmailAddress = EmailAddress.Trim(),
IsMailAccessGranted = IsMailSupportEnabled,
IsCalendarAccessGranted = serverInformation.CalendarSupportMode != ImapCalendarSupportMode.Disabled, IsCalendarAccessGranted = serverInformation.CalendarSupportMode != ImapCalendarSupportMode.Disabled,
ServerInformation = serverInformation ServerInformation = serverInformation
}); });
@@ -863,7 +898,9 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
private async Task<bool> ValidateAccountUniquenessAsync(Guid? excludedAccountId) 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 ? _accountCreationContext?.AccountName
: null; : null;
@@ -889,6 +926,15 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
return true; 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) private async Task SaveEditFlowAsync(CustomServerInformation serverInformation)
{ {
var account = await _accountService.GetAccountAsync(_editingAccountId); var account = await _accountService.GetAccountAsync(_editingAccountId);
@@ -897,6 +943,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
account.SenderName = DisplayName.Trim(); account.SenderName = DisplayName.Trim();
account.Address = EmailAddress.Trim(); account.Address = EmailAddress.Trim();
account.IsMailAccessGranted = IsMailSupportEnabled;
account.IsCalendarAccessGranted = serverInformation.CalendarSupportMode != ImapCalendarSupportMode.Disabled; account.IsCalendarAccessGranted = serverInformation.CalendarSupportMode != ImapCalendarSupportMode.Disabled;
serverInformation.Id = account.ServerInformation?.Id ?? Guid.NewGuid(); serverInformation.Id = account.ServerInformation?.Id ?? Guid.NewGuid();
@@ -908,11 +955,14 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
await _accountService.UpdateAccountCustomServerInformationAsync(serverInformation); await _accountService.UpdateAccountCustomServerInformationAsync(serverInformation);
await _accountService.UpdateAccountAsync(account); await _accountService.UpdateAccountAsync(account);
if (account.IsMailAccessGranted)
{
Messenger.Send(new NewMailSynchronizationRequested(new MailSynchronizationOptions Messenger.Send(new NewMailSynchronizationRequested(new MailSynchronizationOptions
{ {
AccountId = account.Id, AccountId = account.Id,
Type = MailSynchronizationType.FullFolders Type = MailSynchronizationType.FullFolders
})); }));
}
if (account.IsCalendarAccessGranted) if (account.IsCalendarAccessGranted)
{ {
@@ -1007,6 +1057,9 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
private void ValidateImapSettings(CustomServerInformation serverInformation) private void ValidateImapSettings(CustomServerInformation serverInformation)
{ {
if (!IsMailSupportEnabled)
return;
ValidateIdentitySettings(); ValidateIdentitySettings();
if (string.IsNullOrWhiteSpace(serverInformation.IncomingServer)) if (string.IsNullOrWhiteSpace(serverInformation.IncomingServer))
@@ -1075,7 +1128,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
private bool TryApplyKnownProviderSettingsIfNeeded(bool requireCompleteImapSettings, bool requireCompleteCalDavSettings) private bool TryApplyKnownProviderSettingsIfNeeded(bool requireCompleteImapSettings, bool requireCompleteCalDavSettings)
{ {
var needsImapSettings = requireCompleteImapSettings && !HasCompleteImapSettings(); var needsImapSettings = IsMailSupportEnabled && requireCompleteImapSettings && !HasCompleteImapSettings();
var needsCalDavSettings = requireCompleteCalDavSettings var needsCalDavSettings = requireCompleteCalDavSettings
&& IsCalendarSupportEnabled && IsCalendarSupportEnabled
&& SelectedCalendarSupportMode == ImapCalendarSupportMode.CalDav && SelectedCalendarSupportMode == ImapCalendarSupportMode.CalDav
@@ -1114,6 +1167,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
SenderName = DisplayName.Trim(), SenderName = DisplayName.Trim(),
ProviderType = MailProviderType.IMAP4, ProviderType = MailProviderType.IMAP4,
SpecialImapProvider = _editingSpecialImapProvider, SpecialImapProvider = _editingSpecialImapProvider,
IsMailAccessGranted = IsMailSupportEnabled,
IsCalendarAccessGranted = mode != ImapCalendarSupportMode.Disabled IsCalendarAccessGranted = mode != ImapCalendarSupportMode.Disabled
}, },
new AccountCreationDialogResult( new AccountCreationDialogResult(
@@ -1121,7 +1175,9 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
DisplayName.Trim(), DisplayName.Trim(),
providerDetails, providerDetails,
string.Empty, string.Empty,
_wizardContext.SelectedInitialSynchronizationRange)); _wizardContext.SelectedInitialSynchronizationRange,
IsMailSupportEnabled,
mode != ImapCalendarSupportMode.Disabled));
if (serverInformation == null) if (serverInformation == null)
return false; return false;
@@ -1158,7 +1214,8 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
&& !string.IsNullOrWhiteSpace(CalDavPassword)); && !string.IsNullOrWhiteSpace(CalDavPassword));
private bool HasCompleteImapSettings() private bool HasCompleteImapSettings()
=> !string.IsNullOrWhiteSpace(IncomingServer) => !IsMailSupportEnabled
|| (!string.IsNullOrWhiteSpace(IncomingServer)
&& !string.IsNullOrWhiteSpace(IncomingServerPort) && !string.IsNullOrWhiteSpace(IncomingServerPort)
&& !string.IsNullOrWhiteSpace(IncomingServerUsername) && !string.IsNullOrWhiteSpace(IncomingServerUsername)
&& !string.IsNullOrWhiteSpace(IncomingServerPassword) && !string.IsNullOrWhiteSpace(IncomingServerPassword)
@@ -1167,7 +1224,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
&& !string.IsNullOrWhiteSpace(OutgoingServerUsername) && !string.IsNullOrWhiteSpace(OutgoingServerUsername)
&& !string.IsNullOrWhiteSpace(OutgoingServerPassword) && !string.IsNullOrWhiteSpace(OutgoingServerPassword)
&& IsValidPort(IncomingServerPort) && IsValidPort(IncomingServerPort)
&& IsValidPort(OutgoingServerPort); && IsValidPort(OutgoingServerPort));
private int FindAuthenticationMethodIndex(ImapAuthenticationMethod method) private int FindAuthenticationMethodIndex(ImapAuthenticationMethod method)
{ {
+86 -7
View File
@@ -46,6 +46,8 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
IRecipient<AccountRemovedMessage>, IRecipient<AccountRemovedMessage>,
IRecipient<AccountUpdatedMessage> IRecipient<AccountUpdatedMessage>
{ {
private const string MailEmptyStateNavigationParameter = IdlePageViewModel.MailEmptyStateParameter;
#region Menu Items #region Menu Items
[ObservableProperty] [ObservableProperty]
@@ -188,7 +190,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
// First clear all account menu items. // First clear all account menu items.
MenuItems.RemoveRange(MenuItems.Where(a => a is IAccountMenuItem)); 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(); List<Guid> initializedAccountIds = new();
@@ -373,7 +375,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
private async Task ForceAllAccountSynchronizationsAsync() private async Task ForceAllAccountSynchronizationsAsync()
{ {
// Run Inbox synchronization for all accounts on startup. // Run Inbox synchronization for all accounts on startup.
var accounts = await _accountService.GetAccountsAsync(); var accounts = await GetMailEnabledAccountsAsync().ConfigureAwait(false);
foreach (var account in accounts) foreach (var account in accounts)
{ {
@@ -442,6 +444,9 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
private async Task ProcessLaunchDefaultAsync() private async Task ProcessLaunchDefaultAsync()
{ {
if (await NavigateToMailEmptyStateIfNeededAsync().ConfigureAwait(false))
return;
if (PreferencesService.StartupEntityId == null) if (PreferencesService.StartupEntityId == null)
{ {
NavigateToWelcomeWizard(); NavigateToWelcomeWizard();
@@ -468,11 +473,19 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
} }
else else
{ {
// Fallback to the welcome wizard if startup entity is not found. var firstMailAccountMenuItem = MenuItems.FirstOrDefault(a => a is IAccountMenuItem) as IAccountMenuItem;
if (firstMailAccountMenuItem != null)
{
firstMailAccountMenuItem.Expand();
await ChangeLoadedAccountAsync(firstMailAccountMenuItem);
}
else
{
NavigateToWelcomeWizard(); NavigateToWelcomeWizard();
} }
} }
} }
}
public async Task NavigateFolderAsync(IBaseFolderMenuItem baseFolderMenuItem, TaskCompletionSource<bool> folderInitAwaitTask = null) public async Task NavigateFolderAsync(IBaseFolderMenuItem baseFolderMenuItem, TaskCompletionSource<bool> folderInitAwaitTask = null)
{ {
@@ -947,6 +960,11 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
operationAccount = selectedFolderMenuItem.ParentAccount; operationAccount = selectedFolderMenuItem.ParentAccount;
} }
if (operationAccount?.IsMailAccessGranted == false)
{
operationAccount = null;
}
// We couldn't find any account so far. // We couldn't find any account so far.
// If there is only 1 account to use, use it. If not, // If there is only 1 account to use, use it. If not,
// send a message for flyout so user can pick from it. // send a message for flyout so user can pick from it.
@@ -956,7 +974,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
// No selected account. // No selected account.
// List all accounts and let user pick one. // List all accounts and let user pick one.
var accounts = await _accountService.GetAccountsAsync(); var accounts = await GetMailEnabledAccountsAsync().ConfigureAwait(false);
if (!accounts.Any()) if (!accounts.Any())
{ {
@@ -1081,7 +1099,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
public async void Receive(MailtoProtocolMessageRequested message) public async void Receive(MailtoProtocolMessageRequested message)
{ {
var accounts = await _accountService.GetAccountsAsync(); var accounts = await GetMailEnabledAccountsAsync().ConfigureAwait(false);
MailAccount targetAccount = null; MailAccount targetAccount = null;
@@ -1114,7 +1132,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
if (shareRequest?.Files == null || shareRequest.Files.Count == 0) if (shareRequest?.Files == null || shareRequest.Files.Count == 0)
return; return;
var accounts = await _accountService.GetAccountsAsync(); var accounts = await GetMailEnabledAccountsAsync().ConfigureAwait(false);
if (!accounts.Any()) if (!accounts.Any())
return; return;
@@ -1188,9 +1206,12 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
else else
{ {
await ExecuteUIThread(() => SelectedMenuItem = null); await ExecuteUIThread(() => SelectedMenuItem = null);
if (!await NavigateToMailEmptyStateIfNeededAsync().ConfigureAwait(false))
{
NavigateToWelcomeWizard(); NavigateToWelcomeWizard();
} }
} }
}
private void NavigateToWelcomeWizard() private void NavigateToWelcomeWizard()
=> NavigationService.Navigate( => NavigationService.Navigate(
@@ -1199,6 +1220,37 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
NavigationReferenceFrame.ShellFrame, NavigationReferenceFrame.ShellFrame,
NavigationTransitionType.None); 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) private bool IsAccountCurrentlyLoaded(Guid accountId)
{ {
return latestSelectedAccountMenuItem?.HoldingAccounts?.Any(a => a.Id == accountId) == true; return latestSelectedAccountMenuItem?.HoldingAccounts?.Any(a => a.Id == accountId) == true;
@@ -1385,7 +1437,9 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
public async void Receive(AccountRemovedMessage message) public async void Receive(AccountRemovedMessage message)
{ {
var remainingAccounts = await _accountService.GetAccountsAsync().ConfigureAwait(false); var remainingAccounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
if (!remainingAccounts.Any()) var remainingMailAccounts = remainingAccounts.Where(account => account.IsMailAccessGranted).ToList();
if (!remainingMailAccounts.Any())
{ {
latestSelectedAccountMenuItem = null; latestSelectedAccountMenuItem = null;
await ExecuteUIThread(() => await ExecuteUIThread(() =>
@@ -1394,6 +1448,12 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
MenuItems?.Clear(); MenuItems?.Clear();
MenuItems?.Add(CreateMailMenuItem); MenuItems?.Add(CreateMailMenuItem);
}); });
if (remainingAccounts.Any())
{
await NavigateToMailEmptyStateIfNeededAsync().ConfigureAwait(false);
}
return; return;
} }
@@ -1413,14 +1473,25 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
await RecreateMenuItemsAsync(); await RecreateMenuItemsAsync();
if (!createdAccount.IsMailAccessGranted)
{
await NavigateToMailEmptyStateIfNeededAsync().ConfigureAwait(false);
}
if (!MenuItems.TryGetAccountMenuItem(createdAccount.Id, out IAccountMenuItem createdMenuItem)) if (!MenuItems.TryGetAccountMenuItem(createdAccount.Id, out IAccountMenuItem createdMenuItem))
{ {
Log.Warning("Created account {AccountId} could not be found in menu items after refresh.", createdAccount.Id); Log.Warning("Created account {AccountId} could not be found in menu items after refresh.", createdAccount.Id);
if (!createdAccount.IsMailAccessGranted)
return;
return; return;
} }
await ChangeLoadedAccountAsync(createdMenuItem); await ChangeLoadedAccountAsync(createdMenuItem);
if (createdAccount.IsMailAccessGranted)
{
// Each created account should start a new synchronization automatically. // Each created account should start a new synchronization automatically.
var options = new MailSynchronizationOptions() var options = new MailSynchronizationOptions()
{ {
@@ -1429,6 +1500,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
}; };
Messenger.Send(new NewMailSynchronizationRequested(options)); Messenger.Send(new NewMailSynchronizationRequested(options));
}
if (createdAccount.IsCalendarAccessGranted) if (createdAccount.IsCalendarAccessGranted)
{ {
@@ -1455,6 +1527,13 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
{ {
var updatedAccount = message.Account; var updatedAccount = message.Account;
if (!updatedAccount.IsMailAccessGranted || !MenuItems.TryGetAccountMenuItem(updatedAccount.Id, out _))
{
await RecreateMenuItemsAsync();
await RestoreSelectedAccountAfterMenuRefreshAsync(false);
return;
}
await ExecuteUIThread(() => await ExecuteUIThread(() =>
{ {
if (MenuItems.TryGetAccountMenuItem(updatedAccount.Id, out IAccountMenuItem foundAccountMenuItem)) 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.Entities.Shared;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Requests.Category; using Wino.Core.Requests.Category;
using Wino.Core.Services; using Wino.Core.Services;
@@ -53,11 +51,11 @@ public partial class MailCategoryManagementPageViewModel : MailBaseViewModel
if (parameters is not Guid accountId) if (parameters is not Guid accountId)
return; return;
Account = await _accountService.GetAccountAsync(accountId).ConfigureAwait(false); Account = await _accountService.GetAccountAsync(accountId);
if (Account != null) if (Account != null)
{ {
await LoadCategoriesAsync().ConfigureAwait(false); await LoadCategoriesAsync();
} }
} }
@@ -15,6 +15,13 @@ using Wino.Messaging.Client.Navigation;
namespace Wino.Mail.ViewModels; namespace Wino.Mail.ViewModels;
public enum ProviderSelectionWizardStep
{
Provider = 0,
Identity = 1,
Capabilities = 2
}
public partial class ProviderSelectionPageViewModel : MailBaseViewModel public partial class ProviderSelectionPageViewModel : MailBaseViewModel
{ {
private readonly IAccountService _accountService; private readonly IAccountService _accountService;
@@ -45,16 +52,50 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsInitialSynchronizationWarningVisible))] [NotifyPropertyChangedFor(nameof(IsInitialSynchronizationWarningVisible))]
[NotifyPropertyChangedFor(nameof(IsMailSynchronizationRangeVisible))]
public partial InitialSynchronizationRangeOption SelectedInitialSynchronizationRange { get; set; } public partial InitialSynchronizationRangeOption SelectedInitialSynchronizationRange { get; set; }
[ObservableProperty] [ObservableProperty]
public partial string AccountName { get; set; } public partial string AccountName { get; set; }
[ObservableProperty] [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 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( public ProviderSelectionPageViewModel(
IAccountService accountService, IAccountService accountService,
@@ -101,35 +142,83 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
p.Type == WizardContext.SelectedProvider.Type && p.Type == WizardContext.SelectedProvider.Type &&
p.SpecialImapProvider == WizardContext.SelectedProvider.SpecialImapProvider); p.SpecialImapProvider == WizardContext.SelectedProvider.SpecialImapProvider);
AccountName = WizardContext.AccountName; AccountName = WizardContext.AccountName;
IsMailAccessEnabled = WizardContext.IsMailAccessEnabled;
IsCalendarAccessEnabled = WizardContext.IsCalendarAccessEnabled;
if (WizardContext.AccountColorHex != null) if (WizardContext.AccountColorHex != null)
SelectedColor = AvailableColors.FirstOrDefault(c => c.Hex == WizardContext.AccountColorHex); 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) 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] [RelayCommand]
private void ClearColor() => SelectedColor = null; 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] [RelayCommand(CanExecute = nameof(CanGoBack))]
private async Task ProceedAsync() private void GoBack()
{ {
if (!CanProceed) return; if (!CanGoBack)
return;
if (await _accountService.AccountNameExistsAsync(AccountName)) 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( await _dialogService.ShowMessageAsync(
Translator.DialogMessage_AccountNameExistsMessage, Translator.DialogMessage_AccountNameExistsMessage,
@@ -138,11 +227,25 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
return; return;
} }
// Persist to wizard context CurrentStep = ProviderSelectionWizardStep.Capabilities;
return;
case ProviderSelectionWizardStep.Capabilities:
await CompleteWizardAsync();
return;
}
}
private async Task CompleteWizardAsync()
{
if (!CanContinue())
return;
WizardContext.SelectedProvider = SelectedProvider; WizardContext.SelectedProvider = SelectedProvider;
WizardContext.AccountName = AccountName?.Trim(); WizardContext.AccountName = AccountName?.Trim();
WizardContext.AccountColorHex = SelectedColor?.Hex ?? string.Empty; WizardContext.AccountColorHex = SelectedColor?.Hex ?? string.Empty;
WizardContext.SelectedInitialSynchronizationRange = SelectedInitialSynchronizationRange?.Range ?? InitialSynchronizationRange.SixMonths; WizardContext.SelectedInitialSynchronizationRange = SelectedInitialSynchronizationRange?.Range ?? InitialSynchronizationRange.SixMonths;
WizardContext.IsMailAccessEnabled = IsMailAccessEnabled;
WizardContext.IsCalendarAccessEnabled = IsCalendarAccessEnabled;
if (WizardContext.IsGenericImap) if (WizardContext.IsGenericImap)
{ {
@@ -172,4 +275,23 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
WinoPage.AccountSetupProgressPage)); 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; } public partial string AppSpecificPassword { get; set; }
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(RequiresAppSpecificPassword))]
public partial int SelectedCalendarModeIndex { get; set; } public partial int SelectedCalendarModeIndex { get; set; }
[ObservableProperty] [ObservableProperty]
public partial bool CanProceed { get; set; } public partial bool CanProceed { get; set; }
public bool IsCalendarModeSelectionVisible => WizardContext.IsCalendarAccessEnabled;
public bool RequiresAppSpecificPassword => WizardContext.IsMailAccessEnabled || SelectedCalendarModeIndex == 1;
public string AppPasswordHelpUrl public string AppPasswordHelpUrl
{ {
get get
@@ -86,8 +90,15 @@ public partial class SpecialImapCredentialsPageViewModel : MailBaseViewModel
_ => 0 _ => 0
}; };
if (!WizardContext.IsCalendarAccessEnabled)
{
SelectedCalendarModeIndex = 0;
}
OnPropertyChanged(nameof(AppPasswordHelpUrl)); OnPropertyChanged(nameof(AppPasswordHelpUrl));
OnPropertyChanged(nameof(CalendarModeCalDavDescription)); OnPropertyChanged(nameof(CalendarModeCalDavDescription));
OnPropertyChanged(nameof(IsCalendarModeSelectionVisible));
OnPropertyChanged(nameof(RequiresAppSpecificPassword));
Validate(); Validate();
} }
@@ -95,13 +106,18 @@ public partial class SpecialImapCredentialsPageViewModel : MailBaseViewModel
partial void OnDisplayNameChanged(string value) => Validate(); partial void OnDisplayNameChanged(string value) => Validate();
partial void OnEmailAddressChanged(string value) => Validate(); partial void OnEmailAddressChanged(string value) => Validate();
partial void OnAppSpecificPasswordChanged(string value) => Validate(); partial void OnAppSpecificPasswordChanged(string value) => Validate();
partial void OnSelectedCalendarModeIndexChanged(int value)
{
OnPropertyChanged(nameof(RequiresAppSpecificPassword));
Validate();
}
private void Validate() private void Validate()
{ {
CanProceed = !string.IsNullOrWhiteSpace(DisplayName) CanProceed = !string.IsNullOrWhiteSpace(DisplayName)
&& !string.IsNullOrWhiteSpace(EmailAddress) && !string.IsNullOrWhiteSpace(EmailAddress)
&& MailAccountAddressValidator.IsValid(EmailAddress) && MailAccountAddressValidator.IsValid(EmailAddress)
&& !string.IsNullOrWhiteSpace(AppSpecificPassword); && (!RequiresAppSpecificPassword || !string.IsNullOrWhiteSpace(AppSpecificPassword));
} }
[RelayCommand] [RelayCommand]
@@ -121,12 +137,14 @@ public partial class SpecialImapCredentialsPageViewModel : MailBaseViewModel
WizardContext.DisplayName = DisplayName?.Trim(); WizardContext.DisplayName = DisplayName?.Trim();
WizardContext.EmailAddress = EmailAddress?.Trim(); WizardContext.EmailAddress = EmailAddress?.Trim();
WizardContext.AppSpecificPassword = AppSpecificPassword?.Trim(); WizardContext.AppSpecificPassword = AppSpecificPassword?.Trim();
WizardContext.CalendarSupportMode = SelectedCalendarModeIndex switch WizardContext.CalendarSupportMode = WizardContext.IsCalendarAccessEnabled
? SelectedCalendarModeIndex switch
{ {
1 => ImapCalendarSupportMode.CalDav, 1 => ImapCalendarSupportMode.CalDav,
2 => ImapCalendarSupportMode.LocalOnly, 2 => ImapCalendarSupportMode.LocalOnly,
_ => ImapCalendarSupportMode.Disabled _ => ImapCalendarSupportMode.Disabled
}; }
: ImapCalendarSupportMode.Disabled;
Messenger.Send(new BreadcrumbNavigationRequested( Messenger.Send(new BreadcrumbNavigationRequested(
Translator.WelcomeWizard_Step3Title, Translator.WelcomeWizard_Step3Title,
@@ -158,7 +158,9 @@ public sealed partial class NewAccountDialog : ContentDialog
AccountNameTextbox.Text.Trim(), AccountNameTextbox.Text.Trim(),
details, details,
SelectedColor?.Hex ?? string.Empty, SelectedColor?.Hex ?? string.Empty,
initialSynchronizationRange); initialSynchronizationRange,
true,
calendarSupportMode != ImapCalendarSupportMode.Disabled);
Hide(); Hide();
return; return;
@@ -185,7 +187,9 @@ public sealed partial class NewAccountDialog : ContentDialog
AccountNameTextbox.Text.Trim(), AccountNameTextbox.Text.Trim(),
null, null,
SelectedColor?.Hex ?? string.Empty, SelectedColor?.Hex ?? string.Empty,
initialSynchronizationRange); initialSynchronizationRange,
true,
true);
Hide(); Hide();
} }
} }
+20
View File
@@ -72,6 +72,26 @@ public static class XamlHelpers
return null; return null;
} }
} }
public static Microsoft.UI.Xaml.Media.Imaging.BitmapImage? StringToBitmapImage(string? imagePath)
{
if (string.IsNullOrWhiteSpace(imagePath))
return null;
try
{
var uri = imagePath.StartsWith("/")
? new Uri($"ms-appx://{imagePath}")
: new Uri(imagePath, UriKind.Absolute);
return new Microsoft.UI.Xaml.Media.Imaging.BitmapImage(uri);
}
catch
{
return null;
}
}
public static InfoBarSeverity InfoBarSeverityConverter(InfoBarMessageType messageType) public static InfoBarSeverity InfoBarSeverityConverter(InfoBarMessageType messageType)
{ {
return messageType switch return messageType switch
@@ -105,6 +105,9 @@ public partial class AccountCalendarStateService : ObservableRecipient,
{ {
lock (_calendarStateLock) lock (_calendarStateLock)
{ {
if (!GroupedAccountCalendarViewModel.SupportsCalendar(groupedAccountCalendar.Account))
return;
groupedAccountCalendar.CalendarSelectionStateChanged += SingleCalendarSelectionStateChanged; groupedAccountCalendar.CalendarSelectionStateChanged += SingleCalendarSelectionStateChanged;
groupedAccountCalendar.CollectiveSelectionStateChanged += SingleGroupCalendarCollectiveStateChanged; groupedAccountCalendar.CollectiveSelectionStateChanged += SingleGroupCalendarCollectiveStateChanged;
try try
@@ -180,6 +183,9 @@ public partial class AccountCalendarStateService : ObservableRecipient,
{ {
lock (_calendarStateLock) lock (_calendarStateLock)
{ {
if (!GroupedAccountCalendarViewModel.SupportsCalendar(accountCalendar.Account))
return;
// Find the group that this calendar belongs to. // Find the group that this calendar belongs to.
var group = _internalGroupedAccountCalendars.FirstOrDefault(g => g.Account.Id == accountCalendar.Account.Id); var group = _internalGroupedAccountCalendars.FirstOrDefault(g => g.Account.Id == accountCalendar.Account.Id);
@@ -396,6 +402,16 @@ public partial class AccountCalendarStateService : ObservableRecipient,
lock (_calendarStateLock) lock (_calendarStateLock)
{ {
groupedAccount = _internalGroupedAccountCalendars.FirstOrDefault(a => a.Account.Id == updatedAccount.Id); groupedAccount = _internalGroupedAccountCalendars.FirstOrDefault(a => a.Account.Id == updatedAccount.Id);
if (!GroupedAccountCalendarViewModel.SupportsCalendar(updatedAccount))
{
if (groupedAccount != null)
{
RemoveGroupedAccountCalendar(groupedAccount);
}
return;
}
} }
groupedAccount?.UpdateAccount(updatedAccount); groupedAccount?.UpdateAccount(updatedAccount);
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
namespace Wino.Services; namespace Wino.Services;
@@ -6,35 +7,73 @@ public class MailAuthenticatorConfiguration : IAuthenticatorConfig
{ {
public string OutlookAuthenticatorClientId => "b19c2035-d740-49ff-b297-de6ec561b208"; public string OutlookAuthenticatorClientId => "b19c2035-d740-49ff-b297-de6ec561b208";
public string[] OutlookScope => public string GmailAuthenticatorClientId => "973025879644-s7b4ur9p3rlgop6a22u7iuptdc0brnrn.apps.googleusercontent.com";
[
public string GmailTokenStoreIdentifier => "WinoMailGmailTokenStore";
public string[] GetOutlookScope(bool isMailAccessGranted, bool isCalendarAccessGranted)
{
var scopes = new List<string>
{
"email", "email",
"mail.readwrite",
"offline_access", "offline_access",
"User.Read"
};
if (isMailAccessGranted)
{
scopes.AddRange(
[
"mail.readwrite",
"mail.send", "mail.send",
"Mail.Send.Shared", "Mail.Send.Shared",
"Mail.ReadWrite.Shared", "Mail.ReadWrite.Shared"
"User.Read", ]);
}
if (isCalendarAccessGranted)
{
scopes.AddRange(
[
"Calendars.ReadBasic", "Calendars.ReadBasic",
"Calendars.ReadWrite", "Calendars.ReadWrite",
"Calendars.ReadWrite.Shared", "Calendars.ReadWrite.Shared",
"Calendars.Read", "Calendars.Read",
"Calendars.Read.Shared", "Calendars.Read.Shared"
]; ]);
}
public string GmailAuthenticatorClientId => "973025879644-s7b4ur9p3rlgop6a22u7iuptdc0brnrn.apps.googleusercontent.com"; return [.. scopes];
}
public string[] GmailScope => public string[] GetGmailScope(bool isMailAccessGranted, bool isCalendarAccessGranted)
{
var scopes = new List<string>
{
"https://www.googleapis.com/auth/userinfo.profile",
"https://www.googleapis.com/auth/userinfo.email"
};
if (isMailAccessGranted)
{
scopes.AddRange(
[ [
"https://mail.google.com/", "https://mail.google.com/",
"https://www.googleapis.com/auth/userinfo.profile", "https://www.googleapis.com/auth/gmail.labels"
"https://www.googleapis.com/auth/gmail.labels", ]);
"https://www.googleapis.com/auth/userinfo.email", }
if (isCalendarAccessGranted)
{
scopes.AddRange(
[
"https://www.googleapis.com/auth/calendar", "https://www.googleapis.com/auth/calendar",
"https://www.googleapis.com/auth/calendar.events", "https://www.googleapis.com/auth/calendar.events",
"https://www.googleapis.com/auth/calendar.settings.readonly", "https://www.googleapis.com/auth/calendar.settings.readonly",
"https://www.googleapis.com/auth/drive.file", "https://www.googleapis.com/auth/drive.file"
]; ]);
}
public string GmailTokenStoreIdentifier => "WinoMailGmailTokenStore";
return [.. scopes];
}
} }
@@ -1,5 +1,4 @@
using Wino.Mail.ViewModels; using Wino.Mail.ViewModels;
using Wino.Mail.WinUI;
namespace Wino.Mail.WinUI.Views.Abstract; namespace Wino.Mail.WinUI.Views.Abstract;
@@ -90,12 +90,12 @@
<SymbolIcon Symbol="ContactInfo" /> <SymbolIcon Symbol="ContactInfo" />
</controls:SegmentedItem.Icon> </controls:SegmentedItem.Icon>
</controls:SegmentedItem> </controls:SegmentedItem>
<controls:SegmentedItem Content="{x:Bind domain:Translator.AccountDetailsPage_TabMail, Mode=OneTime}"> <controls:SegmentedItem Content="{x:Bind domain:Translator.AccountDetailsPage_TabMail, Mode=OneTime}" Visibility="{x:Bind ViewModel.HasMailAccess, Mode=OneWay}">
<controls:SegmentedItem.Icon> <controls:SegmentedItem.Icon>
<SymbolIcon Symbol="Mail" /> <SymbolIcon Symbol="Mail" />
</controls:SegmentedItem.Icon> </controls:SegmentedItem.Icon>
</controls:SegmentedItem> </controls:SegmentedItem>
<controls:SegmentedItem Content="{x:Bind domain:Translator.AccountDetailsPage_TabCalendar, Mode=OneTime}"> <controls:SegmentedItem Content="{x:Bind domain:Translator.AccountDetailsPage_TabCalendar, Mode=OneTime}" Visibility="{x:Bind ViewModel.HasCalendarAccess, Mode=OneWay}">
<controls:SegmentedItem.Icon> <controls:SegmentedItem.Icon>
<FontIcon Glyph="&#xE163;" /> <FontIcon Glyph="&#xE163;" />
</controls:SegmentedItem.Icon> </controls:SegmentedItem.Icon>
@@ -117,7 +117,10 @@
Text="{x:Bind ViewModel.AccountName, Mode=TwoWay}" /> Text="{x:Bind ViewModel.AccountName, Mode=TwoWay}" />
</controls:SettingsCard> </controls:SettingsCard>
<controls:SettingsCard Description="{x:Bind domain:Translator.SettingsEditAccountDetails_Description}" Header="{x:Bind domain:Translator.AccountSettingsDialog_AccountName}"> <controls:SettingsCard
Description="{x:Bind domain:Translator.SettingsEditAccountDetails_Description}"
Header="{x:Bind domain:Translator.AccountSettingsDialog_AccountName}"
Visibility="{x:Bind ViewModel.HasMailAccess, Mode=OneWay}">
<controls:SettingsCard.HeaderIcon> <controls:SettingsCard.HeaderIcon>
<SymbolIcon Symbol="Mail" /> <SymbolIcon Symbol="Mail" />
</controls:SettingsCard.HeaderIcon> </controls:SettingsCard.HeaderIcon>
@@ -127,7 +130,9 @@
Text="{x:Bind ViewModel.SenderName, Mode=TwoWay}" /> Text="{x:Bind ViewModel.SenderName, Mode=TwoWay}" />
</controls:SettingsCard> </controls:SettingsCard>
<controls:SettingsCard Header="{x:Bind domain:Translator.IMAPSetupDialog_MailAddress}"> <controls:SettingsCard
Header="{x:Bind domain:Translator.IMAPSetupDialog_MailAddress}"
Visibility="{x:Bind ViewModel.HasMailAccess, Mode=OneWay}">
<controls:SettingsCard.HeaderIcon> <controls:SettingsCard.HeaderIcon>
<SymbolIcon Symbol="Mail" /> <SymbolIcon Symbol="Mail" />
</controls:SettingsCard.HeaderIcon> </controls:SettingsCard.HeaderIcon>
@@ -137,6 +142,25 @@
Text="{x:Bind ViewModel.Address, Mode=OneWay}" /> Text="{x:Bind ViewModel.Address, Mode=OneWay}" />
</controls:SettingsCard> </controls:SettingsCard>
<controls:SettingsCard
Description="{x:Bind domain:Translator.AccountDetailsPage_CapabilityDescription, Mode=OneTime}"
Header="{x:Bind domain:Translator.AccountDetailsPage_CapabilityTitle, Mode=OneTime}"
Visibility="{x:Bind ViewModel.IsOAuthCapabilityEditable, Mode=OneWay}">
<controls:SettingsCard.HeaderIcon>
<FontIcon Glyph="&#xE787;" />
</controls:SettingsCard.HeaderIcon>
<ComboBox
MinWidth="220"
ItemsSource="{x:Bind ViewModel.CapabilityOptions, Mode=OneWay}"
SelectedItem="{x:Bind ViewModel.SelectedCapabilityOption, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="mailViewModels:AccountCapabilityOption">
<TextBlock Text="{x:Bind DisplayText}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</controls:SettingsCard>
<controls:SettingsCard <controls:SettingsCard
HorizontalContentAlignment="Stretch" HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch" VerticalContentAlignment="Stretch"
@@ -212,6 +236,7 @@
<Grid <Grid
x:Name="MailSettingsPanel" x:Name="MailSettingsPanel"
Grid.Row="2" Grid.Row="2"
x:Load="{x:Bind ViewModel.HasMailAccess, Mode=OneWay}"
Visibility="Collapsed"> Visibility="Collapsed">
<StackPanel Spacing="4"> <StackPanel Spacing="4">
<StackPanel.ChildrenTransitions> <StackPanel.ChildrenTransitions>
@@ -403,6 +428,7 @@
<Grid <Grid
x:Name="CalendarSettingsPanel" x:Name="CalendarSettingsPanel"
Grid.Row="2" Grid.Row="2"
x:Load="{x:Bind ViewModel.HasCalendarAccess, Mode=OneWay}"
Visibility="Collapsed"> Visibility="Collapsed">
<StackPanel MaxWidth="900" Spacing="12"> <StackPanel MaxWidth="900" Spacing="12">
@@ -25,7 +25,7 @@
Margin="0,2,0,0" Margin="0,2,0,0"
Click="RootAccountTemplate_Click" Click="RootAccountTemplate_Click"
CommandParameter="{x:Bind}" CommandParameter="{x:Bind}"
Description="{x:Bind Account.Address}" Description="{x:Bind DescriptionText}"
Header="{x:Bind Account.Name}" Header="{x:Bind Account.Name}"
IsClickEnabled="True"> IsClickEnabled="True">
<winuiControls:SettingsCard.HeaderIcon> <winuiControls:SettingsCard.HeaderIcon>
@@ -199,6 +199,7 @@
x:Name="AccountsListView" x:Name="AccountsListView"
Grid.Row="1" Grid.Row="1"
x:Load="{x:Bind ViewModel.HasAccountsDefined, Mode=OneWay}" x:Load="{x:Bind ViewModel.HasAccountsDefined, Mode=OneWay}"
CanReorderItems="True"
ItemContainerStyle="{StaticResource StretchedItemContainerStyle}" ItemContainerStyle="{StaticResource StretchedItemContainerStyle}"
ItemTemplateSelector="{StaticResource AccountProviderViewModelTemplateSelector}" ItemTemplateSelector="{StaticResource AccountProviderViewModelTemplateSelector}"
ItemsSource="{x:Bind ViewModel.Accounts, Mode=OneWay}" ItemsSource="{x:Bind ViewModel.Accounts, Mode=OneWay}"
@@ -208,7 +209,7 @@
<winuiControls:SettingsCard Description="{x:Bind domain:Translator.SettingsStartupItem_Description}" Header="{x:Bind domain:Translator.SettingsStartupItem_Title}"> <winuiControls:SettingsCard Description="{x:Bind domain:Translator.SettingsStartupItem_Description}" Header="{x:Bind domain:Translator.SettingsStartupItem_Title}">
<ComboBox <ComboBox
MinWidth="150" MinWidth="150"
ItemsSource="{x:Bind ViewModel.Accounts, Mode=OneTime}" ItemsSource="{x:Bind ViewModel.StartupAccounts, Mode=OneWay}"
SelectedItem="{x:Bind ViewModel.StartupAccount, Mode=TwoWay}"> SelectedItem="{x:Bind ViewModel.StartupAccount, Mode=TwoWay}">
<ComboBox.ItemTemplate> <ComboBox.ItemTemplate>
<DataTemplate x:DataType="interfaces:IAccountProviderDetailViewModel"> <DataTemplate x:DataType="interfaces:IAccountProviderDetailViewModel">
@@ -220,16 +221,10 @@
<SymbolIcon Symbol="Account" /> <SymbolIcon Symbol="Account" />
</winuiControls:SettingsCard.HeaderIcon> </winuiControls:SettingsCard.HeaderIcon>
</winuiControls:SettingsCard> </winuiControls:SettingsCard>
<winuiControls:SettingsCard <winuiControls:SettingsCard Description="{x:Bind domain:Translator.WinoAccount_Management_LocalDataSectionDescription}" Header="{x:Bind domain:Translator.WinoAccount_Management_LocalDataSectionTitle}">
Description="{x:Bind domain:Translator.WinoAccount_Management_LocalDataSectionDescription}"
Header="{x:Bind domain:Translator.WinoAccount_Management_LocalDataSectionTitle}">
<StackPanel Orientation="Horizontal" Spacing="12"> <StackPanel Orientation="Horizontal" Spacing="12">
<Button <Button Command="{x:Bind ViewModel.ImportLocalDataCommand}" Content="{x:Bind domain:Translator.WinoAccount_Management_LocalDataImportAction}" />
Command="{x:Bind ViewModel.ImportLocalDataCommand}" <Button Command="{x:Bind ViewModel.ExportLocalDataCommand}" Content="{x:Bind domain:Translator.WinoAccount_Management_LocalDataExportAction}" />
Content="{x:Bind domain:Translator.WinoAccount_Management_LocalDataImportAction}" />
<Button
Command="{x:Bind ViewModel.ExportLocalDataCommand}"
Content="{x:Bind domain:Translator.WinoAccount_Management_LocalDataExportAction}" />
</StackPanel> </StackPanel>
<winuiControls:SettingsCard.HeaderIcon> <winuiControls:SettingsCard.HeaderIcon>
<SymbolIcon Symbol="Sync" /> <SymbolIcon Symbol="Sync" />
@@ -76,12 +76,18 @@
Grid.Row="1" Grid.Row="1"
Grid.ColumnSpan="2" Grid.ColumnSpan="2"
Header="{x:Bind ViewModel.PasswordHeaderText, Mode=OneWay}" Header="{x:Bind ViewModel.PasswordHeaderText, Mode=OneWay}"
Password="{x:Bind ViewModel.Password, Mode=TwoWay}" /> Password="{x:Bind ViewModel.Password, Mode=TwoWay}"
Visibility="{x:Bind ViewModel.IsMailPasswordInputVisible, Mode=OneWay}" />
</Grid> </Grid>
<StackPanel Spacing="10">
<CheckBox
Content="{x:Bind ViewModel.EnableMailSupportText, Mode=OneWay}"
IsChecked="{x:Bind ViewModel.IsMailSupportEnabled, Mode=TwoWay}" />
<CheckBox <CheckBox
Content="{x:Bind ViewModel.EnableCalendarSupportText, Mode=OneWay}" Content="{x:Bind ViewModel.EnableCalendarSupportText, Mode=OneWay}"
IsChecked="{x:Bind ViewModel.IsCalendarSupportEnabled, Mode=TwoWay}" /> IsChecked="{x:Bind ViewModel.IsCalendarSupportEnabled, Mode=TwoWay}" />
</StackPanel>
<StackPanel Spacing="8"> <StackPanel Spacing="8">
<TextBlock <TextBlock
@@ -93,7 +99,8 @@
HorizontalAlignment="Left" HorizontalAlignment="Left"
Command="{x:Bind ViewModel.AutoDiscoverSettingsCommand}" Command="{x:Bind ViewModel.AutoDiscoverSettingsCommand}"
Content="{x:Bind ViewModel.AutoDiscoverButtonText, Mode=OneWay}" Content="{x:Bind ViewModel.AutoDiscoverButtonText, Mode=OneWay}"
Style="{ThemeResource AccentButtonStyle}" /> Style="{ThemeResource AccentButtonStyle}"
Visibility="{x:Bind ViewModel.IsMailActionsVisible, Mode=OneWay}" />
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</Border> </Border>
@@ -103,7 +110,8 @@
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}" BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1" BorderThickness="1"
CornerRadius="8"> CornerRadius="8"
Visibility="{x:Bind ViewModel.IsMailSettingsVisible, Mode=OneWay}">
<StackPanel Spacing="16"> <StackPanel Spacing="16">
<StackPanel Spacing="2"> <StackPanel Spacing="2">
<TextBlock <TextBlock
@@ -176,7 +184,8 @@
<Button <Button
HorizontalAlignment="Right" HorizontalAlignment="Right"
Command="{x:Bind ViewModel.TestImapConnectionCommand}" Command="{x:Bind ViewModel.TestImapConnectionCommand}"
Content="{x:Bind ViewModel.TestImapButtonText, Mode=OneWay}" /> Content="{x:Bind ViewModel.TestImapButtonText, Mode=OneWay}"
Visibility="{x:Bind ViewModel.IsMailActionsVisible, Mode=OneWay}" />
</StackPanel> </StackPanel>
</Border> </Border>
+34 -2
View File
@@ -9,7 +9,39 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"> mc:Ignorable="d">
<!-- Empty page for disposing composer or renderer page. --> <Grid>
<Grid /> <StackPanel
MaxWidth="440"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="12"
Visibility="{x:Bind ViewModel.IsMailEmptyStateVisible, Mode=OneWay}">
<TextBlock
HorizontalAlignment="Center"
Style="{StaticResource TitleTextBlockStyle}"
Text="{x:Bind ViewModel.MailEmptyStateTitle, Mode=OneWay}"
TextAlignment="Center"
TextWrapping="WrapWholeWords" />
<TextBlock
HorizontalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource BodyTextBlockStyle}"
Text="{x:Bind ViewModel.MailEmptyStateMessage, Mode=OneWay}"
TextAlignment="Center"
TextWrapping="WrapWholeWords" />
<StackPanel
HorizontalAlignment="Center"
Orientation="Horizontal"
Spacing="12">
<Button
Command="{x:Bind ViewModel.AddAccountCommand}"
Content="{x:Bind ViewModel.AddAccountText, Mode=OneWay}"
Style="{ThemeResource AccentButtonStyle}" />
<Button
Command="{x:Bind ViewModel.ManageAccountsCommand}"
Content="{x:Bind ViewModel.ManageAccountsText, Mode=OneWay}" />
</StackPanel>
</StackPanel>
</Grid>
</abstract:IdlePageAbstract> </abstract:IdlePageAbstract>
File diff suppressed because one or more lines are too long
@@ -12,6 +12,16 @@ public sealed partial class ProviderSelectionPage : ProviderSelectionPageAbstrac
private void ProviderSelectionChanged(ItemsView sender, ItemsViewSelectionChangedEventArgs args) private void ProviderSelectionChanged(ItemsView sender, ItemsViewSelectionChangedEventArgs args)
{ {
if (sender.SelectedItem == null) return;
ViewModel.SelectedProvider = sender.SelectedItem as Wino.Core.Domain.Interfaces.IProviderDetail; ViewModel.SelectedProvider = sender.SelectedItem as Wino.Core.Domain.Interfaces.IProviderDetail;
} }
private void AccountColorGridView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count > 0)
{
AccountColorFlyout.Hide();
}
}
} }
@@ -50,24 +50,33 @@
<PasswordBox <PasswordBox
x:Name="AppPasswordBox" x:Name="AppPasswordBox"
Header="{x:Bind domain:Translator.ProviderSelection_AppPasswordHeader}" Header="{x:Bind domain:Translator.ProviderSelection_AppPasswordHeader}"
PasswordChanged="AppPasswordChanged" /> PasswordChanged="AppPasswordChanged"
Visibility="{x:Bind ViewModel.RequiresAppSpecificPassword, Mode=OneWay}" />
<HyperlinkButton <HyperlinkButton
HorizontalAlignment="Right" HorizontalAlignment="Right"
Command="{x:Bind ViewModel.OpenAppPasswordHelpCommand}" Command="{x:Bind ViewModel.OpenAppPasswordHelpCommand}"
Content="{x:Bind domain:Translator.ProviderSelection_AppPasswordHelp}" /> Content="{x:Bind domain:Translator.ProviderSelection_AppPasswordHelp}"
Visibility="{x:Bind ViewModel.RequiresAppSpecificPassword, Mode=OneWay}" />
<!-- Divider --> <!-- Divider -->
<Rectangle Height="1" Fill="{ThemeResource CardStrokeColorDefaultBrush}" /> <Rectangle
Height="1"
Fill="{ThemeResource CardStrokeColorDefaultBrush}"
Visibility="{x:Bind ViewModel.IsCalendarModeSelectionVisible, Mode=OneWay}" />
<!-- Calendar Mode --> <!-- Calendar Mode -->
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{x:Bind domain:Translator.ProviderSelection_CalendarModeHeader}" /> <TextBlock
Style="{StaticResource BodyStrongTextBlockStyle}"
Text="{x:Bind domain:Translator.ProviderSelection_CalendarModeHeader}"
Visibility="{x:Bind ViewModel.IsCalendarModeSelectionVisible, Mode=OneWay}" />
<ListView <ListView
x:Name="CalendarModeListView" x:Name="CalendarModeListView"
IsItemClickEnabled="False" IsItemClickEnabled="False"
SelectionChanged="CalendarModeSelectionChanged" SelectionChanged="CalendarModeSelectionChanged"
SelectionMode="Single"> SelectionMode="Single"
Visibility="{x:Bind ViewModel.IsCalendarModeSelectionVisible, Mode=OneWay}">
<!-- Disabled --> <!-- Disabled -->
<ListViewItem> <ListViewItem>
<Grid Padding="12" ColumnSpacing="10"> <Grid Padding="12" ColumnSpacing="10">
+6 -1
View File
@@ -371,8 +371,13 @@ public sealed partial class WinoAppShell : Views.Abstract.WinoAppShellAbstract,
{ {
_ = DispatcherQueue.EnqueueAsync(async () => _ = DispatcherQueue.EnqueueAsync(async () =>
{ {
ViewModel.NavigationService.ChangeApplicationMode(WinoApplicationMode.Mail);
await ViewModel.MailClient.HandleAccountCreatedAsync(message.Account); await ViewModel.MailClient.HandleAccountCreatedAsync(message.Account);
var targetMode = !message.Account.IsMailAccessGranted && message.Account.IsCalendarAccessGranted
? WinoApplicationMode.Calendar
: WinoApplicationMode.Mail;
ViewModel.NavigationService.ChangeApplicationMode(targetMode);
}); });
} }
+3 -3
View File
@@ -211,9 +211,6 @@ public class AccountService : BaseDatabaseService, IAccountService
Guard.IsNotNull(token); Guard.IsNotNull(token);
// Enable calendar access since new token includes calendar scopes
account.IsCalendarAccessGranted = true;
await UpdateAccountAsync(account); await UpdateAccountAsync(account);
} }
@@ -270,6 +267,9 @@ public class AccountService : BaseDatabaseService, IAccountService
public async Task CreateRootAliasAsync(Guid accountId, string address) public async Task CreateRootAliasAsync(Guid accountId, string address)
{ {
if (string.IsNullOrWhiteSpace(address))
return;
var rootAlias = new MailAccountAlias() var rootAlias = new MailAccountAlias()
{ {
AccountId = accountId, AccountId = accountId,
+7
View File
@@ -97,6 +97,13 @@ public class DatabaseService : IDatabaseService
.ConfigureAwait(false); .ConfigureAwait(false);
} }
if (!accountColumns.Any(c => c.Name == nameof(MailAccount.IsMailAccessGranted)))
{
await Connection
.ExecuteAsync($"ALTER TABLE {nameof(MailAccount)} ADD COLUMN {nameof(MailAccount.IsMailAccessGranted)} INTEGER NOT NULL DEFAULT 1")
.ConfigureAwait(false);
}
var folderColumns = await Connection.GetTableInfoAsync(nameof(MailItemFolder)).ConfigureAwait(false); var folderColumns = await Connection.GetTableInfoAsync(nameof(MailItemFolder)).ConfigureAwait(false);
if (!folderColumns.Any(c => c.Name == nameof(MailItemFolder.HighestKnownUid))) if (!folderColumns.Any(c => c.Name == nameof(MailItemFolder.HighestKnownUid)))
+43 -1
View File
@@ -2,6 +2,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
@@ -245,7 +246,11 @@ public sealed class WinoAccountDataSyncService : IWinoAccountDataSyncService
var serverInformation = CreateImportedServerInformation(mailbox, account.Id); var serverInformation = CreateImportedServerInformation(mailbox, account.Id);
await _accountService.CreateAccountAsync(account, serverInformation).ConfigureAwait(false); await _accountService.CreateAccountAsync(account, serverInformation).ConfigureAwait(false);
if (account.IsMailAccessGranted)
{
await _accountService.CreateRootAliasAsync(account.Id, account.Address).ConfigureAwait(false); await _accountService.CreateRootAliasAsync(account.Id, account.Address).ConfigureAwait(false);
}
if (account.ProviderType == MailProviderType.IMAP4) if (account.ProviderType == MailProviderType.IMAP4)
{ {
@@ -289,7 +294,7 @@ public sealed class WinoAccountDataSyncService : IWinoAccountDataSyncService
? account.ServerInformation ? account.ServerInformation
: null; : null;
return new UserMailboxSyncItemDto var mailbox = new UserMailboxSyncItemDto
{ {
Address = account.Address ?? string.Empty, Address = account.Address ?? string.Empty,
ProviderType = (int)account.ProviderType, ProviderType = (int)account.ProviderType,
@@ -316,6 +321,10 @@ public sealed class WinoAccountDataSyncService : IWinoAccountDataSyncService
ProxyServerPort = serverInformation?.ProxyServerPort, ProxyServerPort = serverInformation?.ProxyServerPort,
MaxConcurrentClients = serverInformation?.MaxConcurrentClients MaxConcurrentClients = serverInformation?.MaxConcurrentClients
}; };
SetOptionalBooleanProperty(mailbox, "IsMailAccessGranted", account.IsMailAccessGranted);
return mailbox;
} }
private static MailAccount CreateImportedAccount(UserMailboxSyncItemDto mailbox) private static MailAccount CreateImportedAccount(UserMailboxSyncItemDto mailbox)
@@ -334,6 +343,7 @@ public sealed class WinoAccountDataSyncService : IWinoAccountDataSyncService
Base64ProfilePictureData = string.Empty, Base64ProfilePictureData = string.Empty,
CreatedAt = DateTime.UtcNow, CreatedAt = DateTime.UtcNow,
InitialSynchronizationRange = InitialSynchronizationRange.SixMonths, InitialSynchronizationRange = InitialSynchronizationRange.SixMonths,
IsMailAccessGranted = GetOptionalBooleanProperty(mailbox, "IsMailAccessGranted", defaultValue: true),
IsCalendarAccessGranted = mailbox.IsCalendarAccessGranted, IsCalendarAccessGranted = mailbox.IsCalendarAccessGranted,
SynchronizationDeltaIdentifier = string.Empty, SynchronizationDeltaIdentifier = string.Empty,
CalendarSynchronizationDeltaIdentifier = string.Empty, CalendarSynchronizationDeltaIdentifier = string.Empty,
@@ -410,6 +420,38 @@ public sealed class WinoAccountDataSyncService : IWinoAccountDataSyncService
private static string CreateMailboxKey(string? address, int providerType) private static string CreateMailboxKey(string? address, int providerType)
=> $"{address?.Trim().ToLowerInvariant()}|{providerType}"; => $"{address?.Trim().ToLowerInvariant()}|{providerType}";
private static bool GetOptionalBooleanProperty<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(
T instance,
string propertyName,
bool defaultValue)
{
if (instance == null)
return defaultValue;
var property = typeof(T).GetProperty(propertyName);
if (property?.PropertyType != typeof(bool) || !property.CanRead)
return defaultValue;
return property.GetValue(instance) is bool boolValue
? boolValue
: defaultValue;
}
private static void SetOptionalBooleanProperty<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(
T instance,
string propertyName,
bool value)
{
if (instance == null)
return;
var property = typeof(T).GetProperty(propertyName);
if (property?.PropertyType == typeof(bool) && property.CanWrite)
{
property.SetValue(instance, value);
}
}
private static string TrimUtf8Bom(string jsonContent) private static string TrimUtf8Bom(string jsonContent)
=> !string.IsNullOrEmpty(jsonContent) && jsonContent[0] == '\uFEFF' => !string.IsNullOrEmpty(jsonContent) && jsonContent[0] == '\uFEFF'
? jsonContent[1..] ? jsonContent[1..]