Merged feature/vNext. Initial commit for Wino Mail 2.0
This commit is contained in:
@@ -1,18 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Wino.Core.Misc;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Accounts;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.Services;
|
||||
using Wino.Core.ViewModels.Data;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
using Wino.Messaging.Client.Calendar;
|
||||
using Wino.Messaging.Client.Navigation;
|
||||
using Wino.Messaging.Server;
|
||||
|
||||
namespace Wino.Mail.ViewModels;
|
||||
|
||||
@@ -20,13 +28,55 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
|
||||
{
|
||||
private readonly IMailDialogService _dialogService;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly IWinoServerConnectionManager _serverConnectionManager;
|
||||
private readonly IFolderService _folderService;
|
||||
private readonly ICalendarService _calendarService;
|
||||
private readonly IStatePersistanceService _statePersistanceService;
|
||||
private readonly INewThemeService _themeService;
|
||||
private readonly IImapTestService _imapTestService;
|
||||
private bool isLoaded = false;
|
||||
|
||||
public MailAccount Account { get; set; }
|
||||
[ObservableProperty]
|
||||
public partial MailAccount Account { get; set; }
|
||||
public ObservableCollection<IMailItemFolder> CurrentFolders { get; set; } = [];
|
||||
public ObservableCollection<AccountCalendar> AccountCalendars { get; set; } = [];
|
||||
public ObservableCollection<AccountCalendarSettingsItemViewModel> AccountCalendarSettingsItems { get; } = [];
|
||||
public ObservableCollection<AccountCalendarShowAsOption> ShowAsOptions { get; } = [];
|
||||
|
||||
[ObservableProperty]
|
||||
public partial AccountCalendar SelectedPrimaryCalendar { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int SelectedTabIndex { get; set; } = 0; // Default to Mail tab
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string AccountName { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string SenderName { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial AppColorViewModel SelectedColor { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsImapServer))]
|
||||
public partial CustomServerInformation ServerInformation { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial List<AppColorViewModel> AvailableColors { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int SelectedIncomingServerConnectionSecurityIndex { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int SelectedIncomingServerAuthenticationMethodIndex { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int SelectedOutgoingServerConnectionSecurityIndex { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int SelectedOutgoingServerAuthenticationMethodIndex { get; set; }
|
||||
|
||||
// Mail-related properties
|
||||
[ObservableProperty]
|
||||
private bool isFocusedInboxEnabled;
|
||||
|
||||
@@ -46,17 +96,56 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
|
||||
private bool isTaskbarBadgeEnabled;
|
||||
|
||||
public bool IsFocusedInboxSupportedForAccount => Account != null && Account.Preferences.IsFocusedInboxEnabled != null;
|
||||
public bool IsImapServer => ServerInformation != null;
|
||||
public string ProviderIconPath => Account?.SpecialImapProvider != SpecialImapProvider.None
|
||||
? $"ms-appx:///Assets/Providers/{Account.SpecialImapProvider}.png"
|
||||
: $"ms-appx:///Assets/Providers/{Account?.ProviderType}.png";
|
||||
public string Address => Account?.Address ?? string.Empty;
|
||||
|
||||
public List<ImapAuthenticationMethodModel> AvailableAuthenticationMethods { get; } =
|
||||
[
|
||||
new ImapAuthenticationMethodModel(Core.Domain.Enums.ImapAuthenticationMethod.Auto, Translator.ImapAuthenticationMethod_Auto),
|
||||
new ImapAuthenticationMethodModel(Core.Domain.Enums.ImapAuthenticationMethod.None, Translator.ImapAuthenticationMethod_None),
|
||||
new ImapAuthenticationMethodModel(Core.Domain.Enums.ImapAuthenticationMethod.NormalPassword, Translator.ImapAuthenticationMethod_Plain),
|
||||
new ImapAuthenticationMethodModel(Core.Domain.Enums.ImapAuthenticationMethod.EncryptedPassword, Translator.ImapAuthenticationMethod_EncryptedPassword),
|
||||
new ImapAuthenticationMethodModel(Core.Domain.Enums.ImapAuthenticationMethod.Ntlm, Translator.ImapAuthenticationMethod_Ntlm),
|
||||
new ImapAuthenticationMethodModel(Core.Domain.Enums.ImapAuthenticationMethod.CramMd5, Translator.ImapAuthenticationMethod_CramMD5),
|
||||
new ImapAuthenticationMethodModel(Core.Domain.Enums.ImapAuthenticationMethod.DigestMd5, Translator.ImapAuthenticationMethod_DigestMD5)
|
||||
];
|
||||
|
||||
public List<ImapConnectionSecurityModel> AvailableConnectionSecurities { get; set; } =
|
||||
[
|
||||
new ImapConnectionSecurityModel(Core.Domain.Enums.ImapConnectionSecurity.Auto, Translator.ImapConnectionSecurity_Auto),
|
||||
new ImapConnectionSecurityModel(Core.Domain.Enums.ImapConnectionSecurity.SslTls, Translator.ImapConnectionSecurity_SslTls),
|
||||
new ImapConnectionSecurityModel(Core.Domain.Enums.ImapConnectionSecurity.StartTls, Translator.ImapConnectionSecurity_StartTls),
|
||||
new ImapConnectionSecurityModel(Core.Domain.Enums.ImapConnectionSecurity.None, Translator.ImapConnectionSecurity_None)
|
||||
];
|
||||
|
||||
|
||||
public AccountDetailsPageViewModel(IMailDialogService dialogService,
|
||||
IAccountService accountService,
|
||||
IWinoServerConnectionManager serverConnectionManager,
|
||||
IFolderService folderService)
|
||||
IFolderService folderService,
|
||||
ICalendarService calendarService,
|
||||
IStatePersistanceService statePersistanceService,
|
||||
INewThemeService themeService,
|
||||
IImapTestService imapTestService)
|
||||
{
|
||||
_dialogService = dialogService;
|
||||
_accountService = accountService;
|
||||
_serverConnectionManager = serverConnectionManager;
|
||||
_folderService = folderService;
|
||||
_calendarService = calendarService;
|
||||
_statePersistanceService = statePersistanceService;
|
||||
_themeService = themeService;
|
||||
_imapTestService = imapTestService;
|
||||
|
||||
var colorHexList = _themeService.GetAvailableAccountColors();
|
||||
AvailableColors = colorHexList.Select(a => new AppColorViewModel(a)).ToList();
|
||||
|
||||
ShowAsOptions.Add(new AccountCalendarShowAsOption(CalendarItemShowAs.Free, Translator.CalendarShowAs_Free));
|
||||
ShowAsOptions.Add(new AccountCalendarShowAsOption(CalendarItemShowAs.Tentative, Translator.CalendarShowAs_Tentative));
|
||||
ShowAsOptions.Add(new AccountCalendarShowAsOption(CalendarItemShowAs.Busy, Translator.CalendarShowAs_Busy));
|
||||
ShowAsOptions.Add(new AccountCalendarShowAsOption(CalendarItemShowAs.OutOfOffice, Translator.CalendarShowAs_OutOfOffice));
|
||||
ShowAsOptions.Add(new AccountCalendarShowAsOption(CalendarItemShowAs.WorkingElsewhere, Translator.CalendarShowAs_WorkingElsewhere));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -71,44 +160,80 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
|
||||
private void EditAliases()
|
||||
=> Messenger.Send(new BreadcrumbNavigationRequested(Translator.SettingsManageAliases_Title, WinoPage.AliasManagementPage, Account.Id));
|
||||
|
||||
public Task FolderSyncToggledAsync(IMailItemFolder folderStructure, bool isEnabled)
|
||||
=> _folderService.ChangeFolderSynchronizationStateAsync(folderStructure.Id, isEnabled);
|
||||
|
||||
public Task FolderShowUnreadToggled(IMailItemFolder folderStructure, bool isEnabled)
|
||||
=> _folderService.ChangeFolderShowUnreadCountStateAsync(folderStructure.Id, isEnabled);
|
||||
[RelayCommand]
|
||||
private void EditImapCalDavSettings()
|
||||
=> Messenger.Send(new BreadcrumbNavigationRequested(
|
||||
Translator.ImapCalDavSettingsPage_TitleEdit,
|
||||
WinoPage.ImapCalDavSettingsPage,
|
||||
ImapCalDavSettingsNavigationContext.CreateForEditMode(Account.Id)));
|
||||
|
||||
[RelayCommand]
|
||||
private void EditAccountDetails()
|
||||
=> Messenger.Send(new BreadcrumbNavigationRequested(Translator.SettingsEditAccountDetails_Title, WinoPage.EditAccountDetailsPage, Account));
|
||||
private async Task SaveChangesAsync()
|
||||
{
|
||||
await UpdateAccountAsync();
|
||||
|
||||
_dialogService.InfoBarMessage(Translator.EditAccountDetailsPage_SaveSuccess_Title, Translator.EditAccountDetailsPage_SaveSuccess_Message, InfoBarMessageType.Success);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task DeleteAccount()
|
||||
private async Task DeleteAccountAsync()
|
||||
{
|
||||
if (Account == null)
|
||||
return;
|
||||
|
||||
var confirmation = await _dialogService.ShowConfirmationDialogAsync(Translator.DialogMessage_DeleteAccountConfirmationTitle,
|
||||
string.Format(Translator.DialogMessage_DeleteAccountConfirmationMessage, Account.Name),
|
||||
Translator.Buttons_Delete);
|
||||
string.Format(Translator.DialogMessage_DeleteAccountConfirmationMessage, Account.Name),
|
||||
Translator.Buttons_Delete);
|
||||
|
||||
if (!confirmation)
|
||||
return;
|
||||
|
||||
await SynchronizationManager.Instance.DestroySynchronizerAsync(Account.Id);
|
||||
await _accountService.DeleteAccountAsync(Account);
|
||||
|
||||
var isSynchronizerKilledResponse = await _serverConnectionManager.GetResponseAsync<bool, KillAccountSynchronizerRequested>(new KillAccountSynchronizerRequested(Account.Id));
|
||||
_dialogService.InfoBarMessage(Translator.Info_AccountDeletedTitle, string.Format(Translator.Info_AccountDeletedMessage, Account.Name), InfoBarMessageType.Success);
|
||||
|
||||
if (isSynchronizerKilledResponse.IsSuccess)
|
||||
Messenger.Send(new BackBreadcrumNavigationRequested());
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ValidateImapSettingsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _accountService.DeleteAccountAsync(Account);
|
||||
|
||||
_dialogService.InfoBarMessage(Translator.Info_AccountDeletedTitle, string.Format(Translator.Info_AccountDeletedMessage, Account.Name), InfoBarMessageType.Success);
|
||||
|
||||
Messenger.Send(new BackBreadcrumNavigationRequested());
|
||||
await _imapTestService.TestImapConnectionAsync(ServerInformation, true);
|
||||
_dialogService.InfoBarMessage(Translator.IMAPSetupDialog_ValidationSuccess_Title, Translator.IMAPSetupDialog_ValidationSuccess_Message, InfoBarMessageType.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_dialogService.InfoBarMessage(Translator.IMAPSetupDialog_ValidationFailed_Title, ex.Message, InfoBarMessageType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task UpdateCustomServerInformationAsync()
|
||||
{
|
||||
if (ServerInformation != null)
|
||||
{
|
||||
ServerInformation.IncomingAuthenticationMethod = AvailableAuthenticationMethods[SelectedIncomingServerAuthenticationMethodIndex].ImapAuthenticationMethod;
|
||||
ServerInformation.IncomingServerSocketOption = AvailableConnectionSecurities[SelectedIncomingServerConnectionSecurityIndex].ImapConnectionSecurity;
|
||||
|
||||
ServerInformation.OutgoingAuthenticationMethod = AvailableAuthenticationMethods[SelectedOutgoingServerAuthenticationMethodIndex].ImapAuthenticationMethod;
|
||||
ServerInformation.OutgoingServerSocketOption = AvailableConnectionSecurities[SelectedOutgoingServerConnectionSecurityIndex].ImapConnectionSecurity;
|
||||
|
||||
Account.ServerInformation = ServerInformation;
|
||||
}
|
||||
|
||||
await _accountService.UpdateAccountCustomServerInformationAsync(Account.ServerInformation);
|
||||
|
||||
_dialogService.InfoBarMessage(Translator.IMAPSetupDialog_SaveImapSuccess_Title, Translator.IMAPSetupDialog_SaveImapSuccess_Message, InfoBarMessageType.Success);
|
||||
}
|
||||
|
||||
public Task FolderSyncToggledAsync(IMailItemFolder folderStructure, bool isEnabled)
|
||||
=> _folderService.ChangeFolderSynchronizationStateAsync(folderStructure.Id, isEnabled);
|
||||
|
||||
public Task FolderShowUnreadToggled(IMailItemFolder folderStructure, bool isEnabled)
|
||||
=> _folderService.ChangeFolderShowUnreadCountStateAsync(folderStructure.Id, isEnabled);
|
||||
|
||||
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
{
|
||||
@@ -117,6 +242,9 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
|
||||
if (parameters is Guid accountId)
|
||||
{
|
||||
Account = await _accountService.GetAccountAsync(accountId);
|
||||
AccountName = Account.Name;
|
||||
SenderName = Account.SenderName;
|
||||
ServerInformation = Account.ServerInformation;
|
||||
|
||||
IsFocusedInboxEnabled = Account.Preferences.IsFocusedInboxEnabled.GetValueOrDefault();
|
||||
AreNotificationsEnabled = Account.Preferences.IsNotificationsEnabled;
|
||||
@@ -126,7 +254,24 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
|
||||
IsAppendMessageSettinEnabled = Account.Preferences.ShouldAppendMessagesToSentFolder;
|
||||
IsTaskbarBadgeEnabled = Account.Preferences.IsTaskbarBadgeEnabled;
|
||||
|
||||
OnPropertyChanged(nameof(IsFocusedInboxSupportedForAccount));
|
||||
if (!string.IsNullOrEmpty(Account.AccountColorHex))
|
||||
{
|
||||
SelectedColor = AvailableColors.FirstOrDefault(a => a.Hex == Account.AccountColorHex);
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedColor = null;
|
||||
}
|
||||
|
||||
if (ServerInformation != null)
|
||||
{
|
||||
SelectedIncomingServerAuthenticationMethodIndex = AvailableAuthenticationMethods.FindIndex(a => a.ImapAuthenticationMethod == ServerInformation.IncomingAuthenticationMethod);
|
||||
SelectedIncomingServerConnectionSecurityIndex = AvailableConnectionSecurities.FindIndex(a => a.ImapConnectionSecurity == ServerInformation.IncomingServerSocketOption);
|
||||
SelectedOutgoingServerAuthenticationMethodIndex = AvailableAuthenticationMethods.FindIndex(a => a.ImapAuthenticationMethod == ServerInformation.OutgoingAuthenticationMethod);
|
||||
SelectedOutgoingServerConnectionSecurityIndex = AvailableConnectionSecurities.FindIndex(a => a.ImapConnectionSecurity == ServerInformation.OutgoingServerSocketOption);
|
||||
}
|
||||
|
||||
SelectedTabIndex = _statePersistanceService.ApplicationMode == WinoApplicationMode.Calendar ? 2 : 1;
|
||||
|
||||
var folderStructures = (await _folderService.GetFolderStructureForAccountAsync(Account.Id, true)).Folders;
|
||||
|
||||
@@ -135,10 +280,90 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
|
||||
CurrentFolders.Add(folder);
|
||||
}
|
||||
|
||||
// Load calendar list
|
||||
await LoadAccountCalendarsAsync();
|
||||
|
||||
isLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
private Task UpdateAccountAsync()
|
||||
{
|
||||
Account.Name = AccountName;
|
||||
Account.SenderName = SenderName;
|
||||
Account.AccountColorHex = SelectedColor?.Hex ?? string.Empty;
|
||||
|
||||
return _accountService.UpdateAccountAsync(Account);
|
||||
}
|
||||
|
||||
private async Task LoadAccountCalendarsAsync()
|
||||
{
|
||||
var calendars = await _calendarService.GetAccountCalendarsAsync(Account.Id);
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
AccountCalendars.Clear();
|
||||
AccountCalendarSettingsItems.Clear();
|
||||
|
||||
foreach (var calendar in calendars)
|
||||
{
|
||||
AccountCalendars.Add(calendar);
|
||||
AccountCalendarSettingsItems.Add(new AccountCalendarSettingsItemViewModel(calendar, ShowAsOptions, AvailableColors));
|
||||
}
|
||||
});
|
||||
|
||||
SelectedPrimaryCalendar = AccountCalendars.FirstOrDefault(calendar => calendar.IsPrimary) ?? AccountCalendars.FirstOrDefault();
|
||||
}
|
||||
|
||||
public AccountCalendarShowAsOption GetShowAsOption(CalendarItemShowAs showAs)
|
||||
=> ShowAsOptions.FirstOrDefault(option => option.ShowAs == showAs) ?? ShowAsOptions.First();
|
||||
|
||||
public async Task UpdateCalendarSynchronizationAsync(AccountCalendar calendar, bool isEnabled)
|
||||
{
|
||||
if (calendar == null || calendar.IsSynchronizationEnabled == isEnabled)
|
||||
return;
|
||||
|
||||
calendar.IsSynchronizationEnabled = isEnabled;
|
||||
await _calendarService.UpdateAccountCalendarAsync(calendar);
|
||||
}
|
||||
|
||||
public async Task UpdateCalendarDefaultShowAsAsync(AccountCalendar calendar, AccountCalendarShowAsOption option)
|
||||
{
|
||||
if (calendar == null || option == null || calendar.DefaultShowAs == option.ShowAs)
|
||||
return;
|
||||
|
||||
calendar.DefaultShowAs = option.ShowAs;
|
||||
await _calendarService.UpdateAccountCalendarAsync(calendar);
|
||||
}
|
||||
|
||||
public async Task UpdateCalendarColorAsync(AccountCalendarSettingsItemViewModel calendarItem, AppColorViewModel color)
|
||||
{
|
||||
if (calendarItem?.Calendar == null || color == null || calendarItem.Calendar.BackgroundColorHex == color.Hex)
|
||||
return;
|
||||
|
||||
calendarItem.SetBackgroundColor(color);
|
||||
await _calendarService.UpdateAccountCalendarAsync(calendarItem.Calendar);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ResetColor()
|
||||
=> SelectedColor = null;
|
||||
|
||||
partial void OnSelectedColorChanged(AppColorViewModel oldValue, AppColorViewModel newValue)
|
||||
{
|
||||
if (Account != null)
|
||||
{
|
||||
_ = UpdateAccountAsync();
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnAccountChanged(MailAccount value)
|
||||
{
|
||||
OnPropertyChanged(nameof(IsFocusedInboxSupportedForAccount));
|
||||
OnPropertyChanged(nameof(ProviderIconPath));
|
||||
OnPropertyChanged(nameof(Address));
|
||||
}
|
||||
|
||||
protected override async void OnPropertyChanged(PropertyChangedEventArgs e)
|
||||
{
|
||||
base.OnPropertyChanged(e);
|
||||
@@ -167,6 +392,64 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
|
||||
Account.Preferences.IsTaskbarBadgeEnabled = IsTaskbarBadgeEnabled;
|
||||
await _accountService.UpdateAccountAsync(Account);
|
||||
break;
|
||||
case nameof(SelectedPrimaryCalendar) when SelectedPrimaryCalendar != null:
|
||||
foreach (var calendar in AccountCalendars)
|
||||
{
|
||||
calendar.IsPrimary = calendar.Id == SelectedPrimaryCalendar.Id;
|
||||
}
|
||||
|
||||
await _calendarService.SetPrimaryCalendarAsync(Account.Id, SelectedPrimaryCalendar.Id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class AccountCalendarShowAsOption
|
||||
{
|
||||
public CalendarItemShowAs ShowAs { get; }
|
||||
public string DisplayText { get; }
|
||||
|
||||
public AccountCalendarShowAsOption(CalendarItemShowAs showAs, string displayText)
|
||||
{
|
||||
ShowAs = showAs;
|
||||
DisplayText = displayText;
|
||||
}
|
||||
}
|
||||
|
||||
public partial class AccountCalendarSettingsItemViewModel : ObservableObject
|
||||
{
|
||||
public AccountCalendar Calendar { get; }
|
||||
public ObservableCollection<AccountCalendarShowAsOption> ShowAsOptions { get; }
|
||||
public List<AppColorViewModel> AvailableColors { get; }
|
||||
|
||||
public string Name => Calendar.Name;
|
||||
public string TimeZone => Calendar.TimeZone;
|
||||
public string BackgroundColorHex => Calendar.BackgroundColorHex;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsSynchronizationEnabled { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial AccountCalendarShowAsOption SelectedShowAsOption { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial AppColorViewModel SelectedColor { get; set; }
|
||||
|
||||
public AccountCalendarSettingsItemViewModel(AccountCalendar calendar, ObservableCollection<AccountCalendarShowAsOption> showAsOptions, List<AppColorViewModel> availableColors)
|
||||
{
|
||||
Calendar = calendar;
|
||||
ShowAsOptions = showAsOptions;
|
||||
AvailableColors = availableColors;
|
||||
IsSynchronizationEnabled = calendar.IsSynchronizationEnabled;
|
||||
SelectedShowAsOption = showAsOptions.FirstOrDefault(option => option.ShowAs == calendar.DefaultShowAs) ?? showAsOptions.FirstOrDefault();
|
||||
SelectedColor = availableColors.FirstOrDefault(color => string.Equals(color.Hex, calendar.BackgroundColorHex, StringComparison.OrdinalIgnoreCase))
|
||||
?? new AppColorViewModel(calendar.BackgroundColorHex ?? ColorHelpers.GenerateFlatColorHex());
|
||||
}
|
||||
|
||||
public void SetBackgroundColor(AppColorViewModel color)
|
||||
{
|
||||
SelectedColor = color;
|
||||
Calendar.BackgroundColorHex = color.Hex;
|
||||
OnPropertyChanged(nameof(BackgroundColorHex));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,43 +12,42 @@ using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Exceptions;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Authentication;
|
||||
using Wino.Core.Domain.Models.Connectivity;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
using Wino.Core.Services;
|
||||
using Wino.Core.ViewModels;
|
||||
using Wino.Core.ViewModels.Data;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
using Wino.Messaging.Client.Navigation;
|
||||
using Wino.Messaging.Server;
|
||||
using Wino.Messaging.UI;
|
||||
|
||||
namespace Wino.Mail.ViewModels;
|
||||
|
||||
public partial class AccountManagementViewModel : AccountManagementPageViewModelBase
|
||||
{
|
||||
private readonly ISpecialImapProviderConfigResolver _specialImapProviderConfigResolver;
|
||||
private readonly IImapTestService _imapTestService;
|
||||
private readonly IWinoLogger _winoLogger;
|
||||
private readonly ISpecialImapProviderConfigResolver _specialImapProviderConfigResolver;
|
||||
private readonly ICalDavClient _calDavClient;
|
||||
|
||||
public IMailDialogService MailDialogService { get; }
|
||||
|
||||
public AccountManagementViewModel(IMailDialogService dialogService,
|
||||
IWinoServerConnectionManager winoServerConnectionManager,
|
||||
INavigationService navigationService,
|
||||
IAccountService accountService,
|
||||
ISpecialImapProviderConfigResolver specialImapProviderConfigResolver,
|
||||
IProviderService providerService,
|
||||
IImapTestService imapTestService,
|
||||
IStoreManagementService storeManagementService,
|
||||
IWinoAccountProfileService winoAccountProfileService,
|
||||
IWinoLogger winoLogger,
|
||||
ISpecialImapProviderConfigResolver specialImapProviderConfigResolver,
|
||||
ICalDavClient calDavClient,
|
||||
IAuthenticationProvider authenticationProvider,
|
||||
IPreferencesService preferencesService) : base(dialogService, winoServerConnectionManager, navigationService, accountService, providerService, storeManagementService, authenticationProvider, preferencesService)
|
||||
IPreferencesService preferencesService) : base(dialogService, navigationService, accountService, providerService, storeManagementService, winoAccountProfileService, authenticationProvider, preferencesService)
|
||||
{
|
||||
MailDialogService = dialogService;
|
||||
_specialImapProviderConfigResolver = specialImapProviderConfigResolver;
|
||||
_imapTestService = imapTestService;
|
||||
_winoLogger = winoLogger;
|
||||
_specialImapProviderConfigResolver = specialImapProviderConfigResolver;
|
||||
_calDavClient = calDavClient;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -88,246 +87,114 @@ public partial class AccountManagementViewModel : AccountManagementPageViewModel
|
||||
return;
|
||||
}
|
||||
|
||||
MailAccount createdAccount = null;
|
||||
IAccountCreationDialog creationDialog = null;
|
||||
Messenger.Send(new BreadcrumbNavigationRequested(Translator.WelcomeWizard_Step2Title, WinoPage.ProviderSelectionPage));
|
||||
}
|
||||
|
||||
try
|
||||
public Task StartAddNewAccountAsync() => AddNewAccountAsync();
|
||||
|
||||
private async Task ValidateSpecialImapConnectivityAsync(CustomServerInformation serverInformation)
|
||||
{
|
||||
var connectivityResult = await SynchronizationManager.Instance
|
||||
.TestImapConnectivityAsync(serverInformation, allowSSLHandshake: false)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (connectivityResult.IsCertificateUIRequired)
|
||||
{
|
||||
var providers = ProviderService.GetAvailableProviders();
|
||||
var certificateMessage =
|
||||
$"{Translator.IMAPSetupDialog_CertificateAllowanceRequired_Row0}\n\n" +
|
||||
$"{Translator.IMAPSetupDialog_CertificateIssuer}: {connectivityResult.CertificateIssuer}\n" +
|
||||
$"{Translator.IMAPSetupDialog_CertificateValidFrom}: {connectivityResult.CertificateValidFromDateString}\n" +
|
||||
$"{Translator.IMAPSetupDialog_CertificateValidTo}: {connectivityResult.CertificateExpirationDateString}\n\n" +
|
||||
$"{Translator.IMAPSetupDialog_CertificateAllowanceRequired_Row1}";
|
||||
|
||||
// Select provider.
|
||||
var accountCreationDialogResult = await MailDialogService.ShowAccountProviderSelectionDialogAsync(providers);
|
||||
var allowCertificate = await ExecuteUIThreadTaskAsync(
|
||||
() => MailDialogService.ShowConfirmationDialogAsync(certificateMessage, Translator.GeneralTitle_Warning, Translator.Buttons_Allow))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var accountCreationCancellationTokenSource = new CancellationTokenSource();
|
||||
if (!allowCertificate)
|
||||
throw new InvalidOperationException(Translator.IMAPSetupDialog_CertificateDenied);
|
||||
|
||||
if (accountCreationDialogResult != null)
|
||||
connectivityResult = await SynchronizationManager.Instance
|
||||
.TestImapConnectivityAsync(serverInformation, allowSSLHandshake: true)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!connectivityResult.IsSuccess)
|
||||
throw new InvalidOperationException(connectivityResult.FailedReason ?? Translator.IMAPSetupDialog_ConnectionFailedMessage);
|
||||
|
||||
if (serverInformation.CalendarSupportMode != ImapCalendarSupportMode.CalDav)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(serverInformation.CalDavServiceUrl))
|
||||
throw new InvalidOperationException(Translator.ImapCalDavSettingsPage_CalDavUrlRequired);
|
||||
|
||||
var settings = new CalDavConnectionSettings
|
||||
{
|
||||
ServiceUri = new Uri(serverInformation.CalDavServiceUrl, UriKind.Absolute),
|
||||
Username = serverInformation.CalDavUsername,
|
||||
Password = serverInformation.CalDavPassword
|
||||
};
|
||||
|
||||
await _calDavClient.DiscoverCalendarsAsync(settings).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task ExecuteUIThreadTaskAsync(Func<Task> action)
|
||||
{
|
||||
if (Dispatcher == null)
|
||||
{
|
||||
await action().ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var completionSource = new TaskCompletionSource<object>();
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
_ = ExecuteAndCaptureAsync();
|
||||
|
||||
async Task ExecuteAndCaptureAsync()
|
||||
{
|
||||
creationDialog = MailDialogService.GetAccountCreationDialog(accountCreationDialogResult);
|
||||
|
||||
CustomServerInformation customServerInformation = null;
|
||||
|
||||
createdAccount = new MailAccount()
|
||||
try
|
||||
{
|
||||
ProviderType = accountCreationDialogResult.ProviderType,
|
||||
Name = accountCreationDialogResult.AccountName,
|
||||
SpecialImapProvider = accountCreationDialogResult.SpecialImapProviderDetails?.SpecialImapProvider ?? SpecialImapProvider.None,
|
||||
Id = Guid.NewGuid(),
|
||||
AccountColorHex = accountCreationDialogResult.AccountColorHex
|
||||
};
|
||||
|
||||
await creationDialog.ShowDialogAsync(accountCreationCancellationTokenSource);
|
||||
await Task.Delay(500);
|
||||
|
||||
creationDialog.State = AccountCreationDialogState.SigningIn;
|
||||
|
||||
string tokenInformation = string.Empty;
|
||||
|
||||
// Custom server implementation requires more async waiting.
|
||||
if (creationDialog is IImapAccountCreationDialog customServerDialog)
|
||||
{
|
||||
// Pass along the account properties and perform initial navigation on the imap frame.
|
||||
customServerDialog.StartImapConnectionSetup(createdAccount);
|
||||
|
||||
customServerInformation = await customServerDialog.GetCustomServerInformationAsync()
|
||||
?? throw new AccountSetupCanceledException();
|
||||
|
||||
// At this point connection is successful.
|
||||
// Save the server setup information and later on we'll fetch folders.
|
||||
|
||||
customServerInformation.AccountId = createdAccount.Id;
|
||||
|
||||
createdAccount.Address = customServerInformation.Address;
|
||||
createdAccount.ServerInformation = customServerInformation;
|
||||
createdAccount.SenderName = customServerInformation.DisplayName;
|
||||
await action().ConfigureAwait(false);
|
||||
completionSource.TrySetResult(null);
|
||||
}
|
||||
else
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Hanle special imap providers like iCloud and Yahoo.
|
||||
if (accountCreationDialogResult.SpecialImapProviderDetails != null)
|
||||
{
|
||||
// Special imap provider testing dialog. This is only available for iCloud and Yahoo.
|
||||
customServerInformation = _specialImapProviderConfigResolver.GetServerInformation(createdAccount, accountCreationDialogResult);
|
||||
customServerInformation.Id = Guid.NewGuid();
|
||||
customServerInformation.AccountId = createdAccount.Id;
|
||||
|
||||
createdAccount.SenderName = accountCreationDialogResult.SpecialImapProviderDetails.SenderName;
|
||||
createdAccount.Address = customServerInformation.Address;
|
||||
|
||||
// Let server validate the imap/smtp connection.
|
||||
var testResultResponse = await WinoServerConnectionManager.GetResponseAsync<ImapConnectivityTestResults, ImapConnectivityTestRequested>(new ImapConnectivityTestRequested(customServerInformation, true));
|
||||
|
||||
if (!testResultResponse.IsSuccess)
|
||||
{
|
||||
throw new Exception($"{Translator.IMAPSetupDialog_ConnectionFailedTitle}\n{testResultResponse.Message}");
|
||||
}
|
||||
else if (!testResultResponse.Data.IsSuccess)
|
||||
{
|
||||
// Server connectivity might succeed, but result might be failed.
|
||||
throw new ImapClientPoolException(testResultResponse.Data.FailedReason, customServerInformation, testResultResponse.Data.FailureProtocolLog);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// OAuth authentication is handled here.
|
||||
// Server authenticates, returns the token info here.
|
||||
|
||||
var tokenInformationResponse = await WinoServerConnectionManager
|
||||
.GetResponseAsync<TokenInformationEx, AuthorizationRequested>(new AuthorizationRequested(accountCreationDialogResult.ProviderType,
|
||||
createdAccount,
|
||||
createdAccount.ProviderType == MailProviderType.Gmail), accountCreationCancellationTokenSource.Token);
|
||||
|
||||
if (creationDialog.State == AccountCreationDialogState.Canceled)
|
||||
throw new AccountSetupCanceledException();
|
||||
|
||||
if (!tokenInformationResponse.IsSuccess)
|
||||
throw new Exception(tokenInformationResponse.Message);
|
||||
|
||||
createdAccount.Address = tokenInformationResponse.Data.AccountAddress;
|
||||
|
||||
tokenInformationResponse.ThrowIfFailed();
|
||||
}
|
||||
completionSource.TrySetException(ex);
|
||||
}
|
||||
|
||||
// Address is still doesn't have a value for API synchronizers.
|
||||
// It'll be synchronized with profile information.
|
||||
|
||||
await AccountService.CreateAccountAsync(createdAccount, customServerInformation);
|
||||
|
||||
// Local account has been created.
|
||||
|
||||
// Sync profile information if supported.
|
||||
if (createdAccount.IsProfileInfoSyncSupported)
|
||||
{
|
||||
// Start profile information synchronization.
|
||||
// It's only available for Outlook and Gmail synchronizers.
|
||||
|
||||
var profileSyncOptions = new MailSynchronizationOptions()
|
||||
{
|
||||
AccountId = createdAccount.Id,
|
||||
Type = MailSynchronizationType.UpdateProfile
|
||||
};
|
||||
|
||||
var profileSynchronizationResponse = await WinoServerConnectionManager.GetResponseAsync<MailSynchronizationResult, NewMailSynchronizationRequested>(new NewMailSynchronizationRequested(profileSyncOptions, SynchronizationSource.Client));
|
||||
|
||||
var profileSynchronizationResult = profileSynchronizationResponse.Data;
|
||||
|
||||
if (profileSynchronizationResult.CompletedState != SynchronizationCompletedState.Success)
|
||||
throw new Exception(Translator.Exception_FailedToSynchronizeProfileInformation);
|
||||
|
||||
createdAccount.SenderName = profileSynchronizationResult.ProfileInformation.SenderName;
|
||||
createdAccount.Base64ProfilePictureData = profileSynchronizationResult.ProfileInformation.Base64ProfilePictureData;
|
||||
|
||||
if (!string.IsNullOrEmpty(profileSynchronizationResult.ProfileInformation.AccountAddress))
|
||||
{
|
||||
createdAccount.Address = profileSynchronizationResult.ProfileInformation.AccountAddress;
|
||||
}
|
||||
|
||||
await AccountService.UpdateProfileInformationAsync(createdAccount.Id, profileSynchronizationResult.ProfileInformation);
|
||||
}
|
||||
|
||||
if (creationDialog is IImapAccountCreationDialog customServerAccountCreationDialog)
|
||||
customServerAccountCreationDialog.ShowPreparingFolders();
|
||||
else
|
||||
creationDialog.State = AccountCreationDialogState.PreparingFolders;
|
||||
|
||||
// Start synchronizing folders.
|
||||
var folderSyncOptions = new MailSynchronizationOptions()
|
||||
{
|
||||
AccountId = createdAccount.Id,
|
||||
Type = MailSynchronizationType.FoldersOnly
|
||||
};
|
||||
|
||||
var folderSynchronizationResponse = await WinoServerConnectionManager.GetResponseAsync<MailSynchronizationResult, NewMailSynchronizationRequested>(new NewMailSynchronizationRequested(folderSyncOptions, SynchronizationSource.Client));
|
||||
|
||||
var folderSynchronizationResult = folderSynchronizationResponse.Data;
|
||||
|
||||
if (folderSynchronizationResult == null || folderSynchronizationResult.CompletedState != SynchronizationCompletedState.Success)
|
||||
throw new Exception($"{Translator.Exception_FailedToSynchronizeFolders}\n{folderSynchronizationResponse.Message}");
|
||||
|
||||
// Sync aliases if supported.
|
||||
if (createdAccount.IsAliasSyncSupported)
|
||||
{
|
||||
// Try to synchronize aliases for the account.
|
||||
|
||||
var aliasSyncOptions = new MailSynchronizationOptions()
|
||||
{
|
||||
AccountId = createdAccount.Id,
|
||||
Type = MailSynchronizationType.Alias
|
||||
};
|
||||
|
||||
var aliasSyncResponse = await WinoServerConnectionManager.GetResponseAsync<MailSynchronizationResult, NewMailSynchronizationRequested>(new NewMailSynchronizationRequested(aliasSyncOptions, SynchronizationSource.Client));
|
||||
var aliasSynchronizationResult = folderSynchronizationResponse.Data;
|
||||
|
||||
if (aliasSynchronizationResult.CompletedState != SynchronizationCompletedState.Success)
|
||||
throw new Exception(Translator.Exception_FailedToSynchronizeAliases);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create root primary alias for the account.
|
||||
// This is only available for accounts that do not support alias synchronization.
|
||||
|
||||
await AccountService.CreateRootAliasAsync(createdAccount.Id, createdAccount.Address);
|
||||
}
|
||||
|
||||
// Send changes to listeners.
|
||||
ReportUIChange(new AccountCreatedMessage(createdAccount));
|
||||
|
||||
// Notify success.
|
||||
DialogService.InfoBarMessage(Translator.Info_AccountCreatedTitle, string.Format(Translator.Info_AccountCreatedMessage, createdAccount.Address), InfoBarMessageType.Success);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex.Message.Contains(nameof(GmailServiceDisabledException)))
|
||||
});
|
||||
|
||||
await completionSource.Task.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<T> ExecuteUIThreadTaskAsync<T>(Func<Task<T>> action)
|
||||
{
|
||||
if (Dispatcher == null)
|
||||
return await action().ConfigureAwait(false);
|
||||
|
||||
var completionSource = new TaskCompletionSource<T>();
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
// For Google Workspace accounts, Gmail API might be disabled by the admin.
|
||||
// Wino can't continue synchronization in this case.
|
||||
// We must notify the user about this and prevent account creation.
|
||||
_ = ExecuteAndCaptureAsync();
|
||||
|
||||
DialogService.InfoBarMessage(Translator.GmailServiceDisabled_Title, Translator.GmailServiceDisabled_Message, InfoBarMessageType.Error);
|
||||
|
||||
if (createdAccount != null)
|
||||
async Task ExecuteAndCaptureAsync()
|
||||
{
|
||||
await AccountService.DeleteAccountAsync(createdAccount);
|
||||
try
|
||||
{
|
||||
var result = await action().ConfigureAwait(false);
|
||||
completionSource.TrySetResult(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
completionSource.TrySetException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (AccountSetupCanceledException)
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
catch (Exception ex) when (ex.Message.Contains(nameof(AccountSetupCanceledException)))
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
catch (ImapClientPoolException testClientPoolException) when (testClientPoolException.CustomServerInformation != null)
|
||||
{
|
||||
var properties = testClientPoolException.CustomServerInformation.GetConnectionProperties();
|
||||
});
|
||||
|
||||
properties.Add("ProtocolLog", testClientPoolException.ProtocolLog);
|
||||
properties.Add("DiagnosticId", PreferencesService.DiagnosticId);
|
||||
|
||||
_winoLogger.TrackEvent("IMAP Test Failed", properties);
|
||||
|
||||
DialogService.InfoBarMessage(Translator.Info_AccountCreationFailedTitle, testClientPoolException.Message, InfoBarMessageType.Error);
|
||||
}
|
||||
catch (ImapClientPoolException clientPoolException) when (clientPoolException.InnerException != null)
|
||||
{
|
||||
DialogService.InfoBarMessage(Translator.Info_AccountCreationFailedTitle, clientPoolException.InnerException.Message, InfoBarMessageType.Error);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to create account.");
|
||||
|
||||
DialogService.InfoBarMessage(Translator.Info_AccountCreationFailedTitle, ex.Message, InfoBarMessageType.Error);
|
||||
|
||||
// Delete account in case of failure.
|
||||
if (createdAccount != null)
|
||||
{
|
||||
await AccountService.DeleteAccountAsync(createdAccount);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
creationDialog?.Complete(false);
|
||||
}
|
||||
return await completionSource.Task.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
|
||||
@@ -0,0 +1,477 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Serilog;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Accounts;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
using Wino.Core.Domain.Exceptions;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
using Wino.Core.Services;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
using Wino.Messaging.Client.Navigation;
|
||||
using Wino.Messaging.UI;
|
||||
|
||||
namespace Wino.Mail.ViewModels;
|
||||
|
||||
public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
|
||||
{
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly ISpecialImapProviderConfigResolver _specialImapProviderConfigResolver;
|
||||
private readonly ICalDavClient _calDavClient;
|
||||
private readonly IMailDialogService _dialogService;
|
||||
|
||||
public WelcomeWizardContext WizardContext { get; }
|
||||
|
||||
public ObservableCollection<AccountSetupStepModel> Steps { get; } = [];
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsSetupComplete { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsSetupFailed { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string FailureMessage { get; set; }
|
||||
|
||||
private MailAccount _createdAccount;
|
||||
private bool _dbWritten;
|
||||
|
||||
public AccountSetupProgressPageViewModel(
|
||||
IAccountService accountService,
|
||||
ISpecialImapProviderConfigResolver specialImapProviderConfigResolver,
|
||||
ICalDavClient calDavClient,
|
||||
IMailDialogService dialogService,
|
||||
WelcomeWizardContext wizardContext)
|
||||
{
|
||||
_accountService = accountService;
|
||||
_specialImapProviderConfigResolver = specialImapProviderConfigResolver;
|
||||
_calDavClient = calDavClient;
|
||||
_dialogService = dialogService;
|
||||
WizardContext = wizardContext;
|
||||
}
|
||||
|
||||
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedTo(mode, parameters);
|
||||
|
||||
// Only run on fresh navigation, not on back-navigation
|
||||
if (mode == NavigationMode.Back) return;
|
||||
|
||||
await RunSetupAsync();
|
||||
}
|
||||
|
||||
private void BuildSteps()
|
||||
{
|
||||
Steps.Clear();
|
||||
|
||||
if (WizardContext.IsOAuthProvider)
|
||||
{
|
||||
Steps.Add(new AccountSetupStepModel
|
||||
{
|
||||
Title = string.Format(Translator.AccountSetup_Step_Authenticating, WizardContext.SelectedProvider.Name)
|
||||
});
|
||||
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_FetchingProfile });
|
||||
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SavingAccount });
|
||||
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingFolders });
|
||||
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_Finalizing });
|
||||
}
|
||||
else if (WizardContext.IsSpecialImapProvider)
|
||||
{
|
||||
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_TestingMailAuth });
|
||||
|
||||
if (WizardContext.CalendarSupportMode == ImapCalendarSupportMode.CalDav)
|
||||
{
|
||||
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_DiscoveringCalDav });
|
||||
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_TestingCalendarAuth });
|
||||
}
|
||||
|
||||
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SavingAccount });
|
||||
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingFolders });
|
||||
|
||||
if (WizardContext.CalendarSupportMode != ImapCalendarSupportMode.Disabled)
|
||||
{
|
||||
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_FetchingCalendarMetadata });
|
||||
}
|
||||
|
||||
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_Finalizing });
|
||||
}
|
||||
else // Generic IMAP
|
||||
{
|
||||
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SavingAccount });
|
||||
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingFolders });
|
||||
|
||||
var setupResult = WizardContext.ImapCalDavSetupResult;
|
||||
if (setupResult?.IsCalendarAccessGranted == true &&
|
||||
setupResult.ServerInformation?.CalendarSupportMode == ImapCalendarSupportMode.CalDav)
|
||||
{
|
||||
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_DiscoveringCalDav });
|
||||
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_TestingCalendarAuth });
|
||||
}
|
||||
|
||||
if (setupResult?.IsCalendarAccessGranted == true)
|
||||
{
|
||||
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_FetchingCalendarMetadata });
|
||||
}
|
||||
|
||||
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_Finalizing });
|
||||
}
|
||||
}
|
||||
|
||||
private int _currentStepIndex;
|
||||
|
||||
private void SetStepInProgress(string title)
|
||||
{
|
||||
for (int i = 0; i < Steps.Count; i++)
|
||||
{
|
||||
if (Steps[i].Title == title)
|
||||
{
|
||||
_currentStepIndex = i;
|
||||
Steps[i].Status = AccountSetupStepStatus.InProgress;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetCurrentStepSucceeded()
|
||||
{
|
||||
if (_currentStepIndex < Steps.Count)
|
||||
Steps[_currentStepIndex].Status = AccountSetupStepStatus.Succeeded;
|
||||
}
|
||||
|
||||
private void SetCurrentStepFailed(string errorMessage)
|
||||
{
|
||||
if (_currentStepIndex < Steps.Count)
|
||||
{
|
||||
Steps[_currentStepIndex].Status = AccountSetupStepStatus.Failed;
|
||||
Steps[_currentStepIndex].ErrorMessage = errorMessage;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunSetupAsync()
|
||||
{
|
||||
IsSetupComplete = false;
|
||||
IsSetupFailed = false;
|
||||
FailureMessage = null;
|
||||
_dbWritten = false;
|
||||
_createdAccount = null;
|
||||
|
||||
BuildSteps();
|
||||
|
||||
try
|
||||
{
|
||||
CustomServerInformation customServerInformation = null;
|
||||
|
||||
// Build account in memory
|
||||
_createdAccount = new MailAccount
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ProviderType = WizardContext.SelectedProvider.Type,
|
||||
Name = WizardContext.AccountName,
|
||||
SpecialImapProvider = WizardContext.SelectedProvider.SpecialImapProvider,
|
||||
AccountColorHex = WizardContext.AccountColorHex,
|
||||
IsCalendarAccessGranted = true
|
||||
};
|
||||
|
||||
if (WizardContext.IsOAuthProvider)
|
||||
{
|
||||
// Step: Authenticating
|
||||
SetStepInProgress(string.Format(Translator.AccountSetup_Step_Authenticating, WizardContext.SelectedProvider.Name));
|
||||
|
||||
var authTokenInfo = await SynchronizationManager.Instance.HandleAuthorizationAsync(
|
||||
WizardContext.SelectedProvider.Type,
|
||||
_createdAccount,
|
||||
_createdAccount.ProviderType == MailProviderType.Gmail);
|
||||
|
||||
_createdAccount.Address = authTokenInfo.AccountAddress;
|
||||
SetCurrentStepSucceeded();
|
||||
|
||||
// Step: Save to DB
|
||||
SetStepInProgress(Translator.AccountSetup_Step_SavingAccount);
|
||||
await _accountService.CreateAccountAsync(_createdAccount, null);
|
||||
_dbWritten = true;
|
||||
SetCurrentStepSucceeded();
|
||||
|
||||
// Step: Profile
|
||||
SetStepInProgress(Translator.AccountSetup_Step_FetchingProfile);
|
||||
var profileResult = await SynchronizationManager.Instance.SynchronizeProfileAsync(_createdAccount.Id);
|
||||
if (profileResult.CompletedState != SynchronizationCompletedState.Success)
|
||||
throw new Exception(Translator.Exception_FailedToSynchronizeProfileInformation);
|
||||
|
||||
if (profileResult.ProfileInformation != null)
|
||||
{
|
||||
_createdAccount.SenderName = profileResult.ProfileInformation.SenderName;
|
||||
_createdAccount.Base64ProfilePictureData = profileResult.ProfileInformation.Base64ProfilePictureData;
|
||||
|
||||
if (!string.IsNullOrEmpty(profileResult.ProfileInformation.AccountAddress))
|
||||
_createdAccount.Address = profileResult.ProfileInformation.AccountAddress;
|
||||
|
||||
await _accountService.UpdateProfileInformationAsync(_createdAccount.Id, profileResult.ProfileInformation);
|
||||
}
|
||||
SetCurrentStepSucceeded();
|
||||
|
||||
// Step: Folders
|
||||
SetStepInProgress(Translator.AccountSetup_Step_SyncingFolders);
|
||||
var folderResult = await SynchronizationManager.Instance.SynchronizeFoldersAsync(_createdAccount.Id);
|
||||
if (folderResult == null || folderResult.CompletedState != SynchronizationCompletedState.Success)
|
||||
throw new Exception(Translator.Exception_FailedToSynchronizeFolders);
|
||||
SetCurrentStepSucceeded();
|
||||
|
||||
// Step: Calendar metadata
|
||||
SetStepInProgress(Translator.AccountSetup_Step_FetchingCalendarMetadata);
|
||||
if (_createdAccount.IsCalendarAccessGranted)
|
||||
{
|
||||
var calResult = await SynchronizationManager.Instance.SynchronizeCalendarAsync(new CalendarSynchronizationOptions
|
||||
{
|
||||
AccountId = _createdAccount.Id,
|
||||
Type = CalendarSynchronizationType.CalendarMetadata
|
||||
});
|
||||
if (calResult == null || calResult.CompletedState != SynchronizationCompletedState.Success)
|
||||
throw new Exception(Translator.Exception_FailedToSynchronizeCalendarMetadata);
|
||||
}
|
||||
SetCurrentStepSucceeded();
|
||||
|
||||
// Step: Aliases
|
||||
SetStepInProgress(Translator.AccountSetup_Step_SyncingAliases);
|
||||
if (_createdAccount.IsAliasSyncSupported)
|
||||
{
|
||||
var aliasResult = await SynchronizationManager.Instance.SynchronizeAliasesAsync(_createdAccount.Id);
|
||||
if (aliasResult.CompletedState != SynchronizationCompletedState.Success)
|
||||
throw new Exception(Translator.Exception_FailedToSynchronizeAliases);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _accountService.CreateRootAliasAsync(_createdAccount.Id, _createdAccount.Address);
|
||||
}
|
||||
SetCurrentStepSucceeded();
|
||||
}
|
||||
else if (WizardContext.IsSpecialImapProvider)
|
||||
{
|
||||
var dialogResult = WizardContext.BuildAccountCreationDialogResult();
|
||||
|
||||
customServerInformation = _specialImapProviderConfigResolver.GetServerInformation(_createdAccount, dialogResult);
|
||||
if (customServerInformation == null) throw new Exception("Failed to resolve server information.");
|
||||
|
||||
customServerInformation.Id = Guid.NewGuid();
|
||||
customServerInformation.AccountId = _createdAccount.Id;
|
||||
|
||||
_createdAccount.Address = WizardContext.EmailAddress;
|
||||
_createdAccount.SenderName = WizardContext.DisplayName;
|
||||
_createdAccount.IsCalendarAccessGranted = customServerInformation.CalendarSupportMode != ImapCalendarSupportMode.Disabled;
|
||||
_createdAccount.ServerInformation = customServerInformation;
|
||||
|
||||
// Step: Test IMAP
|
||||
SetStepInProgress(Translator.AccountSetup_Step_TestingMailAuth);
|
||||
await ValidateImapConnectivityAsync(customServerInformation);
|
||||
SetCurrentStepSucceeded();
|
||||
|
||||
// Step: CalDAV discovery and testing (if applicable)
|
||||
if (customServerInformation.CalendarSupportMode == ImapCalendarSupportMode.CalDav)
|
||||
{
|
||||
SetStepInProgress(Translator.AccountSetup_Step_DiscoveringCalDav);
|
||||
SetCurrentStepSucceeded();
|
||||
|
||||
SetStepInProgress(Translator.AccountSetup_Step_TestingCalendarAuth);
|
||||
await ValidateCalDavConnectivityAsync(customServerInformation);
|
||||
SetCurrentStepSucceeded();
|
||||
}
|
||||
|
||||
// Step: Save to DB
|
||||
SetStepInProgress(Translator.AccountSetup_Step_SavingAccount);
|
||||
await _accountService.CreateAccountAsync(_createdAccount, customServerInformation);
|
||||
_dbWritten = true;
|
||||
SetCurrentStepSucceeded();
|
||||
|
||||
// Step: Folders
|
||||
SetStepInProgress(Translator.AccountSetup_Step_SyncingFolders);
|
||||
var folderResult = await SynchronizationManager.Instance.SynchronizeFoldersAsync(_createdAccount.Id);
|
||||
if (folderResult == null || folderResult.CompletedState != SynchronizationCompletedState.Success)
|
||||
throw new Exception(Translator.Exception_FailedToSynchronizeFolders);
|
||||
SetCurrentStepSucceeded();
|
||||
|
||||
// Step: Calendar metadata (if not disabled)
|
||||
if (_createdAccount.IsCalendarAccessGranted)
|
||||
{
|
||||
SetStepInProgress(Translator.AccountSetup_Step_FetchingCalendarMetadata);
|
||||
var calResult = await SynchronizationManager.Instance.SynchronizeCalendarAsync(new CalendarSynchronizationOptions
|
||||
{
|
||||
AccountId = _createdAccount.Id,
|
||||
Type = CalendarSynchronizationType.CalendarMetadata
|
||||
});
|
||||
if (calResult == null || calResult.CompletedState != SynchronizationCompletedState.Success)
|
||||
throw new Exception(Translator.Exception_FailedToSynchronizeCalendarMetadata);
|
||||
SetCurrentStepSucceeded();
|
||||
}
|
||||
|
||||
// Aliases for IMAP
|
||||
await _accountService.CreateRootAliasAsync(_createdAccount.Id, _createdAccount.Address);
|
||||
}
|
||||
else // Generic IMAP
|
||||
{
|
||||
var setupResult = WizardContext.ImapCalDavSetupResult
|
||||
?? throw new Exception("IMAP setup was not completed.");
|
||||
|
||||
customServerInformation = setupResult.ServerInformation
|
||||
?? throw new Exception("Server information is missing.");
|
||||
|
||||
customServerInformation.Id = Guid.NewGuid();
|
||||
customServerInformation.AccountId = _createdAccount.Id;
|
||||
|
||||
_createdAccount.Address = setupResult.EmailAddress;
|
||||
_createdAccount.SenderName = setupResult.DisplayName;
|
||||
_createdAccount.IsCalendarAccessGranted = setupResult.IsCalendarAccessGranted;
|
||||
_createdAccount.ServerInformation = customServerInformation;
|
||||
|
||||
// Step: Save to DB
|
||||
SetStepInProgress(Translator.AccountSetup_Step_SavingAccount);
|
||||
await _accountService.CreateAccountAsync(_createdAccount, customServerInformation);
|
||||
_dbWritten = true;
|
||||
SetCurrentStepSucceeded();
|
||||
|
||||
// Step: Folders
|
||||
SetStepInProgress(Translator.AccountSetup_Step_SyncingFolders);
|
||||
var folderResult = await SynchronizationManager.Instance.SynchronizeFoldersAsync(_createdAccount.Id);
|
||||
if (folderResult == null || folderResult.CompletedState != SynchronizationCompletedState.Success)
|
||||
throw new Exception(Translator.Exception_FailedToSynchronizeFolders);
|
||||
SetCurrentStepSucceeded();
|
||||
|
||||
// Step: CalDAV (if applicable)
|
||||
if (setupResult.IsCalendarAccessGranted &&
|
||||
customServerInformation.CalendarSupportMode == ImapCalendarSupportMode.CalDav)
|
||||
{
|
||||
SetStepInProgress(Translator.AccountSetup_Step_DiscoveringCalDav);
|
||||
SetCurrentStepSucceeded();
|
||||
|
||||
SetStepInProgress(Translator.AccountSetup_Step_TestingCalendarAuth);
|
||||
await ValidateCalDavConnectivityAsync(customServerInformation);
|
||||
SetCurrentStepSucceeded();
|
||||
}
|
||||
|
||||
// Step: Calendar metadata
|
||||
if (setupResult.IsCalendarAccessGranted)
|
||||
{
|
||||
SetStepInProgress(Translator.AccountSetup_Step_FetchingCalendarMetadata);
|
||||
var calResult = await SynchronizationManager.Instance.SynchronizeCalendarAsync(new CalendarSynchronizationOptions
|
||||
{
|
||||
AccountId = _createdAccount.Id,
|
||||
Type = CalendarSynchronizationType.CalendarMetadata
|
||||
});
|
||||
if (calResult == null || calResult.CompletedState != SynchronizationCompletedState.Success)
|
||||
throw new Exception(Translator.Exception_FailedToSynchronizeCalendarMetadata);
|
||||
SetCurrentStepSucceeded();
|
||||
}
|
||||
|
||||
// Aliases for IMAP
|
||||
await _accountService.CreateRootAliasAsync(_createdAccount.Id, _createdAccount.Address);
|
||||
}
|
||||
|
||||
// Step: Finalizing
|
||||
SetStepInProgress(Translator.AccountSetup_Step_Finalizing);
|
||||
SetCurrentStepSucceeded();
|
||||
|
||||
IsSetupComplete = true;
|
||||
|
||||
// Notify listeners — this triggers ShellWindow creation from App.xaml.cs
|
||||
Messenger.Send(new AccountCreatedMessage(_createdAccount));
|
||||
}
|
||||
catch (AccountSetupCanceledException)
|
||||
{
|
||||
// User canceled authentication — go back silently, no error UI
|
||||
Messenger.Send(new BackBreadcrumNavigationRequested(NavigationTransitionEffect.FromLeft));
|
||||
}
|
||||
catch (Exception ex) when (ex.Message.Contains(nameof(AccountSetupCanceledException)))
|
||||
{
|
||||
// Wrapped cancellation — same silent behavior
|
||||
Messenger.Send(new BackBreadcrumNavigationRequested(NavigationTransitionEffect.FromLeft));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Account setup failed.");
|
||||
|
||||
SetCurrentStepFailed(ex.Message);
|
||||
IsSetupFailed = true;
|
||||
FailureMessage = Translator.AccountSetup_FailureMessage;
|
||||
|
||||
// Rollback if DB write happened
|
||||
if (_dbWritten && _createdAccount != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _accountService.DeleteAccountAsync(_createdAccount);
|
||||
}
|
||||
catch (Exception deleteEx)
|
||||
{
|
||||
Log.Error(deleteEx, "Failed to rollback account creation.");
|
||||
}
|
||||
|
||||
_dbWritten = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ValidateImapConnectivityAsync(CustomServerInformation serverInformation)
|
||||
{
|
||||
var connectivityResult = await SynchronizationManager.Instance
|
||||
.TestImapConnectivityAsync(serverInformation, allowSSLHandshake: false);
|
||||
|
||||
if (connectivityResult.IsCertificateUIRequired)
|
||||
{
|
||||
var certificateMessage =
|
||||
$"{Translator.IMAPSetupDialog_CertificateAllowanceRequired_Row0}\n\n" +
|
||||
$"{Translator.IMAPSetupDialog_CertificateIssuer}: {connectivityResult.CertificateIssuer}\n" +
|
||||
$"{Translator.IMAPSetupDialog_CertificateValidFrom}: {connectivityResult.CertificateValidFromDateString}\n" +
|
||||
$"{Translator.IMAPSetupDialog_CertificateValidTo}: {connectivityResult.CertificateExpirationDateString}\n\n" +
|
||||
$"{Translator.IMAPSetupDialog_CertificateAllowanceRequired_Row1}";
|
||||
|
||||
var allowCertificate = await _dialogService.ShowConfirmationDialogAsync(
|
||||
certificateMessage,
|
||||
Translator.GeneralTitle_Warning,
|
||||
Translator.Buttons_Allow);
|
||||
|
||||
if (!allowCertificate)
|
||||
throw new InvalidOperationException(Translator.IMAPSetupDialog_CertificateDenied);
|
||||
|
||||
connectivityResult = await SynchronizationManager.Instance
|
||||
.TestImapConnectivityAsync(serverInformation, allowSSLHandshake: true);
|
||||
}
|
||||
|
||||
if (!connectivityResult.IsSuccess)
|
||||
throw new InvalidOperationException(connectivityResult.FailedReason ?? Translator.IMAPSetupDialog_ConnectionFailedMessage);
|
||||
}
|
||||
|
||||
private async Task ValidateCalDavConnectivityAsync(CustomServerInformation serverInformation)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(serverInformation.CalDavServiceUrl))
|
||||
throw new InvalidOperationException(Translator.ImapCalDavSettingsPage_CalDavUrlRequired);
|
||||
|
||||
var settings = new CalDavConnectionSettings
|
||||
{
|
||||
ServiceUri = new Uri(serverInformation.CalDavServiceUrl, UriKind.Absolute),
|
||||
Username = serverInformation.CalDavUsername,
|
||||
Password = serverInformation.CalDavPassword
|
||||
};
|
||||
|
||||
await _calDavClient.DiscoverCalendarsAsync(settings);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void GoBack()
|
||||
{
|
||||
Messenger.Send(new BackBreadcrumNavigationRequested(NavigationTransitionEffect.FromLeft));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task TryAgainAsync()
|
||||
{
|
||||
await RunSetupAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
@@ -12,7 +13,7 @@ using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
using Wino.Messaging.Server;
|
||||
using Wino.Core.Services;
|
||||
|
||||
namespace Wino.Mail.ViewModels;
|
||||
|
||||
@@ -20,7 +21,7 @@ public partial class AliasManagementPageViewModel : MailBaseViewModel
|
||||
{
|
||||
private readonly IMailDialogService _dialogService;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly IWinoServerConnectionManager _winoServerConnectionManager;
|
||||
private readonly ISmimeCertificateService _smimeCertificateService;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(CanSynchronizeAliases))]
|
||||
@@ -33,11 +34,11 @@ public partial class AliasManagementPageViewModel : MailBaseViewModel
|
||||
|
||||
public AliasManagementPageViewModel(IMailDialogService dialogService,
|
||||
IAccountService accountService,
|
||||
IWinoServerConnectionManager winoServerConnectionManager)
|
||||
ISmimeCertificateService smimeCertificateService)
|
||||
{
|
||||
_dialogService = dialogService;
|
||||
_accountService = accountService;
|
||||
_winoServerConnectionManager = winoServerConnectionManager;
|
||||
_smimeCertificateService = smimeCertificateService;
|
||||
}
|
||||
|
||||
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
@@ -54,7 +55,22 @@ public partial class AliasManagementPageViewModel : MailBaseViewModel
|
||||
|
||||
private async Task LoadAliasesAsync()
|
||||
{
|
||||
AccountAliases = await _accountService.GetAccountAliasesAsync(Account.Id);
|
||||
var aliases = await _accountService.GetAccountAliasesAsync(Account.Id);
|
||||
foreach (var alias in aliases)
|
||||
{
|
||||
alias.Certificates.Clear();
|
||||
alias.Certificates.Add(null); // First blank optioon
|
||||
var certs = _smimeCertificateService.GetCertificates()
|
||||
.Where(cert => cert.Subject.Contains(alias.AliasAddress, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
foreach (var cert in certs)
|
||||
alias.Certificates.Add(cert);
|
||||
|
||||
alias.SelectedSigningCertificate = !string.IsNullOrEmpty(alias.SelectedSigningCertificateThumbprint)
|
||||
? alias.Certificates.FirstOrDefault(c => c?.Thumbprint == alias.SelectedSigningCertificateThumbprint)
|
||||
: null;
|
||||
}
|
||||
AccountAliases = aliases;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -82,12 +98,12 @@ public partial class AliasManagementPageViewModel : MailBaseViewModel
|
||||
Type = MailSynchronizationType.Alias
|
||||
};
|
||||
|
||||
var aliasSyncResponse = await _winoServerConnectionManager.GetResponseAsync<MailSynchronizationResult, NewMailSynchronizationRequested>(new NewMailSynchronizationRequested(aliasSyncOptions, SynchronizationSource.Client));
|
||||
var aliasSyncResult = await SynchronizationManager.Instance.SynchronizeAliasesAsync(Account.Id);
|
||||
|
||||
if (aliasSyncResponse.IsSuccess)
|
||||
if (aliasSyncResult.CompletedState == SynchronizationCompletedState.Success)
|
||||
await LoadAliasesAsync();
|
||||
else
|
||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error, aliasSyncResponse.Message, InfoBarMessageType.Error);
|
||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error, "Failed to synchronize aliases", InfoBarMessageType.Error);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -151,4 +167,20 @@ public partial class AliasManagementPageViewModel : MailBaseViewModel
|
||||
await _accountService.DeleteAccountAliasAsync(alias.Id);
|
||||
await LoadAliasesAsync();
|
||||
}
|
||||
|
||||
public async Task SetAliasSmimeEncryption(MailAccountAlias alias, bool value)
|
||||
{
|
||||
alias.IsSmimeEncryptionEnabled = value;
|
||||
await _accountService.UpdateAccountAliasesAsync(Account.Id, AccountAliases);
|
||||
await LoadAliasesAsync();
|
||||
}
|
||||
|
||||
public async Task SetSelectedSigningCertificate(MailAccountAlias alias, X509Certificate2 cert)
|
||||
{
|
||||
alias.SelectedSigningCertificate = cert;
|
||||
alias.SelectedSigningCertificateThumbprint = cert?.Thumbprint;
|
||||
|
||||
await _accountService.UpdateAccountAliasesAsync(Account.Id, AccountAliases);
|
||||
await LoadAliasesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,92 +1,31 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
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.Ai;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Messaging.Server;
|
||||
using Wino.Core.Domain.Models.Translations;
|
||||
|
||||
namespace Wino.Mail.ViewModels;
|
||||
|
||||
public partial class AppPreferencesPageViewModel : MailBaseViewModel
|
||||
{
|
||||
public IPreferencesService PreferencesService { get; }
|
||||
|
||||
[ObservableProperty]
|
||||
private List<string> _appTerminationBehavior;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial List<string> SearchModes { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsStartupBehaviorDisabled))]
|
||||
[NotifyPropertyChangedFor(nameof(IsStartupBehaviorEnabled))]
|
||||
private StartupBehaviorResult startupBehaviorResult;
|
||||
|
||||
private int _emailSyncIntervalMinutes;
|
||||
public int EmailSyncIntervalMinutes
|
||||
{
|
||||
get => _emailSyncIntervalMinutes;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _emailSyncIntervalMinutes, value);
|
||||
|
||||
PreferencesService.EmailSyncIntervalMinutes = value;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsStartupBehaviorDisabled => !IsStartupBehaviorEnabled;
|
||||
public bool IsStartupBehaviorEnabled => StartupBehaviorResult == StartupBehaviorResult.Enabled;
|
||||
|
||||
private string _selectedAppTerminationBehavior;
|
||||
public string SelectedAppTerminationBehavior
|
||||
{
|
||||
get => _selectedAppTerminationBehavior;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _selectedAppTerminationBehavior, value);
|
||||
|
||||
PreferencesService.ServerTerminationBehavior = (ServerBackgroundMode)AppTerminationBehavior.IndexOf(value);
|
||||
}
|
||||
}
|
||||
|
||||
private string _selectedDefaultSearchMode;
|
||||
public string SelectedDefaultSearchMode
|
||||
{
|
||||
get => _selectedDefaultSearchMode;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _selectedDefaultSearchMode, value);
|
||||
|
||||
PreferencesService.DefaultSearchMode = (SearchMode)SearchModes.IndexOf(value);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly IMailDialogService _dialogService;
|
||||
private readonly IWinoServerConnectionManager _winoServerConnectionManager;
|
||||
private readonly IStartupBehaviorService _startupBehaviorService;
|
||||
|
||||
public AppPreferencesPageViewModel(IMailDialogService dialogService,
|
||||
IPreferencesService preferencesService,
|
||||
IWinoServerConnectionManager winoServerConnectionManager,
|
||||
IStartupBehaviorService startupBehaviorService)
|
||||
public AppPreferencesPageViewModel(
|
||||
IMailDialogService dialogService,
|
||||
IPreferencesService preferencesService,
|
||||
IStartupBehaviorService startupBehaviorService,
|
||||
ITranslationService translationService,
|
||||
IAiActionOptionsService aiActionOptionsService)
|
||||
{
|
||||
_dialogService = dialogService;
|
||||
PreferencesService = preferencesService;
|
||||
_winoServerConnectionManager = winoServerConnectionManager;
|
||||
_startupBehaviorService = startupBehaviorService;
|
||||
|
||||
// Load the app termination behavior options
|
||||
|
||||
_appTerminationBehavior =
|
||||
[
|
||||
Translator.SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Title, // "Minimize to tray"
|
||||
Translator.SettingsAppPreferences_ServerBackgroundingMode_Invisible_Title, // "Invisible"
|
||||
Translator.SettingsAppPreferences_ServerBackgroundingMode_Terminate_Title // "Terminate"
|
||||
];
|
||||
_translationService = translationService;
|
||||
_aiActionOptionsService = aiActionOptionsService;
|
||||
|
||||
SearchModes =
|
||||
[
|
||||
@@ -94,22 +33,137 @@ public partial class AppPreferencesPageViewModel : MailBaseViewModel
|
||||
Translator.SettingsAppPreferences_SearchMode_Online
|
||||
];
|
||||
|
||||
SelectedAppTerminationBehavior = _appTerminationBehavior[(int)PreferencesService.ServerTerminationBehavior];
|
||||
ApplicationModes =
|
||||
[
|
||||
Translator.SettingsAppPreferences_ApplicationMode_Mail,
|
||||
Translator.SettingsAppPreferences_ApplicationMode_Calendar,
|
||||
Translator.ContactsPage_Title,
|
||||
Translator.MenuSettings
|
||||
];
|
||||
|
||||
SelectedDefaultSearchMode = SearchModes[(int)PreferencesService.DefaultSearchMode];
|
||||
SelectedDefaultApplicationMode = ApplicationModes[(int)PreferencesService.DefaultApplicationMode];
|
||||
EmailSyncIntervalMinutes = PreferencesService.EmailSyncIntervalMinutes;
|
||||
SummarySavePath = PreferencesService.AiSummarySavePath;
|
||||
}
|
||||
|
||||
public IPreferencesService PreferencesService { get; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial List<string> SearchModes { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial List<string> ApplicationModes { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial List<AppLanguageModel> AvailableLanguages { get; set; } = [];
|
||||
|
||||
[ObservableProperty]
|
||||
public partial AppLanguageModel SelectedLanguage { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial List<AiTranslateLanguageOption> AvailableAiLanguages { get; set; } = [];
|
||||
|
||||
[ObservableProperty]
|
||||
public partial AiTranslateLanguageOption SelectedDefaultTranslationLanguage { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial AiTranslateLanguageOption SelectedSummarizeLanguage { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string SummarySavePath { get; set; } = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool HasInvalidSummarySavePath { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsStartupBehaviorDisabled))]
|
||||
[NotifyPropertyChangedFor(nameof(IsStartupBehaviorEnabled))]
|
||||
private StartupBehaviorResult startupBehaviorResult;
|
||||
|
||||
private readonly IMailDialogService _dialogService;
|
||||
private readonly IStartupBehaviorService _startupBehaviorService;
|
||||
private readonly ITranslationService _translationService;
|
||||
private readonly IAiActionOptionsService _aiActionOptionsService;
|
||||
private bool _isLanguageInitialized;
|
||||
private bool _isAiPreferencesInitialized;
|
||||
private int _emailSyncIntervalMinutes;
|
||||
private string _selectedDefaultSearchMode;
|
||||
private string _selectedDefaultApplicationMode;
|
||||
|
||||
public int EmailSyncIntervalMinutes
|
||||
{
|
||||
get => _emailSyncIntervalMinutes;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _emailSyncIntervalMinutes, value);
|
||||
PreferencesService.EmailSyncIntervalMinutes = value;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsStartupBehaviorDisabled => !IsStartupBehaviorEnabled;
|
||||
public bool IsStartupBehaviorEnabled => StartupBehaviorResult == StartupBehaviorResult.Enabled;
|
||||
|
||||
public string SelectedDefaultSearchMode
|
||||
{
|
||||
get => _selectedDefaultSearchMode;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _selectedDefaultSearchMode, value);
|
||||
PreferencesService.DefaultSearchMode = (SearchMode)SearchModes.IndexOf(value);
|
||||
}
|
||||
}
|
||||
|
||||
public string SelectedDefaultApplicationMode
|
||||
{
|
||||
get => _selectedDefaultApplicationMode;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _selectedDefaultApplicationMode, value);
|
||||
PreferencesService.DefaultApplicationMode = (WinoApplicationMode)ApplicationModes.IndexOf(value);
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnSelectedLanguageChanged(AppLanguageModel value)
|
||||
{
|
||||
if (!_isLanguageInitialized || value == null)
|
||||
return;
|
||||
|
||||
_ = _translationService.InitializeLanguageAsync(value.Language);
|
||||
}
|
||||
|
||||
partial void OnSelectedDefaultTranslationLanguageChanged(AiTranslateLanguageOption value)
|
||||
{
|
||||
if (!_isAiPreferencesInitialized || value == null)
|
||||
return;
|
||||
|
||||
PreferencesService.AiDefaultTranslationLanguageCode = value.Code;
|
||||
}
|
||||
|
||||
partial void OnSelectedSummarizeLanguageChanged(AiTranslateLanguageOption value)
|
||||
{
|
||||
if (!_isAiPreferencesInitialized || value == null)
|
||||
return;
|
||||
|
||||
PreferencesService.AiSummarizeLanguageCode = value.Code;
|
||||
}
|
||||
|
||||
partial void OnSummarySavePathChanged(string value)
|
||||
{
|
||||
if (!_isAiPreferencesInitialized)
|
||||
return;
|
||||
|
||||
PreferencesService.AiSummarySavePath = value ?? string.Empty;
|
||||
RefreshSummarySavePathState();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ToggleStartupBehaviorAsync()
|
||||
{
|
||||
if (IsStartupBehaviorEnabled)
|
||||
{
|
||||
await DisableStartupAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
await EnableStartupAsync();
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(IsStartupBehaviorEnabled));
|
||||
}
|
||||
@@ -117,14 +171,12 @@ public partial class AppPreferencesPageViewModel : MailBaseViewModel
|
||||
private async Task EnableStartupAsync()
|
||||
{
|
||||
StartupBehaviorResult = await _startupBehaviorService.ToggleStartupBehavior(true);
|
||||
|
||||
NotifyCurrentStartupState();
|
||||
}
|
||||
|
||||
private async Task DisableStartupAsync()
|
||||
{
|
||||
StartupBehaviorResult = await _startupBehaviorService.ToggleStartupBehavior(false);
|
||||
|
||||
NotifyCurrentStartupState();
|
||||
}
|
||||
|
||||
@@ -152,25 +204,56 @@ public partial class AppPreferencesPageViewModel : MailBaseViewModel
|
||||
}
|
||||
}
|
||||
|
||||
protected override async void OnPropertyChanged(PropertyChangedEventArgs e)
|
||||
[RelayCommand]
|
||||
private async Task BrowseSummarySavePathAsync()
|
||||
{
|
||||
base.OnPropertyChanged(e);
|
||||
var pickedPath = await _dialogService.PickWindowsFolderAsync();
|
||||
if (string.IsNullOrWhiteSpace(pickedPath))
|
||||
return;
|
||||
|
||||
if (e.PropertyName == nameof(SelectedAppTerminationBehavior))
|
||||
{
|
||||
var terminationModeChangedResult = await _winoServerConnectionManager.GetResponseAsync<bool, ServerTerminationModeChanged>(new ServerTerminationModeChanged(PreferencesService.ServerTerminationBehavior));
|
||||
|
||||
if (!terminationModeChangedResult.IsSuccess)
|
||||
{
|
||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error, terminationModeChangedResult.Message, InfoBarMessageType.Error);
|
||||
}
|
||||
}
|
||||
await ExecuteUIThread(() => SummarySavePath = pickedPath);
|
||||
}
|
||||
|
||||
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedTo(mode, parameters);
|
||||
|
||||
StartupBehaviorResult = await _startupBehaviorService.GetCurrentStartupBehaviorAsync();
|
||||
var availableLanguages = _translationService.GetAvailableLanguages();
|
||||
var availableAiLanguages = new List<AiTranslateLanguageOption>(_aiActionOptionsService.GetTranslateLanguageOptions());
|
||||
var startupBehaviorResult = await _startupBehaviorService.GetCurrentStartupBehaviorAsync();
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
AvailableLanguages = availableLanguages;
|
||||
SelectedLanguage = AvailableLanguages.Find(language => language.Language == PreferencesService.CurrentLanguage)
|
||||
?? (AvailableLanguages.Count > 0 ? AvailableLanguages[0] : null);
|
||||
_isLanguageInitialized = true;
|
||||
|
||||
AvailableAiLanguages = availableAiLanguages;
|
||||
SelectedDefaultTranslationLanguage = FindAiLanguageOption(PreferencesService.AiDefaultTranslationLanguageCode)
|
||||
?? FindAiLanguageOption("en-US")
|
||||
?? (AvailableAiLanguages.Count > 0 ? AvailableAiLanguages[0] : null);
|
||||
SelectedSummarizeLanguage = FindAiLanguageOption(PreferencesService.AiSummarizeLanguageCode)
|
||||
?? FindAiLanguageOption("en-US")
|
||||
?? (AvailableAiLanguages.Count > 0 ? AvailableAiLanguages[0] : null);
|
||||
SummarySavePath = PreferencesService.AiSummarySavePath;
|
||||
RefreshSummarySavePathState();
|
||||
_isAiPreferencesInitialized = true;
|
||||
|
||||
StartupBehaviorResult = startupBehaviorResult;
|
||||
});
|
||||
}
|
||||
|
||||
private AiTranslateLanguageOption FindAiLanguageOption(string languageCode)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(languageCode))
|
||||
return null;
|
||||
|
||||
return AvailableAiLanguages.Find(option => option.Code == languageCode);
|
||||
}
|
||||
|
||||
private void RefreshSummarySavePathState()
|
||||
{
|
||||
HasInvalidSummarySavePath = !string.IsNullOrWhiteSpace(SummarySavePath) && !Directory.Exists(SummarySavePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for group headers in the flat collection
|
||||
/// </summary>
|
||||
public abstract partial class GroupHeaderBase : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
private int itemCount;
|
||||
|
||||
[ObservableProperty]
|
||||
private int unreadCount;
|
||||
|
||||
protected GroupHeaderBase(string key, string displayName)
|
||||
{
|
||||
Key = key;
|
||||
DisplayName = displayName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The unique key for this group (used for sorting and identification)
|
||||
/// </summary>
|
||||
public string Key { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The display name shown in the UI
|
||||
/// </summary>
|
||||
public string DisplayName { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Group header for date-based grouping
|
||||
/// </summary>
|
||||
public partial class DateGroupHeader : GroupHeaderBase
|
||||
{
|
||||
public DateGroupHeader(DateTime date) : base(date.ToString("yyyy-MM-dd"), FormatDisplayName(date))
|
||||
{
|
||||
Date = date;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The date this group represents
|
||||
/// </summary>
|
||||
public DateTime Date { get; }
|
||||
|
||||
private static string FormatDisplayName(DateTime date)
|
||||
{
|
||||
var today = DateTime.Today;
|
||||
var yesterday = today.AddDays(-1);
|
||||
|
||||
return date.Date switch
|
||||
{
|
||||
var d when d == today => "Today",
|
||||
var d when d == yesterday => "Yesterday",
|
||||
var d when d >= today.AddDays(-7) => date.ToString("dddd"), // This week
|
||||
var d when d.Year == today.Year => date.ToString("MMMM dd"), // This year
|
||||
_ => date.ToString("MMMM dd, yyyy") // Other years
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Group header for sender name-based grouping
|
||||
/// </summary>
|
||||
public partial class SenderGroupHeader : GroupHeaderBase
|
||||
{
|
||||
public SenderGroupHeader(string senderName) : base(senderName, senderName)
|
||||
{
|
||||
SenderName = senderName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The sender name this group represents
|
||||
/// </summary>
|
||||
public string SenderName { get; }
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Collections;
|
||||
|
||||
internal class ThreadingManager
|
||||
{
|
||||
private readonly IThreadingStrategyProvider _threadingStrategyProvider;
|
||||
|
||||
public ThreadingManager(IThreadingStrategyProvider threadingStrategyProvider)
|
||||
{
|
||||
_threadingStrategyProvider = threadingStrategyProvider;
|
||||
}
|
||||
|
||||
public bool ShouldThread(MailCopy newItem, IMailItem existingItem)
|
||||
{
|
||||
if (_threadingStrategyProvider == null) return false;
|
||||
|
||||
var strategy = _threadingStrategyProvider.GetStrategy(newItem.AssignedAccount.ProviderType);
|
||||
return strategy?.ShouldThreadWithItem(newItem, existingItem) ?? false;
|
||||
}
|
||||
|
||||
public ThreadMailItem CreateNewThread(IMailItem existingItem, MailCopy newItem)
|
||||
{
|
||||
var thread = new ThreadMailItem();
|
||||
thread.AddThreadItem(existingItem);
|
||||
thread.AddThreadItem(newItem);
|
||||
return thread;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,39 +3,64 @@ using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using MimeKit;
|
||||
using MimeKit.Cryptography;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Exceptions;
|
||||
using Wino.Core.Domain.Extensions;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.Extensions;
|
||||
using Wino.Core.Services;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
using Wino.Mail.ViewModels.Messages;
|
||||
using Wino.Messaging.Client.Mails;
|
||||
using Wino.Messaging.Server;
|
||||
using Wino.Messaging.UI;
|
||||
|
||||
namespace Wino.Mail.ViewModels;
|
||||
|
||||
public partial class ComposePageViewModel : MailBaseViewModel
|
||||
public partial class ComposePageViewModel : MailBaseViewModel,
|
||||
IRecipient<ReaderItemRefreshRequestedEvent>,
|
||||
IRecipient<SynchronizationActionsAdded>,
|
||||
IRecipient<SynchronizationActionsCompleted>,
|
||||
IRecipient<AccountSynchronizerStateChanged>
|
||||
{
|
||||
private static readonly TimeSpan LocalDraftRetryGracePeriod = TimeSpan.FromSeconds(15);
|
||||
|
||||
public Func<Task<string>> GetHTMLBodyFunction;
|
||||
|
||||
public override async Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args)
|
||||
{
|
||||
if (args.Handled || args.Mode != WinoApplicationMode.Mail)
|
||||
return;
|
||||
|
||||
if (args.Action == KeyboardShortcutAction.Send)
|
||||
{
|
||||
await SendAsync();
|
||||
args.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// When we send the message or discard it, we need to block the mime update
|
||||
// Update is triggered when we leave the page.
|
||||
private bool isUpdatingMimeBlocked = false;
|
||||
|
||||
private bool canSendMail => ComposingAccount != null && !IsLocalDraft && CurrentMimeMessage != null;
|
||||
private bool canSendMail => ComposingAccount != null && !IsLocalDraft && CurrentMimeMessage != null && !IsDraftBusy;
|
||||
private bool canSendLocalDraftToServer => ComposingAccount != null && IsLocalDraft && CurrentMimeMessage != null && !IsDraftBusy && !IsRetryingSendToServer;
|
||||
|
||||
[NotifyCanExecuteChangedFor(nameof(DiscardCommand))]
|
||||
[NotifyCanExecuteChangedFor(nameof(SendCommand))]
|
||||
[NotifyCanExecuteChangedFor(nameof(SendToServerCommand))]
|
||||
[ObservableProperty]
|
||||
private MimeMessage currentMimeMessage = null;
|
||||
|
||||
@@ -47,47 +72,72 @@ public partial class ComposePageViewModel : MailBaseViewModel
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsLocalDraft))]
|
||||
[NotifyPropertyChangedFor(nameof(ShouldShowSendToServerButton))]
|
||||
[NotifyPropertyChangedFor(nameof(ShouldShowSendButton))]
|
||||
[NotifyCanExecuteChangedFor(nameof(DiscardCommand))]
|
||||
[NotifyCanExecuteChangedFor(nameof(SendCommand))]
|
||||
private MailItemViewModel currentMailDraftItem;
|
||||
[NotifyCanExecuteChangedFor(nameof(SendToServerCommand))]
|
||||
public partial MailItemViewModel CurrentMailDraftItem { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isImportanceSelected;
|
||||
[NotifyPropertyChangedFor(nameof(ShouldShowSendToServerButton))]
|
||||
[NotifyCanExecuteChangedFor(nameof(DiscardCommand))]
|
||||
[NotifyCanExecuteChangedFor(nameof(SendCommand))]
|
||||
[NotifyCanExecuteChangedFor(nameof(SendToServerCommand))]
|
||||
public partial bool IsDraftBusy { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
private MessageImportance selectedMessageImportance;
|
||||
[NotifyCanExecuteChangedFor(nameof(SendToServerCommand))]
|
||||
public partial bool IsRetryingSendToServer { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isCCBCCVisible;
|
||||
public partial bool IsImportanceSelected { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
private string subject;
|
||||
public partial MessageImportance SelectedMessageImportance { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsCCBCCVisible { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string Subject { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(DiscardCommand))]
|
||||
[NotifyCanExecuteChangedFor(nameof(SendCommand))]
|
||||
private MailAccount composingAccount;
|
||||
[NotifyCanExecuteChangedFor(nameof(SendToServerCommand))]
|
||||
public partial MailAccount ComposingAccount { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
private List<MailAccountAlias> availableAliases;
|
||||
public partial List<MailAccountAlias> AvailableAliases { get; set; }
|
||||
[ObservableProperty]
|
||||
public partial MailAccountAlias SelectedAlias { get; set; }
|
||||
[ObservableProperty]
|
||||
public partial bool IsDraggingOverComposerGrid { get; set; }
|
||||
[ObservableProperty]
|
||||
public partial bool IsDraggingOverFilesDropZone { get; set; }
|
||||
[ObservableProperty]
|
||||
public partial bool IsDraggingOverImagesDropZone { get; set; }
|
||||
[ObservableProperty]
|
||||
public partial bool IsSmimeSignatureEnabled { get; set; }
|
||||
[ObservableProperty]
|
||||
public partial bool IsSmimeEncryptionEnabled { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
private MailAccountAlias selectedAlias;
|
||||
public partial X509Certificate2 SelectedSigningCertificate { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isDraggingOverComposerGrid;
|
||||
public ObservableCollection<X509Certificate2> AvailableCertificates = [];
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isDraggingOverFilesDropZone;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isDraggingOverImagesDropZone;
|
||||
public bool AreCertificatesAvailable => AvailableCertificates.Count > 0;
|
||||
|
||||
public ObservableCollection<EmailTemplate> AvailableEmailTemplates { get; } = [];
|
||||
public ObservableCollection<MailAttachmentViewModel> IncludedAttachments { get; set; } = [];
|
||||
public ObservableCollection<MailAccount> Accounts { get; set; } = [];
|
||||
public ObservableCollection<AccountContact> ToItems { get; set; } = [];
|
||||
public ObservableCollection<AccountContact> CCItems { get; set; } = [];
|
||||
public ObservableCollection<AccountContact> BCCItems { get; set; } = [];
|
||||
public bool ShouldShowSendToServerButton => IsLocalDraft && !IsDraftBusy;
|
||||
public bool ShouldShowSendButton => !IsLocalDraft;
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -99,11 +149,12 @@ public partial class ComposePageViewModel : MailBaseViewModel
|
||||
private readonly IFileService _fileService;
|
||||
private readonly IFolderService _folderService;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly IEmailTemplateService _emailTemplateService;
|
||||
private readonly IWinoRequestDelegator _worker;
|
||||
public readonly IFontService FontService;
|
||||
public readonly IPreferencesService PreferencesService;
|
||||
private readonly IWinoServerConnectionManager _winoServerConnectionManager;
|
||||
public readonly IContactService ContactService;
|
||||
public readonly ISmimeCertificateService _smimeCertificateService;
|
||||
|
||||
public ComposePageViewModel(IMailDialogService dialogService,
|
||||
IMailService mailService,
|
||||
@@ -112,11 +163,12 @@ public partial class ComposePageViewModel : MailBaseViewModel
|
||||
INativeAppService nativeAppService,
|
||||
IFolderService folderService,
|
||||
IAccountService accountService,
|
||||
IEmailTemplateService emailTemplateService,
|
||||
IWinoRequestDelegator worker,
|
||||
IContactService contactService,
|
||||
IFontService fontService,
|
||||
IPreferencesService preferencesService,
|
||||
IWinoServerConnectionManager winoServerConnectionManager)
|
||||
ISmimeCertificateService smimeCertificateService)
|
||||
{
|
||||
NativeAppService = nativeAppService;
|
||||
ContactService = contactService;
|
||||
@@ -129,8 +181,40 @@ public partial class ComposePageViewModel : MailBaseViewModel
|
||||
_mimeFileService = mimeFileService;
|
||||
_fileService = fileService;
|
||||
_accountService = accountService;
|
||||
_emailTemplateService = emailTemplateService;
|
||||
_worker = worker;
|
||||
_winoServerConnectionManager = winoServerConnectionManager;
|
||||
_smimeCertificateService = smimeCertificateService;
|
||||
|
||||
foreach (var cert in _smimeCertificateService.GetCertificates(emailAddress: SelectedAlias?.AliasAddress))
|
||||
{
|
||||
if (cert != null)
|
||||
{
|
||||
AvailableCertificates.Add(cert);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnSelectedAliasChanged(MailAccountAlias value)
|
||||
{
|
||||
if (value != null)
|
||||
{
|
||||
IsSmimeSignatureEnabled = value.SelectedSigningCertificateThumbprint != null;
|
||||
IsSmimeEncryptionEnabled = value.IsSmimeEncryptionEnabled;
|
||||
|
||||
AvailableCertificates.Clear();
|
||||
var certs = _smimeCertificateService.GetCertificates(emailAddress: SelectedAlias.AliasAddress);
|
||||
foreach (var cert in certs)
|
||||
{
|
||||
AvailableCertificates.Add(cert);
|
||||
}
|
||||
SelectedSigningCertificate = AvailableCertificates
|
||||
.Where(c => c.Thumbprint == SelectedAlias.SelectedSigningCertificateThumbprint).FirstOrDefault() ?? AvailableCertificates.FirstOrDefault();
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnSelectedSigningCertificateChanged(X509Certificate2 value)
|
||||
{
|
||||
IsSmimeSignatureEnabled = value != null;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -214,25 +298,116 @@ public partial class ComposePageViewModel : MailBaseViewModel
|
||||
|
||||
isUpdatingMimeBlocked = true;
|
||||
|
||||
var assignedAccount = CurrentMailDraftItem.AssignedAccount;
|
||||
var assignedAccount = CurrentMailDraftItem.MailCopy.AssignedAccount;
|
||||
var sentFolder = await _folderService.GetSpecialFolderByAccountIdAsync(assignedAccount.Id, SpecialFolderType.Sent);
|
||||
|
||||
|
||||
// Load alias certs
|
||||
var certs = _smimeCertificateService.GetCertificates(emailAddress: SelectedAlias.AliasAddress);
|
||||
|
||||
if (IsSmimeSignatureEnabled)
|
||||
{
|
||||
var signingCertificate = !string.IsNullOrEmpty(SelectedAlias.SelectedSigningCertificateThumbprint)
|
||||
? certs.FirstOrDefault(c => c?.Thumbprint == SelectedAlias.SelectedSigningCertificateThumbprint)
|
||||
: null;
|
||||
|
||||
var signer = new CmsSigner(signingCertificate) { DigestAlgorithm = DigestAlgorithm.Sha1 };
|
||||
|
||||
if (IsSmimeEncryptionEnabled)
|
||||
{
|
||||
var recipients = new CmsRecipientCollection();
|
||||
var cmsRecipients = CurrentMimeMessage.To.Mailboxes
|
||||
.Select(mailbox => new CmsRecipient(
|
||||
_smimeCertificateService.GetCertificates(emailAddress: mailbox.Address).FirstOrDefault() ?? _smimeCertificateService.GetCertificates(StoreName.AddressBook, emailAddress: mailbox.Address).FirstOrDefault()
|
||||
));
|
||||
foreach (var recipient in cmsRecipients)
|
||||
{
|
||||
recipients.Add(recipient);
|
||||
}
|
||||
|
||||
CurrentMimeMessage.Body = ApplicationPkcs7Mime.SignAndEncrypt(signer, recipients, CurrentMimeMessage.Body);
|
||||
}
|
||||
else
|
||||
{
|
||||
// CurrentMimeMessage.Body = MultipartSigned.Create(signer, CurrentMimeMessage.Body);
|
||||
CurrentMimeMessage.Body = ApplicationPkcs7Mime.Sign(signer, CurrentMimeMessage.Body);
|
||||
}
|
||||
}
|
||||
else if (IsSmimeEncryptionEnabled)
|
||||
{
|
||||
// var encryptionCertificate = !string.IsNullOrEmpty(SelectedAlias.SelectedEncryptionCertificateThumbprint)
|
||||
// ? certs.FirstOrDefault(c => c?.Thumbprint == SelectedAlias.SelectedEncryptionCertificateThumbprint)
|
||||
// : null;
|
||||
// Encrypt the message if encryption certificate is selected.
|
||||
CurrentMimeMessage.Body = ApplicationPkcs7Mime.Encrypt(CurrentMimeMessage.To.Mailboxes, CurrentMimeMessage.Body);
|
||||
}
|
||||
|
||||
using MemoryStream memoryStream = new();
|
||||
CurrentMimeMessage.WriteTo(FormatOptions.Default, memoryStream);
|
||||
byte[] buffer = memoryStream.GetBuffer();
|
||||
int count = (int)memoryStream.Length;
|
||||
|
||||
var base64EncodedMessage = Convert.ToBase64String(buffer);
|
||||
var base64EncodedMessage = Convert.ToBase64String(memoryStream.ToArray());
|
||||
var draftSendPreparationRequest = new SendDraftPreparationRequest(CurrentMailDraftItem.MailCopy,
|
||||
SelectedAlias,
|
||||
sentFolder,
|
||||
CurrentMailDraftItem.AssignedFolder,
|
||||
CurrentMailDraftItem.AssignedAccount.Preferences,
|
||||
CurrentMailDraftItem.MailCopy.AssignedFolder,
|
||||
CurrentMailDraftItem.MailCopy.AssignedAccount.Preferences,
|
||||
base64EncodedMessage);
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
IsDraftBusy = true;
|
||||
});
|
||||
|
||||
await _worker.ExecuteAsync(draftSendPreparationRequest);
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(canSendLocalDraftToServer))]
|
||||
private async Task SendToServerAsync()
|
||||
{
|
||||
if (CurrentMailDraftItem?.MailCopy == null || ComposingAccount == null || CurrentMimeMessage == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
IsRetryingSendToServer = true;
|
||||
IsDraftBusy = true;
|
||||
NotifyComposeActionStateChanged();
|
||||
});
|
||||
|
||||
await UpdateMimeChangesAsync().ConfigureAwait(false);
|
||||
|
||||
var localDraftCopy = CurrentMailDraftItem.MailCopy;
|
||||
var (retryReason, referenceMailCopy) = await ResolveRetryDraftContextAsync().ConfigureAwait(false);
|
||||
var draftPreparationRequest = new DraftPreparationRequest(
|
||||
localDraftCopy.AssignedAccount ?? ComposingAccount,
|
||||
localDraftCopy,
|
||||
CurrentMimeMessage.GetBase64MimeMessage(),
|
||||
retryReason,
|
||||
referenceMailCopy);
|
||||
|
||||
await _worker.ExecuteAsync(draftPreparationRequest).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_dialogService.InfoBarMessage(Translator.Info_RequestCreationFailedTitle, ex.Message, InfoBarMessageType.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
IsRetryingSendToServer = false;
|
||||
});
|
||||
|
||||
await UpdatePendingOperationStateAsync().ConfigureAwait(false);
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
NotifyComposeActionStateChanged();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateMimeChangesAsync()
|
||||
{
|
||||
if (isUpdatingMimeBlocked || CurrentMimeMessage == null || ComposingAccount == null || CurrentMailDraftItem == null) return;
|
||||
@@ -332,13 +507,13 @@ public partial class ComposePageViewModel : MailBaseViewModel
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnNavigatedFrom(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedFrom(mode, parameters);
|
||||
//public override void OnNavigatedFrom(NavigationMode mode, object parameters)
|
||||
//{
|
||||
// base.OnNavigatedFrom(mode, parameters);
|
||||
|
||||
/// Do not put any code here.
|
||||
/// Make sure to use Page's OnNavigatedTo instead.
|
||||
}
|
||||
// /// Do not put any code here.
|
||||
// /// Make sure to use Page's OnNavigatedTo instead.
|
||||
//}
|
||||
|
||||
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
{
|
||||
@@ -348,17 +523,97 @@ public partial class ComposePageViewModel : MailBaseViewModel
|
||||
{
|
||||
CurrentMailDraftItem = mailItem;
|
||||
|
||||
await UpdatePendingOperationStateAsync();
|
||||
await LoadEmailTemplatesAsync();
|
||||
await TryPrepareComposeAsync(true);
|
||||
}
|
||||
}
|
||||
|
||||
public async void Receive(ReaderItemRefreshRequestedEvent message)
|
||||
{
|
||||
if (message.MailItemViewModel == null || !message.MailItemViewModel.IsDraft) return;
|
||||
|
||||
// Save current draft before switching.
|
||||
await UpdateMimeChangesAsync();
|
||||
|
||||
// Reset state for the new draft.
|
||||
isUpdatingMimeBlocked = false;
|
||||
ComposingAccount = null;
|
||||
IncludedAttachments.Clear();
|
||||
|
||||
// Set the new draft item and prepare it.
|
||||
CurrentMailDraftItem = message.MailItemViewModel;
|
||||
await UpdatePendingOperationStateAsync();
|
||||
await LoadEmailTemplatesAsync();
|
||||
await TryPrepareComposeAsync(true);
|
||||
}
|
||||
|
||||
private async Task LoadEmailTemplatesAsync()
|
||||
{
|
||||
var templates = await _emailTemplateService.GetEmailTemplatesAsync().ConfigureAwait(false);
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
AvailableEmailTemplates.Clear();
|
||||
|
||||
foreach (var template in templates)
|
||||
{
|
||||
AvailableEmailTemplates.Add(template);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async void Receive(SynchronizationActionsAdded message)
|
||||
{
|
||||
if (!ShouldTrackDraftSynchronizationState(message.AccountId))
|
||||
return;
|
||||
|
||||
await UpdatePendingOperationStateAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async void Receive(SynchronizationActionsCompleted message)
|
||||
{
|
||||
if (!ShouldTrackDraftSynchronizationState(message.AccountId))
|
||||
return;
|
||||
|
||||
await UpdatePendingOperationStateAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async void Receive(AccountSynchronizerStateChanged message)
|
||||
{
|
||||
if (message.NewState != AccountSynchronizerState.Idle || !ShouldTrackDraftSynchronizationState(message.AccountId))
|
||||
return;
|
||||
|
||||
await UpdatePendingOperationStateAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected override void RegisterRecipients()
|
||||
{
|
||||
base.RegisterRecipients();
|
||||
|
||||
Messenger.Register<ReaderItemRefreshRequestedEvent>(this);
|
||||
Messenger.Register<SynchronizationActionsAdded>(this);
|
||||
Messenger.Register<SynchronizationActionsCompleted>(this);
|
||||
Messenger.Register<AccountSynchronizerStateChanged>(this);
|
||||
}
|
||||
|
||||
protected override void UnregisterRecipients()
|
||||
{
|
||||
base.UnregisterRecipients();
|
||||
|
||||
Messenger.Unregister<ReaderItemRefreshRequestedEvent>(this);
|
||||
Messenger.Unregister<SynchronizationActionsAdded>(this);
|
||||
Messenger.Unregister<SynchronizationActionsCompleted>(this);
|
||||
Messenger.Unregister<AccountSynchronizerStateChanged>(this);
|
||||
}
|
||||
|
||||
private async Task<bool> InitializeComposerAccountAsync()
|
||||
{
|
||||
if (CurrentMailDraftItem == null) return false;
|
||||
|
||||
if (ComposingAccount != null) return true;
|
||||
|
||||
var composingAccount = await _accountService.GetAccountAsync(CurrentMailDraftItem.AssignedAccount.Id).ConfigureAwait(false);
|
||||
var composingAccount = await _accountService.GetAccountAsync(CurrentMailDraftItem.MailCopy.AssignedAccount.Id).ConfigureAwait(false);
|
||||
if (composingAccount == null) return false;
|
||||
|
||||
var aliases = await _accountService.GetAccountAliasesAsync(composingAccount.Id).ConfigureAwait(false);
|
||||
@@ -377,7 +632,7 @@ public partial class ComposePageViewModel : MailBaseViewModel
|
||||
primaryAlias = aliases.Find(a => a.AliasAddress == CurrentMailDraftItem.FromAddress);
|
||||
}
|
||||
|
||||
primaryAlias ??= await _accountService.GetPrimaryAccountAliasAsync(ComposingAccount.Id).ConfigureAwait(false);
|
||||
primaryAlias ??= await _accountService.GetPrimaryAccountAliasAsync(composingAccount.Id).ConfigureAwait(false);
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
@@ -389,6 +644,44 @@ public partial class ComposePageViewModel : MailBaseViewModel
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task UpdatePendingOperationStateAsync()
|
||||
{
|
||||
var hasPendingOperation = false;
|
||||
var keepBusyForInitialGracePeriod = false;
|
||||
|
||||
if (CurrentMailDraftItem?.MailCopy == null || !CurrentMailDraftItem.MailCopy.IsDraft)
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
IsDraftBusy = false;
|
||||
NotifyComposeActionStateChanged();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var accountId = CurrentMailDraftItem.MailCopy.AssignedAccount?.Id ?? Guid.Empty;
|
||||
|
||||
if (accountId != Guid.Empty)
|
||||
{
|
||||
var synchronizer = await SynchronizationManager.Instance.GetSynchronizerAsync(accountId).ConfigureAwait(false);
|
||||
hasPendingOperation = synchronizer?.HasPendingOperation(CurrentMailDraftItem.MailCopy.UniqueId) ?? false;
|
||||
}
|
||||
|
||||
// Newly created local drafts can have a short period where request queue is empty
|
||||
// while folder synchronization/mapping is still in progress.
|
||||
// Keep progress visible during this grace period to prevent "Send to server" flicker.
|
||||
if (!hasPendingOperation && CurrentMailDraftItem.MailCopy.IsLocalDraft)
|
||||
{
|
||||
keepBusyForInitialGracePeriod = IsWithinLocalDraftRetryGracePeriod(CurrentMailDraftItem.MailCopy);
|
||||
}
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
IsDraftBusy = hasPendingOperation || keepBusyForInitialGracePeriod;
|
||||
NotifyComposeActionStateChanged();
|
||||
});
|
||||
}
|
||||
|
||||
private async Task TryPrepareComposeAsync(bool downloadIfNeeded)
|
||||
{
|
||||
if (CurrentMailDraftItem == null) return;
|
||||
@@ -397,7 +690,7 @@ public partial class ComposePageViewModel : MailBaseViewModel
|
||||
|
||||
if (!isComposerInitialized) return;
|
||||
|
||||
retry:
|
||||
retry:
|
||||
|
||||
// Replying existing message.
|
||||
MimeMessageInformation mimeMessageInformation = null;
|
||||
@@ -412,13 +705,12 @@ public partial class ComposePageViewModel : MailBaseViewModel
|
||||
{
|
||||
downloadIfNeeded = false;
|
||||
|
||||
var package = new DownloadMissingMessageRequested(CurrentMailDraftItem.AssignedAccount.Id, CurrentMailDraftItem.MailCopy);
|
||||
var downloadResponse = await _winoServerConnectionManager.GetResponseAsync<bool, DownloadMissingMessageRequested>(package);
|
||||
// Download missing MIME message using SynchronizationManager
|
||||
await SynchronizationManager.Instance.DownloadMimeMessageAsync(
|
||||
CurrentMailDraftItem.MailCopy,
|
||||
CurrentMailDraftItem.MailCopy.AssignedAccount.Id);
|
||||
|
||||
if (downloadResponse.IsSuccess)
|
||||
{
|
||||
goto retry;
|
||||
}
|
||||
goto retry;
|
||||
}
|
||||
else
|
||||
_dialogService.InfoBarMessage(Translator.Info_ComposerMissingMIMETitle, Translator.Info_ComposerMissingMIMEMessage, InfoBarMessageType.Error);
|
||||
@@ -471,6 +763,8 @@ public partial class ComposePageViewModel : MailBaseViewModel
|
||||
{
|
||||
if (CurrentMimeMessage == null) return;
|
||||
|
||||
IncludedAttachments.Clear();
|
||||
|
||||
foreach (var attachment in CurrentMimeMessage.Attachments)
|
||||
{
|
||||
if (attachment.IsAttachment && attachment is MimePart attachmentPart)
|
||||
@@ -530,6 +824,32 @@ public partial class ComposePageViewModel : MailBaseViewModel
|
||||
list.Add(new MailboxAddress(item.Name, item.Address));
|
||||
}
|
||||
|
||||
private async Task<(DraftCreationReason reason, MailCopy referenceMailCopy)> ResolveRetryDraftContextAsync()
|
||||
{
|
||||
if (CurrentMimeMessage == null || CurrentMailDraftItem?.MailCopy?.AssignedAccount == null)
|
||||
return (DraftCreationReason.Empty, null);
|
||||
|
||||
var inReplyTo = CurrentMimeMessage.InReplyTo;
|
||||
if (string.IsNullOrWhiteSpace(inReplyTo) && CurrentMimeMessage.Headers.Contains(HeaderId.InReplyTo))
|
||||
inReplyTo = CurrentMimeMessage.Headers[HeaderId.InReplyTo];
|
||||
|
||||
inReplyTo = MailHeaderExtensions.StripAngleBrackets(inReplyTo);
|
||||
if (string.IsNullOrWhiteSpace(inReplyTo))
|
||||
return (DraftCreationReason.Empty, null);
|
||||
|
||||
var accountId = CurrentMailDraftItem.MailCopy.AssignedAccount.Id;
|
||||
var referenceMailCopy = await _mailService.GetMailCopyByMessageIdAsync(accountId, inReplyTo).ConfigureAwait(false);
|
||||
if (referenceMailCopy == null)
|
||||
return (DraftCreationReason.Empty, null);
|
||||
|
||||
// We cannot perfectly reconstruct original intent (Reply vs ReplyAll) from persisted data.
|
||||
// Infer ReplyAll when multiple recipients exist on the local MIME.
|
||||
var totalRecipients = CurrentMimeMessage.To.Mailboxes.Count() + CurrentMimeMessage.Cc.Mailboxes.Count();
|
||||
var reason = totalRecipients > 1 ? DraftCreationReason.ReplyAll : DraftCreationReason.Reply;
|
||||
|
||||
return (reason, referenceMailCopy);
|
||||
}
|
||||
|
||||
public async Task<AccountContact> GetAddressInformationAsync(string tokenText, ObservableCollection<AccountContact> collection)
|
||||
{
|
||||
// Get model from the service. This will make sure the name is properly included if there is any record.
|
||||
@@ -554,21 +874,57 @@ public partial class ComposePageViewModel : MailBaseViewModel
|
||||
_dialogService.InfoBarMessage(Translator.Info_InvalidAddressTitle, string.Format(Translator.Info_InvalidAddressMessage, address), InfoBarMessageType.Warning);
|
||||
}
|
||||
|
||||
protected override async void OnMailUpdated(MailCopy updatedMail)
|
||||
protected override async void OnMailUpdated(MailCopy updatedMail, MailUpdateSource source, MailCopyChangeFlags changedProperties)
|
||||
{
|
||||
base.OnMailUpdated(updatedMail);
|
||||
base.OnMailUpdated(updatedMail, source, changedProperties);
|
||||
|
||||
if (CurrentMailDraftItem == null) return;
|
||||
|
||||
if (updatedMail.UniqueId == CurrentMailDraftItem.UniqueId)
|
||||
if (updatedMail.UniqueId == CurrentMailDraftItem.MailCopy.UniqueId)
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
await ExecuteUIThread(async () =>
|
||||
{
|
||||
CurrentMailDraftItem.MailCopy = updatedMail;
|
||||
|
||||
DiscardCommand.NotifyCanExecuteChanged();
|
||||
SendCommand.NotifyCanExecuteChanged();
|
||||
CurrentMailDraftItem.UpdateFrom(updatedMail, changedProperties);
|
||||
await UpdatePendingOperationStateAsync();
|
||||
NotifyComposeActionStateChanged();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void NotifyComposeActionStateChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(IsLocalDraft));
|
||||
OnPropertyChanged(nameof(ShouldShowSendToServerButton));
|
||||
OnPropertyChanged(nameof(ShouldShowSendButton));
|
||||
|
||||
DiscardCommand.NotifyCanExecuteChanged();
|
||||
SendCommand.NotifyCanExecuteChanged();
|
||||
SendToServerCommand.NotifyCanExecuteChanged();
|
||||
}
|
||||
|
||||
private bool ShouldTrackDraftSynchronizationState(Guid accountId)
|
||||
{
|
||||
if (accountId == Guid.Empty)
|
||||
return false;
|
||||
|
||||
var currentDraftAccountId = CurrentMailDraftItem?.MailCopy?.AssignedAccount?.Id
|
||||
?? ComposingAccount?.Id
|
||||
?? Guid.Empty;
|
||||
|
||||
return currentDraftAccountId != Guid.Empty && currentDraftAccountId == accountId;
|
||||
}
|
||||
|
||||
private bool IsWithinLocalDraftRetryGracePeriod(MailCopy localDraft)
|
||||
{
|
||||
if (localDraft == null || localDraft.CreationDate == default)
|
||||
return false;
|
||||
|
||||
var elapsed = DateTime.UtcNow - localDraft.CreationDate;
|
||||
|
||||
// Clock skew safety.
|
||||
if (elapsed < TimeSpan.Zero)
|
||||
return true;
|
||||
|
||||
return elapsed < LocalDraftRetryGracePeriod;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,489 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
using Wino.Messaging.Client.Contacts;
|
||||
|
||||
namespace Wino.Mail.ViewModels;
|
||||
|
||||
public partial class ContactsPageViewModel : MailBaseViewModel,
|
||||
IRecipient<NewContactRequested>
|
||||
{
|
||||
private const int ContactPageSize = 50;
|
||||
|
||||
private readonly IContactService _contactService;
|
||||
private readonly IMailDialogService _dialogService;
|
||||
private readonly IContactPictureFileService _contactPictureFileService;
|
||||
|
||||
private CancellationTokenSource _searchDebounceCancellationTokenSource;
|
||||
private int _currentOffset = 0;
|
||||
private int _currentQueryVersion = 0;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string SearchQuery { get; set; } = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(LoadMoreContactsCommand))]
|
||||
[NotifyPropertyChangedFor(nameof(IsEmpty))]
|
||||
public partial bool IsLoading { get; set; } = false;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(LoadMoreContactsCommand))]
|
||||
public partial bool IsLoadingMore { get; set; } = false;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(LoadMoreContactsCommand))]
|
||||
public partial bool HasMoreContacts { get; set; } = false;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsSelectionMode { get; set; } = false;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(DeleteSelectedContactsCommand))]
|
||||
public partial int SelectedContactsCount { get; set; } = 0;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int TotalContactsCount { get; set; } = 0;
|
||||
|
||||
public bool IsEmpty => !IsLoading && Contacts.Count == 0;
|
||||
public bool CanLoadMoreContacts => HasMoreContacts && !IsLoading && !IsLoadingMore;
|
||||
public bool CanDeleteSelectedContacts => SelectedContactsCount > 0;
|
||||
|
||||
public ObservableCollection<AccountContactViewModel> Contacts { get; } = new();
|
||||
public ObservableCollection<AccountContactViewModel> SelectedContacts { get; } = new();
|
||||
|
||||
public ContactsPageViewModel(IContactService contactService, IMailDialogService dialogService, IContactPictureFileService contactPictureFileService)
|
||||
{
|
||||
_contactService = contactService;
|
||||
_dialogService = dialogService;
|
||||
_contactPictureFileService = contactPictureFileService;
|
||||
|
||||
Contacts.CollectionChanged += ContactsCollectionChanged;
|
||||
}
|
||||
|
||||
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedTo(mode, parameters);
|
||||
|
||||
SelectedContacts.CollectionChanged -= SelectedContactsChanged;
|
||||
SelectedContacts.CollectionChanged += SelectedContactsChanged;
|
||||
|
||||
await ReloadContactsAsync();
|
||||
}
|
||||
|
||||
public override void OnNavigatedFrom(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedFrom(mode, parameters);
|
||||
|
||||
SelectedContacts.CollectionChanged -= SelectedContactsChanged;
|
||||
|
||||
_searchDebounceCancellationTokenSource?.Cancel();
|
||||
_searchDebounceCancellationTokenSource?.Dispose();
|
||||
_searchDebounceCancellationTokenSource = null;
|
||||
}
|
||||
|
||||
private async void SelectedContactsChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
=> await ExecuteUIThread(() => { SelectedContactsCount = SelectedContacts.Count; });
|
||||
|
||||
private async void ContactsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
=> await ExecuteUIThread(() => { OnPropertyChanged(nameof(IsEmpty)); });
|
||||
|
||||
void IRecipient<NewContactRequested>.Receive(NewContactRequested message)
|
||||
=> _ = AddContactAsync();
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ReloadContactsAsync()
|
||||
{
|
||||
var queryVersion = ++_currentQueryVersion;
|
||||
_currentOffset = 0;
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
HasMoreContacts = false;
|
||||
Contacts.Clear();
|
||||
SelectedContacts.Clear();
|
||||
});
|
||||
|
||||
await LoadContactsPageAsync(queryVersion, reset: true);
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanLoadMoreContacts))]
|
||||
private async Task LoadMoreContactsAsync()
|
||||
{
|
||||
await LoadContactsPageAsync(_currentQueryVersion, reset: false);
|
||||
}
|
||||
|
||||
private async Task LoadContactsPageAsync(int queryVersion, bool reset)
|
||||
{
|
||||
if (IsLoading || IsLoadingMore)
|
||||
return;
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
if (reset)
|
||||
IsLoading = true;
|
||||
else
|
||||
IsLoadingMore = true;
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
var searchQuery = string.IsNullOrWhiteSpace(SearchQuery) ? null : SearchQuery.Trim();
|
||||
var page = await _contactService.GetContactsPageAsync(
|
||||
_currentOffset,
|
||||
ContactPageSize,
|
||||
searchQuery,
|
||||
excludeRootContacts: true).ConfigureAwait(false);
|
||||
|
||||
if (queryVersion != _currentQueryVersion)
|
||||
return;
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
if (reset)
|
||||
{
|
||||
Contacts.Clear();
|
||||
}
|
||||
|
||||
foreach (var contact in page.Contacts)
|
||||
{
|
||||
Contacts.Add(new AccountContactViewModel(contact));
|
||||
}
|
||||
|
||||
TotalContactsCount = page.TotalCount;
|
||||
HasMoreContacts = page.HasMore;
|
||||
_currentOffset = Contacts.Count;
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (queryVersion != _currentQueryVersion)
|
||||
return;
|
||||
|
||||
_dialogService.InfoBarMessage(
|
||||
Translator.ContactInfoBar_ErrorTitle,
|
||||
string.Format(Translator.ContactInfoBar_FailedToLoadContacts, ex.Message),
|
||||
InfoBarMessageType.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (queryVersion == _currentQueryVersion)
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
if (reset)
|
||||
IsLoading = false;
|
||||
else
|
||||
IsLoadingMore = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task AddContactAsync()
|
||||
{
|
||||
var result = await _dialogService.ShowEditContactDialogAsync(null);
|
||||
|
||||
if (result == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
var newContact = await _contactService.CreateNewContactAsync(result.Address, result.Name);
|
||||
|
||||
if (result.ContactPictureFileId.HasValue)
|
||||
{
|
||||
newContact.ContactPictureFileId = result.ContactPictureFileId;
|
||||
await _contactService.UpdateContactAsync(newContact);
|
||||
}
|
||||
|
||||
await ReloadContactsAsync();
|
||||
|
||||
_dialogService.InfoBarMessage(
|
||||
Translator.ContactInfoBar_SuccessTitle,
|
||||
Translator.ContactInfoBar_ContactAdded,
|
||||
InfoBarMessageType.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_dialogService.InfoBarMessage(
|
||||
Translator.ContactInfoBar_ErrorTitle,
|
||||
string.Format(Translator.ContactInfoBar_FailedToAddContact, ex.Message),
|
||||
InfoBarMessageType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void RegisterRecipients()
|
||||
{
|
||||
base.RegisterRecipients();
|
||||
|
||||
Messenger.Register<NewContactRequested>(this);
|
||||
}
|
||||
|
||||
protected override void UnregisterRecipients()
|
||||
{
|
||||
base.UnregisterRecipients();
|
||||
|
||||
Messenger.Unregister<NewContactRequested>(this);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task EditContactAsync(AccountContactViewModel contactViewModel)
|
||||
{
|
||||
var contact = contactViewModel?.SourceContact;
|
||||
if (contact == null) return;
|
||||
|
||||
var result = await _dialogService.ShowEditContactDialogAsync(contact);
|
||||
|
||||
if (result == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
contact.Name = result.Name;
|
||||
contact.ContactPictureFileId = result.ContactPictureFileId;
|
||||
contact.IsOverridden = result.IsOverridden;
|
||||
|
||||
await _contactService.UpdateContactAsync(contact);
|
||||
await ReloadContactsAsync();
|
||||
|
||||
_dialogService.InfoBarMessage(
|
||||
Translator.ContactInfoBar_SuccessTitle,
|
||||
Translator.ContactInfoBar_ContactUpdated,
|
||||
InfoBarMessageType.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_dialogService.InfoBarMessage(
|
||||
Translator.ContactInfoBar_ErrorTitle,
|
||||
string.Format(Translator.ContactInfoBar_FailedToUpdateContact, ex.Message),
|
||||
InfoBarMessageType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task DeleteContactAsync(AccountContactViewModel contactViewModel)
|
||||
{
|
||||
var contact = contactViewModel?.SourceContact;
|
||||
if (contact == null || contact.IsRootContact)
|
||||
{
|
||||
_dialogService.InfoBarMessage(
|
||||
Translator.ContactInfoBar_WarningTitle,
|
||||
Translator.ContactInfoBar_CannotDeleteRoot,
|
||||
InfoBarMessageType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var confirmed = await _dialogService.ShowConfirmationDialogAsync(
|
||||
string.Format(Translator.ContactConfirmDialog_DeleteMessage, contact.Name ?? contact.Address),
|
||||
Translator.ContactConfirmDialog_DeleteTitle,
|
||||
Translator.ContactConfirmDialog_DeleteButton);
|
||||
|
||||
if (confirmed)
|
||||
{
|
||||
await DeleteContactsInternalAsync(new[] { contact });
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanDeleteSelectedContacts))]
|
||||
private async Task DeleteSelectedContactsAsync()
|
||||
{
|
||||
if (SelectedContacts.Count == 0) return;
|
||||
|
||||
var deletableContacts = SelectedContacts
|
||||
.Select(c => c?.SourceContact)
|
||||
.Where(c => c != null && !c.IsRootContact)
|
||||
.GroupBy(c => c.Address, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(g => g.First())
|
||||
.ToList();
|
||||
|
||||
if (deletableContacts.Count == 0)
|
||||
{
|
||||
_dialogService.InfoBarMessage(
|
||||
Translator.ContactInfoBar_WarningTitle,
|
||||
Translator.ContactInfoBar_CannotDeleteRoot,
|
||||
InfoBarMessageType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var confirmed = await _dialogService.ShowConfirmationDialogAsync(
|
||||
string.Format(Translator.ContactConfirmDialog_DeleteMultipleMessage, deletableContacts.Count),
|
||||
Translator.ContactConfirmDialog_DeleteTitle,
|
||||
Translator.ContactConfirmDialog_DeleteButton);
|
||||
|
||||
if (confirmed)
|
||||
{
|
||||
await DeleteContactsInternalAsync(deletableContacts);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteContactsInternalAsync(IEnumerable<AccountContact> contactsToDelete)
|
||||
{
|
||||
try
|
||||
{
|
||||
var addresses = contactsToDelete
|
||||
.Select(c => c.Address)
|
||||
.Where(a => !string.IsNullOrWhiteSpace(a))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
if (addresses.Count == 0) return;
|
||||
|
||||
await _contactService.DeleteContactsAsync(addresses);
|
||||
await ReloadContactsAsync();
|
||||
|
||||
_dialogService.InfoBarMessage(
|
||||
Translator.ContactInfoBar_SuccessTitle,
|
||||
Translator.ContactInfoBar_ContactsDeleted,
|
||||
InfoBarMessageType.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_dialogService.InfoBarMessage(
|
||||
Translator.ContactInfoBar_ErrorTitle,
|
||||
string.Format(Translator.ContactInfoBar_FailedToDeleteContacts, ex.Message),
|
||||
InfoBarMessageType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ToggleSelection()
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
IsSelectionMode = !IsSelectionMode;
|
||||
|
||||
if (!IsSelectionMode)
|
||||
{
|
||||
SelectedContacts.Clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SelectAllContacts()
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
SelectedContacts.Clear();
|
||||
|
||||
foreach (var contact in Contacts)
|
||||
{
|
||||
SelectedContacts.Add(contact);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ClearSelection()
|
||||
{
|
||||
await ExecuteUIThread(() => { SelectedContacts.Clear(); });
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task PickContactPhotoAsync(AccountContactViewModel contactViewModel)
|
||||
{
|
||||
var contact = contactViewModel?.SourceContact;
|
||||
if (contact == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
var files = await _dialogService.PickFilesAsync(".png", ".jpg", ".jpeg");
|
||||
|
||||
if (files?.Any() == true)
|
||||
{
|
||||
var file = files.First();
|
||||
|
||||
if (contact.ContactPictureFileId.HasValue)
|
||||
await _contactPictureFileService.DeleteContactPictureAsync(contact.ContactPictureFileId.Value);
|
||||
|
||||
contact.ContactPictureFileId = await _contactPictureFileService
|
||||
.SaveContactPictureAsync(file.Data)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await _contactService.UpdateContactAsync(contact);
|
||||
await RefreshContactInUiAsync(contact);
|
||||
|
||||
_dialogService.InfoBarMessage(
|
||||
Translator.ContactInfoBar_SuccessTitle,
|
||||
Translator.ContactInfoBar_ContactPhotoUpdated,
|
||||
InfoBarMessageType.Success);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_dialogService.InfoBarMessage(
|
||||
Translator.ContactInfoBar_ErrorTitle,
|
||||
string.Format(Translator.ContactInfoBar_FailedToUpdatePhoto, ex.Message),
|
||||
InfoBarMessageType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshContactInUiAsync(AccountContact contact)
|
||||
{
|
||||
if (contact == null || string.IsNullOrWhiteSpace(contact.Address))
|
||||
return;
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
ReplaceContactByAddress(Contacts, contact);
|
||||
ReplaceContactByAddress(SelectedContacts, contact);
|
||||
});
|
||||
}
|
||||
|
||||
private static void ReplaceContactByAddress(ObservableCollection<AccountContactViewModel> source, AccountContact updatedContact)
|
||||
{
|
||||
var index = source
|
||||
.Select((item, i) => new { item, i })
|
||||
.FirstOrDefault(x => string.Equals(x.item.Address, updatedContact.Address, StringComparison.OrdinalIgnoreCase))
|
||||
?.i ?? -1;
|
||||
|
||||
if (index < 0) return;
|
||||
|
||||
source[index] = new AccountContactViewModel(CloneContact(updatedContact));
|
||||
}
|
||||
|
||||
private static AccountContact CloneContact(AccountContact contact)
|
||||
=> new()
|
||||
{
|
||||
Address = contact.Address,
|
||||
Name = contact.Name,
|
||||
ContactPictureFileId = contact.ContactPictureFileId,
|
||||
IsRootContact = contact.IsRootContact,
|
||||
IsOverridden = contact.IsOverridden
|
||||
};
|
||||
|
||||
partial void OnSearchQueryChanged(string value)
|
||||
{
|
||||
DebounceSearchAndReload();
|
||||
}
|
||||
|
||||
private async void DebounceSearchAndReload()
|
||||
{
|
||||
_searchDebounceCancellationTokenSource?.Cancel();
|
||||
_searchDebounceCancellationTokenSource?.Dispose();
|
||||
|
||||
_searchDebounceCancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(250, _searchDebounceCancellationTokenSource.Token);
|
||||
await ReloadContactsAsync();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Ignore stale search input.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Mail.ViewModels;
|
||||
|
||||
public partial class CreateEmailTemplatePageViewModel(
|
||||
IEmailTemplateService emailTemplateService,
|
||||
IMailDialogService dialogService,
|
||||
INavigationService navigationService) : MailBaseViewModel
|
||||
{
|
||||
private readonly IEmailTemplateService _emailTemplateService = emailTemplateService;
|
||||
private readonly IMailDialogService _dialogService = dialogService;
|
||||
|
||||
private EmailTemplate _editingTemplate;
|
||||
|
||||
public INavigationService NavigationService { get; } = navigationService;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string TemplateName { get; set; } = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string TemplateDescription { get; set; } = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsExistingTemplate { get; set; }
|
||||
|
||||
public async Task<string> LoadAsync(object parameter)
|
||||
{
|
||||
EmailTemplate template = null;
|
||||
|
||||
var templateId = parameter switch
|
||||
{
|
||||
Guid guid when guid != Guid.Empty => guid,
|
||||
string value when Guid.TryParse(value, out var parsedGuid) => parsedGuid,
|
||||
EmailTemplate emailTemplate when emailTemplate.Id != Guid.Empty => emailTemplate.Id,
|
||||
_ => Guid.Empty
|
||||
};
|
||||
|
||||
if (templateId != Guid.Empty)
|
||||
{
|
||||
template = await _emailTemplateService.GetEmailTemplateAsync(templateId).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_editingTemplate = template;
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
IsExistingTemplate = template != null;
|
||||
TemplateName = template?.Name ?? string.Empty;
|
||||
TemplateDescription = template?.Description ?? string.Empty;
|
||||
});
|
||||
|
||||
return template?.HtmlContent ?? string.Empty;
|
||||
}
|
||||
|
||||
public async Task SaveAsync(string htmlContent)
|
||||
{
|
||||
var trimmedName = TemplateName?.Trim() ?? string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(trimmedName))
|
||||
{
|
||||
_dialogService.InfoBarMessage(
|
||||
Translator.GeneralTitle_Error,
|
||||
Translator.SettingsEmailTemplates_NameRequired,
|
||||
InfoBarMessageType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var template = _editingTemplate ?? new EmailTemplate
|
||||
{
|
||||
Id = Guid.NewGuid()
|
||||
};
|
||||
|
||||
template.Name = trimmedName;
|
||||
template.Description = TemplateDescription?.Trim() ?? string.Empty;
|
||||
template.HtmlContent = htmlContent ?? string.Empty;
|
||||
|
||||
if (_editingTemplate == null)
|
||||
{
|
||||
await _emailTemplateService.CreateEmailTemplateAsync(template).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _emailTemplateService.UpdateEmailTemplateAsync(template).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_editingTemplate = template;
|
||||
NavigationService.GoBack();
|
||||
}
|
||||
|
||||
public async Task DeleteAsync()
|
||||
{
|
||||
if (_editingTemplate == null)
|
||||
return;
|
||||
|
||||
var shouldDelete = await _dialogService.ShowConfirmationDialogAsync(
|
||||
string.Format(Translator.DialogMessage_DeleteEmailTemplateConfirmationMessage, _editingTemplate.Name),
|
||||
Translator.DialogMessage_DeleteEmailTemplateConfirmationTitle,
|
||||
Translator.Buttons_Delete).ConfigureAwait(false);
|
||||
|
||||
if (!shouldDelete)
|
||||
return;
|
||||
|
||||
await _emailTemplateService.DeleteEmailTemplateAsync(_editingTemplate).ConfigureAwait(false);
|
||||
NavigationService.GoBack();
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,28 @@
|
||||
using System;
|
||||
using System;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Data;
|
||||
|
||||
public partial class AccountContactViewModel : ObservableObject
|
||||
public partial class AccountContactViewModel : ObservableObject, IMailItemDisplayInformation
|
||||
{
|
||||
public AccountContact SourceContact { get; }
|
||||
public string Address { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Base64ContactPicture { get; set; }
|
||||
public Guid? ContactPictureFileId { get; set; }
|
||||
public bool IsRootContact { get; set; }
|
||||
public bool IsOverridden { get; set; }
|
||||
|
||||
public AccountContactViewModel(AccountContact contact)
|
||||
{
|
||||
SourceContact = contact;
|
||||
Address = contact.Address;
|
||||
Name = contact.Name;
|
||||
Base64ContactPicture = contact.Base64ContactPicture;
|
||||
ContactPictureFileId = contact.ContactPictureFileId;
|
||||
IsRootContact = contact.IsRootContact;
|
||||
IsOverridden = contact.IsOverridden;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -49,4 +54,26 @@ public partial class AccountContactViewModel : ObservableObject
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool ThumbnailUpdatedEvent { get; set; }
|
||||
|
||||
// IMailItemDisplayInformation implementation for avatar-only rendering.
|
||||
public string Subject => string.Empty;
|
||||
public string FromName => Name ?? string.Empty;
|
||||
public string FromAddress => Address ?? string.Empty;
|
||||
public string PreviewText => string.Empty;
|
||||
public bool IsRead => true;
|
||||
public bool IsDraft => false;
|
||||
public bool HasAttachments => false;
|
||||
public bool IsCalendarEvent => false;
|
||||
public bool IsFlagged => false;
|
||||
public DateTime CreationDate => default;
|
||||
public bool IsBusy => false;
|
||||
public bool IsThreadExpanded => false;
|
||||
public AccountContact SenderContact => new()
|
||||
{
|
||||
Address = Address,
|
||||
Name = Name,
|
||||
ContactPictureFileId = ContactPictureFileId,
|
||||
IsRootContact = IsRootContact,
|
||||
IsOverridden = IsOverridden
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
using System.Windows.Input;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Extensions;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Data;
|
||||
|
||||
public partial class AccountStorageItemViewModel(MailAccount account, long sizeBytes, ICommand deleteAllCommand, ICommand deleteOneMonthCommand, ICommand deleteThreeMonthsCommand, ICommand deleteSixMonthsCommand, ICommand deleteYearCommand) : ObservableObject
|
||||
{
|
||||
public MailAccount Account { get; } = account;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsBusy { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(SizeText))]
|
||||
public partial long SizeBytes { get; set; } = sizeBytes;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string SizeDescription { get; set; } = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ICommand DeleteAllCommand { get; set; } = deleteAllCommand;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ICommand DeleteOneMonthCommand { get; set; } = deleteOneMonthCommand;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ICommand DeleteThreeMonthsCommand { get; set; } = deleteThreeMonthsCommand;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ICommand DeleteSixMonthsCommand { get; set; } = deleteSixMonthsCommand;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ICommand DeleteYearCommand { get; set; } = deleteYearCommand;
|
||||
|
||||
public string AccountName => string.IsNullOrWhiteSpace(Account.Name) ? Account.Address ?? string.Empty : Account.Name;
|
||||
public string AccountAddress => Account.Address ?? string.Empty;
|
||||
public string SizeText => SizeBytes.GetBytesReadable();
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Common interface for mail items that can be displayed in a mail list.
|
||||
/// Implemented by both MailItemViewModel and ThreadMailItemViewModel.
|
||||
/// </summary>
|
||||
public interface IMailListItem : IMailHashContainer, IMailListItemSorting, INotifyPropertyChanged
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the latest creation date for sorting purposes.
|
||||
/// For MailItemViewModel: the mail's creation date
|
||||
/// For ThreadMailItemViewModel: the latest email's creation date
|
||||
/// </summary>
|
||||
DateTime CreationDate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the sender's name for grouping purposes.
|
||||
/// For MailItemViewModel: the mail's from name
|
||||
/// For ThreadMailItemViewModel: the latest email's from name
|
||||
/// </summary>
|
||||
string FromName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this item is selected.
|
||||
/// For MailItemViewModel: returns IsSelected
|
||||
/// For ThreadMailItemViewModel: returns IsSelected
|
||||
/// </summary>
|
||||
bool IsSelected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this item is currently processing a network operation.
|
||||
/// </summary>
|
||||
bool IsBusy { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets all selected mail items within this list item.
|
||||
/// For MailItemViewModel: returns itself if IsSelected is true, otherwise empty
|
||||
/// For ThreadMailItemViewModel: returns all selected emails within the thread
|
||||
/// </summary>
|
||||
IEnumerable<MailItemViewModel> GetSelectedMailItems();
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Models.Accounts;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Data;
|
||||
|
||||
public enum ImapCalDavSettingsPageMode
|
||||
{
|
||||
Create,
|
||||
Edit,
|
||||
Wizard
|
||||
}
|
||||
|
||||
public sealed class ImapCalDavSettingsNavigationContext
|
||||
{
|
||||
public ImapCalDavSettingsPageMode Mode { get; init; }
|
||||
public Guid AccountId { get; init; }
|
||||
public AccountCreationDialogResult AccountCreationDialogResult { get; init; }
|
||||
public TaskCompletionSource<ImapCalDavSetupResult> CompletionSource { get; init; }
|
||||
|
||||
public static ImapCalDavSettingsNavigationContext CreateForCreateMode(
|
||||
AccountCreationDialogResult accountCreationDialogResult,
|
||||
TaskCompletionSource<ImapCalDavSetupResult> completionSource)
|
||||
=> new()
|
||||
{
|
||||
Mode = ImapCalDavSettingsPageMode.Create,
|
||||
AccountCreationDialogResult = accountCreationDialogResult,
|
||||
CompletionSource = completionSource
|
||||
};
|
||||
|
||||
public static ImapCalDavSettingsNavigationContext CreateForEditMode(Guid accountId)
|
||||
=> new()
|
||||
{
|
||||
Mode = ImapCalDavSettingsPageMode.Edit,
|
||||
AccountId = accountId
|
||||
};
|
||||
|
||||
public static ImapCalDavSettingsNavigationContext CreateForWizardMode(
|
||||
AccountCreationDialogResult accountCreationDialogResult)
|
||||
=> new()
|
||||
{
|
||||
Mode = ImapCalDavSettingsPageMode.Wizard,
|
||||
AccountCreationDialogResult = accountCreationDialogResult
|
||||
};
|
||||
|
||||
public bool IsWizardMode => Mode == ImapCalDavSettingsPageMode.Wizard;
|
||||
}
|
||||
|
||||
public sealed class ImapCalDavSetupResult
|
||||
{
|
||||
public string DisplayName { get; init; }
|
||||
public string EmailAddress { get; init; }
|
||||
public bool IsCalendarAccessGranted { get; init; }
|
||||
public CustomServerInformation ServerInformation { get; init; }
|
||||
}
|
||||
|
||||
public sealed class ImapCalendarSupportModeOption(ImapCalendarSupportMode mode, string title)
|
||||
{
|
||||
public ImapCalendarSupportMode Mode { get; } = mode;
|
||||
public string Title { get; } = title;
|
||||
}
|
||||
@@ -6,6 +6,25 @@ public class MailItemContainer
|
||||
{
|
||||
public MailItemViewModel ItemViewModel { get; set; }
|
||||
public ThreadMailItemViewModel ThreadViewModel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the mail item is currently visible in the UI's Items collection.
|
||||
/// For threaded items, this indicates if the individual mail item is visible (thread must be expanded).
|
||||
/// </summary>
|
||||
public bool IsItemVisible { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the thread expander (if applicable) is currently visible in the UI's Items collection.
|
||||
/// Only relevant when ThreadViewModel is not null.
|
||||
/// </summary>
|
||||
public bool IsThreadVisible { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the container can be successfully navigated to in the UI.
|
||||
/// For standalone items: true if IsItemVisible is true.
|
||||
/// For threaded items: true if IsThreadVisible is true (the thread expander can be navigated to).
|
||||
/// </summary>
|
||||
public bool CanNavigate => ThreadViewModel != null ? IsThreadVisible : IsItemVisible;
|
||||
|
||||
public MailItemContainer(MailItemViewModel itemViewModel, ThreadMailItemViewModel threadViewModel) : this(itemViewModel)
|
||||
{
|
||||
|
||||
@@ -3,34 +3,78 @@ using System.Collections.Generic;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Single view model for IMailItem representation.
|
||||
/// </summary>
|
||||
public partial class MailItemViewModel(MailCopy mailCopy) : ObservableObject, IMailItem
|
||||
public partial class MailItemViewModel(MailCopy mailCopy) : ObservableRecipient, IMailListItem, IMailItemDisplayInformation
|
||||
{
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(CreationDate))]
|
||||
[NotifyPropertyChangedFor(nameof(IsFlagged))]
|
||||
[NotifyPropertyChangedFor(nameof(FromName))]
|
||||
[NotifyPropertyChangedFor(nameof(IsFocused))]
|
||||
[NotifyPropertyChangedFor(nameof(IsRead))]
|
||||
[NotifyPropertyChangedFor(nameof(IsDraft))]
|
||||
[NotifyPropertyChangedFor(nameof(DraftId))]
|
||||
[NotifyPropertyChangedFor(nameof(Id))]
|
||||
[NotifyPropertyChangedFor(nameof(Subject))]
|
||||
[NotifyPropertyChangedFor(nameof(PreviewText))]
|
||||
[NotifyPropertyChangedFor(nameof(FromAddress))]
|
||||
[NotifyPropertyChangedFor(nameof(HasAttachments))]
|
||||
[NotifyPropertyChangedFor(nameof(IsCalendarEvent))]
|
||||
[NotifyPropertyChangedFor(nameof(Importance))]
|
||||
[NotifyPropertyChangedFor(nameof(ThreadId))]
|
||||
[NotifyPropertyChangedFor(nameof(MessageId))]
|
||||
[NotifyPropertyChangedFor(nameof(References))]
|
||||
[NotifyPropertyChangedFor(nameof(InReplyTo))]
|
||||
[NotifyPropertyChangedFor(nameof(FileId))]
|
||||
[NotifyPropertyChangedFor(nameof(FolderId))]
|
||||
[NotifyPropertyChangedFor(nameof(UniqueId))]
|
||||
[NotifyPropertyChangedFor(nameof(ContactPictureFileId))]
|
||||
[NotifyPropertyChangedFor(nameof(SenderContact))]
|
||||
public partial MailCopy MailCopy { get; set; } = mailCopy;
|
||||
|
||||
public Guid UniqueId => ((IMailItem)MailCopy).UniqueId;
|
||||
public string ThreadId => ((IMailItem)MailCopy).ThreadId;
|
||||
public string MessageId => ((IMailItem)MailCopy).MessageId;
|
||||
public DateTime CreationDate => ((IMailItem)MailCopy).CreationDate;
|
||||
public string References => ((IMailItem)MailCopy).References;
|
||||
public string InReplyTo => ((IMailItem)MailCopy).InReplyTo;
|
||||
[ObservableProperty]
|
||||
public partial bool IsDisplayedInThread { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedRecipients]
|
||||
public partial bool IsSelected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Direct callback invoked when <see cref="IsSelected"/> changes.
|
||||
/// Used by the ListViewItem container to update its IsCustomSelected DP
|
||||
/// without subscribing to INotifyPropertyChanged (faster, AOT-safe).
|
||||
/// </summary>
|
||||
public Action<bool> OnSelectionChanged { get; set; }
|
||||
|
||||
partial void OnIsSelectedChanged(bool value) => OnSelectionChanged?.Invoke(value);
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if this mail item is currently being processed by a network operation.
|
||||
/// Used to show loading state in the UI.
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
public partial bool IsBusy { get; set; }
|
||||
|
||||
public bool IsThreadExpanded => false;
|
||||
|
||||
public AccountContact SenderContact => MailCopy.SenderContact;
|
||||
|
||||
public DateTime CreationDate
|
||||
{
|
||||
get => MailCopy.CreationDate;
|
||||
set => SetProperty(MailCopy.CreationDate, value, MailCopy, (u, n) => u.CreationDate = n);
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool ThumbnailUpdatedEvent { get; set; } = false;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsCustomFocused { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsSelected { get; set; }
|
||||
|
||||
public bool IsFlagged
|
||||
{
|
||||
get => MailCopy.IsFlagged;
|
||||
@@ -97,13 +141,274 @@ public partial class MailItemViewModel(MailCopy mailCopy) : ObservableObject, IM
|
||||
set => SetProperty(MailCopy.HasAttachments, value, MailCopy, (u, n) => u.HasAttachments = n);
|
||||
}
|
||||
|
||||
public MailItemFolder AssignedFolder => ((IMailItem)MailCopy).AssignedFolder;
|
||||
public bool IsCalendarEvent => MailCopy.ItemType == MailItemType.CalendarInvitation;
|
||||
|
||||
public MailAccount AssignedAccount => ((IMailItem)MailCopy).AssignedAccount;
|
||||
public MailImportance Importance
|
||||
{
|
||||
get => MailCopy.Importance;
|
||||
set => SetProperty(MailCopy.Importance, value, MailCopy, (u, n) => u.Importance = n);
|
||||
}
|
||||
|
||||
public Guid FileId => ((IMailItem)MailCopy).FileId;
|
||||
public string ThreadId
|
||||
{
|
||||
get => MailCopy.ThreadId;
|
||||
set => SetProperty(MailCopy.ThreadId, value, MailCopy, (u, n) => u.ThreadId = n);
|
||||
}
|
||||
|
||||
public AccountContact SenderContact => ((IMailItem)MailCopy).SenderContact;
|
||||
public string MessageId
|
||||
{
|
||||
get => MailCopy.MessageId;
|
||||
set => SetProperty(MailCopy.MessageId, value, MailCopy, (u, n) => u.MessageId = n);
|
||||
}
|
||||
|
||||
public IEnumerable<Guid> GetContainingIds() => new[] { UniqueId };
|
||||
public string References
|
||||
{
|
||||
get => MailCopy.References;
|
||||
set => SetProperty(MailCopy.References, value, MailCopy, (u, n) => u.References = n);
|
||||
}
|
||||
|
||||
public string InReplyTo
|
||||
{
|
||||
get => MailCopy.InReplyTo;
|
||||
set => SetProperty(MailCopy.InReplyTo, value, MailCopy, (u, n) => u.InReplyTo = n);
|
||||
}
|
||||
|
||||
public Guid FileId
|
||||
{
|
||||
get => MailCopy.FileId;
|
||||
set => SetProperty(MailCopy.FileId, value, MailCopy, (u, n) => u.FileId = n);
|
||||
}
|
||||
|
||||
public Guid FolderId
|
||||
{
|
||||
get => MailCopy.FolderId;
|
||||
set => SetProperty(MailCopy.FolderId, value, MailCopy, (u, n) => u.FolderId = n);
|
||||
}
|
||||
|
||||
public Guid UniqueId
|
||||
{
|
||||
get => MailCopy.UniqueId;
|
||||
set => SetProperty(MailCopy.UniqueId, value, MailCopy, (u, n) => u.UniqueId = n);
|
||||
}
|
||||
|
||||
public Guid? ContactPictureFileId
|
||||
{
|
||||
get => MailCopy.SenderContact?.ContactPictureFileId;
|
||||
set => SetProperty(MailCopy.SenderContact?.ContactPictureFileId, value, MailCopy, (u, n) =>
|
||||
{
|
||||
if (u.SenderContact != null)
|
||||
u.SenderContact.ContactPictureFileId = n;
|
||||
});
|
||||
}
|
||||
|
||||
public DateTime SortingDate => CreationDate;
|
||||
|
||||
public string SortingName => FromName;
|
||||
|
||||
public IEnumerable<Guid> GetContainingIds() => [MailCopy.UniqueId];
|
||||
|
||||
public IEnumerable<MailItemViewModel> GetSelectedMailItems()
|
||||
{
|
||||
if (IsSelected)
|
||||
{
|
||||
yield return this;
|
||||
}
|
||||
}
|
||||
|
||||
public static MailCopyChangeFlags GetChangeFlagsForProperty(string propertyName)
|
||||
{
|
||||
return propertyName switch
|
||||
{
|
||||
nameof(CreationDate) or nameof(SortingDate) => MailCopyChangeFlags.CreationDate,
|
||||
nameof(IsFlagged) => MailCopyChangeFlags.IsFlagged,
|
||||
nameof(FromName) or nameof(SortingName) => MailCopyChangeFlags.FromName,
|
||||
nameof(IsFocused) => MailCopyChangeFlags.IsFocused,
|
||||
nameof(IsRead) => MailCopyChangeFlags.IsRead,
|
||||
nameof(IsDraft) => MailCopyChangeFlags.IsDraft,
|
||||
nameof(DraftId) => MailCopyChangeFlags.DraftId,
|
||||
nameof(Id) => MailCopyChangeFlags.Id,
|
||||
nameof(Subject) => MailCopyChangeFlags.Subject,
|
||||
nameof(PreviewText) => MailCopyChangeFlags.PreviewText,
|
||||
nameof(FromAddress) => MailCopyChangeFlags.FromAddress,
|
||||
nameof(HasAttachments) => MailCopyChangeFlags.HasAttachments,
|
||||
nameof(IsCalendarEvent) => MailCopyChangeFlags.ItemType,
|
||||
nameof(Importance) => MailCopyChangeFlags.Importance,
|
||||
nameof(ThreadId) => MailCopyChangeFlags.ThreadId,
|
||||
nameof(MessageId) => MailCopyChangeFlags.MessageId,
|
||||
nameof(References) => MailCopyChangeFlags.References,
|
||||
nameof(InReplyTo) => MailCopyChangeFlags.InReplyTo,
|
||||
nameof(FileId) => MailCopyChangeFlags.FileId,
|
||||
nameof(FolderId) => MailCopyChangeFlags.FolderId,
|
||||
nameof(UniqueId) => MailCopyChangeFlags.UniqueId,
|
||||
nameof(ContactPictureFileId) or nameof(SenderContact) => MailCopyChangeFlags.SenderContact,
|
||||
_ => MailCopyChangeFlags.None
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the existing <see cref="MailCopy"/> while raising only the relevant UI notifications.
|
||||
/// </summary>
|
||||
/// <param name="source">Source data used to update this item.</param>
|
||||
/// <param name="changeHint">
|
||||
/// Optional set of known changes. This is required when <paramref name="source"/> is the same instance
|
||||
/// and has already been mutated by Apply/Revert flows.
|
||||
/// </param>
|
||||
/// <returns>The effective set of changed fields used for notifications.</returns>
|
||||
public MailCopyChangeFlags UpdateFrom(MailCopy source, MailCopyChangeFlags changeHint = MailCopyChangeFlags.None)
|
||||
{
|
||||
if (source == null) return MailCopyChangeFlags.None;
|
||||
|
||||
var changedFlags = MailCopyChangeFlags.None;
|
||||
var isSameReference = ReferenceEquals(MailCopy, source);
|
||||
|
||||
if (!isSameReference)
|
||||
{
|
||||
changedFlags |= SetIfChanged(MailCopy.Id, source.Id, value => MailCopy.Id = value, MailCopyChangeFlags.Id);
|
||||
changedFlags |= SetIfChanged(MailCopy.FolderId, source.FolderId, value => MailCopy.FolderId = value, MailCopyChangeFlags.FolderId);
|
||||
changedFlags |= SetIfChanged(MailCopy.ThreadId, source.ThreadId, value => MailCopy.ThreadId = value, MailCopyChangeFlags.ThreadId);
|
||||
changedFlags |= SetIfChanged(MailCopy.MessageId, source.MessageId, value => MailCopy.MessageId = value, MailCopyChangeFlags.MessageId);
|
||||
changedFlags |= SetIfChanged(MailCopy.References, source.References, value => MailCopy.References = value, MailCopyChangeFlags.References);
|
||||
changedFlags |= SetIfChanged(MailCopy.InReplyTo, source.InReplyTo, value => MailCopy.InReplyTo = value, MailCopyChangeFlags.InReplyTo);
|
||||
changedFlags |= SetIfChanged(MailCopy.IsDraft, source.IsDraft, value => MailCopy.IsDraft = value, MailCopyChangeFlags.IsDraft);
|
||||
changedFlags |= SetIfChanged(MailCopy.DraftId, source.DraftId, value => MailCopy.DraftId = value, MailCopyChangeFlags.DraftId);
|
||||
changedFlags |= SetIfChanged(MailCopy.CreationDate, source.CreationDate, value => MailCopy.CreationDate = value, MailCopyChangeFlags.CreationDate);
|
||||
changedFlags |= SetIfChanged(MailCopy.Subject, source.Subject, value => MailCopy.Subject = value, MailCopyChangeFlags.Subject);
|
||||
changedFlags |= SetIfChanged(MailCopy.PreviewText, source.PreviewText, value => MailCopy.PreviewText = value, MailCopyChangeFlags.PreviewText);
|
||||
changedFlags |= SetIfChanged(MailCopy.FromName, source.FromName, value => MailCopy.FromName = value, MailCopyChangeFlags.FromName);
|
||||
changedFlags |= SetIfChanged(MailCopy.FromAddress, source.FromAddress, value => MailCopy.FromAddress = value, MailCopyChangeFlags.FromAddress);
|
||||
changedFlags |= SetIfChanged(MailCopy.HasAttachments, source.HasAttachments, value => MailCopy.HasAttachments = value, MailCopyChangeFlags.HasAttachments);
|
||||
changedFlags |= SetIfChanged(MailCopy.Importance, source.Importance, value => MailCopy.Importance = value, MailCopyChangeFlags.Importance);
|
||||
changedFlags |= SetIfChanged(MailCopy.IsRead, source.IsRead, value => MailCopy.IsRead = value, MailCopyChangeFlags.IsRead);
|
||||
changedFlags |= SetIfChanged(MailCopy.IsFlagged, source.IsFlagged, value => MailCopy.IsFlagged = value, MailCopyChangeFlags.IsFlagged);
|
||||
changedFlags |= SetIfChanged(MailCopy.IsFocused, source.IsFocused, value => MailCopy.IsFocused = value, MailCopyChangeFlags.IsFocused);
|
||||
changedFlags |= SetIfChanged(MailCopy.FileId, source.FileId, value => MailCopy.FileId = value, MailCopyChangeFlags.FileId);
|
||||
changedFlags |= SetIfChanged(MailCopy.ItemType, source.ItemType, value => MailCopy.ItemType = value, MailCopyChangeFlags.ItemType);
|
||||
changedFlags |= SetIfChanged(MailCopy.SenderContact, source.SenderContact, value => MailCopy.SenderContact = value, MailCopyChangeFlags.SenderContact);
|
||||
changedFlags |= SetIfChanged(MailCopy.AssignedAccount, source.AssignedAccount, value => MailCopy.AssignedAccount = value, MailCopyChangeFlags.AssignedAccount);
|
||||
changedFlags |= SetIfChanged(MailCopy.AssignedFolder, source.AssignedFolder, value => MailCopy.AssignedFolder = value, MailCopyChangeFlags.AssignedFolder);
|
||||
changedFlags |= SetIfChanged(MailCopy.UniqueId, source.UniqueId, value => MailCopy.UniqueId = value, MailCopyChangeFlags.UniqueId);
|
||||
}
|
||||
|
||||
changedFlags |= changeHint;
|
||||
|
||||
if (isSameReference && changedFlags == MailCopyChangeFlags.None)
|
||||
{
|
||||
// Without a hint there is no reliable way to diff in-place updates on the same instance.
|
||||
// Fall back to full refresh to preserve correctness.
|
||||
changedFlags = MailCopyChangeFlags.All;
|
||||
}
|
||||
|
||||
RaisePropertyChanges(changedFlags);
|
||||
|
||||
return changedFlags;
|
||||
}
|
||||
|
||||
private static MailCopyChangeFlags SetIfChanged<T>(T currentValue, T newValue, Action<T> setter, MailCopyChangeFlags flag)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(currentValue, newValue))
|
||||
return MailCopyChangeFlags.None;
|
||||
|
||||
setter(newValue);
|
||||
return flag;
|
||||
}
|
||||
|
||||
private void RaisePropertyChanges(MailCopyChangeFlags changedFlags)
|
||||
{
|
||||
if (changedFlags == MailCopyChangeFlags.None)
|
||||
return;
|
||||
|
||||
var changedProperties = new List<string>(12);
|
||||
|
||||
void Queue(string propertyName)
|
||||
{
|
||||
if (!changedProperties.Contains(propertyName))
|
||||
{
|
||||
changedProperties.Add(propertyName);
|
||||
}
|
||||
}
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.CreationDate) != 0)
|
||||
{
|
||||
Queue(nameof(CreationDate));
|
||||
Queue(nameof(SortingDate));
|
||||
}
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.IsFlagged) != 0)
|
||||
Queue(nameof(IsFlagged));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.FromName) != 0)
|
||||
{
|
||||
Queue(nameof(FromName));
|
||||
Queue(nameof(SortingName));
|
||||
}
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.FromAddress) != 0)
|
||||
{
|
||||
Queue(nameof(FromAddress));
|
||||
Queue(nameof(FromName));
|
||||
Queue(nameof(SortingName));
|
||||
}
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.IsFocused) != 0)
|
||||
Queue(nameof(IsFocused));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.IsRead) != 0)
|
||||
Queue(nameof(IsRead));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.IsDraft) != 0)
|
||||
Queue(nameof(IsDraft));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.DraftId) != 0)
|
||||
Queue(nameof(DraftId));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.Id) != 0)
|
||||
Queue(nameof(Id));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.Subject) != 0)
|
||||
Queue(nameof(Subject));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.PreviewText) != 0)
|
||||
Queue(nameof(PreviewText));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.HasAttachments) != 0)
|
||||
Queue(nameof(HasAttachments));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.ItemType) != 0)
|
||||
Queue(nameof(IsCalendarEvent));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.Importance) != 0)
|
||||
Queue(nameof(Importance));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.ThreadId) != 0)
|
||||
Queue(nameof(ThreadId));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.MessageId) != 0)
|
||||
Queue(nameof(MessageId));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.References) != 0)
|
||||
Queue(nameof(References));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.InReplyTo) != 0)
|
||||
Queue(nameof(InReplyTo));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.FileId) != 0)
|
||||
Queue(nameof(FileId));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.FolderId) != 0)
|
||||
Queue(nameof(FolderId));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.UniqueId) != 0)
|
||||
Queue(nameof(UniqueId));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.SenderContact) != 0)
|
||||
{
|
||||
Queue(nameof(ContactPictureFileId));
|
||||
Queue(nameof(SenderContact));
|
||||
}
|
||||
|
||||
foreach (var changedProperty in changedProperties)
|
||||
{
|
||||
OnPropertyChanged(changedProperty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,126 +1,484 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Thread mail item (multiple IMailItem) view model representation.
|
||||
/// </summary>
|
||||
public partial class ThreadMailItemViewModel : ObservableObject, IMailItemThread, IComparable<string>, IComparable<DateTime>
|
||||
public partial class ThreadMailItemViewModel : ObservableRecipient, IMailListItem, IMailItemDisplayInformation
|
||||
{
|
||||
public ObservableCollection<IMailItem> ThreadItems => (MailItem as IMailItemThread)?.ThreadItems ?? [];
|
||||
public AccountContact SenderContact => ((IMailItemThread)MailItem).SenderContact;
|
||||
private readonly string _threadId;
|
||||
private readonly HashSet<Guid> _uniqueIdSet = [];
|
||||
private MailItemViewModel _cachedLatestMailViewModel;
|
||||
private int _suspendChildPropertyNotificationsCount;
|
||||
|
||||
[ObservableProperty]
|
||||
private ThreadMailItem mailItem;
|
||||
[NotifyPropertyChangedRecipients]
|
||||
[NotifyPropertyChangedFor(nameof(IsSelectedOrExpanded))]
|
||||
public partial bool IsThreadExpanded { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isThreadExpanded;
|
||||
[NotifyPropertyChangedRecipients]
|
||||
[NotifyPropertyChangedFor(nameof(IsSelectedOrExpanded))]
|
||||
public partial bool IsSelected { get; set; }
|
||||
|
||||
public ThreadMailItemViewModel(ThreadMailItem threadMailItem)
|
||||
/// <summary>
|
||||
/// Direct callback invoked when <see cref="IsSelected"/> changes.
|
||||
/// Used by the ListViewItem container to update its IsCustomSelected DP
|
||||
/// without subscribing to INotifyPropertyChanged (faster, AOT-safe).
|
||||
/// </summary>
|
||||
public Action<bool> OnSelectionChanged { get; set; }
|
||||
|
||||
partial void OnIsSelectedChanged(bool value) => OnSelectionChanged?.Invoke(value);
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsBusy { get; set; }
|
||||
|
||||
public bool IsSelectedOrExpanded => IsSelected || IsThreadExpanded;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of emails in this thread
|
||||
/// </summary>
|
||||
public int EmailCount => ThreadEmails.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the latest email's subject for display
|
||||
/// </summary>
|
||||
public string Subject => latestMailViewModel?.MailCopy?.Subject;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the latest email's sender name for display
|
||||
/// </summary>
|
||||
public string FromName => latestMailViewModel?.MailCopy?.FromName ?? Translator.UnknownSender;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the latest email's creation date for sorting
|
||||
/// </summary>
|
||||
public DateTime CreationDate => latestMailViewModel?.MailCopy?.CreationDate ?? DateTime.MinValue;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the latest email's sender address for display
|
||||
/// </summary>
|
||||
public string FromAddress => latestMailViewModel?.FromAddress ?? string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the preview text from the latest email
|
||||
/// </summary>
|
||||
public string PreviewText => latestMailViewModel?.PreviewText ?? string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether any email in this thread has attachments
|
||||
/// </summary>
|
||||
public bool HasAttachments => ThreadEmails.Any(e => e.HasAttachments);
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether any email in this thread is a calendar invitation.
|
||||
/// </summary>
|
||||
public bool IsCalendarEvent => ThreadEmails.Any(e => e.IsCalendarEvent);
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether any email in this thread is flagged
|
||||
/// </summary>
|
||||
public bool IsFlagged => ThreadEmails.Any(e => e.IsFlagged);
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the latest email is focused
|
||||
/// </summary>
|
||||
public bool IsFocused => latestMailViewModel?.IsFocused ?? false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether all emails in this thread are read
|
||||
/// </summary>
|
||||
public bool IsRead => ThreadEmails.All(e => e.IsRead);
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether any email in this thread is a draft
|
||||
/// </summary>
|
||||
public bool IsDraft => ThreadEmails.Any(e => e.IsDraft);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the draft ID from the latest email if it's a draft
|
||||
/// </summary>
|
||||
public string DraftId => latestMailViewModel?.DraftId ?? string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ID from the latest email
|
||||
/// </summary>
|
||||
public string Id => latestMailViewModel?.Id ?? string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the importance of the latest email
|
||||
/// </summary>
|
||||
public MailImportance Importance => latestMailViewModel?.Importance ?? MailImportance.Normal;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the thread ID from the latest email
|
||||
/// </summary>
|
||||
public string ThreadId => latestMailViewModel?.ThreadId ?? _threadId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the message ID from the latest email
|
||||
/// </summary>
|
||||
public string MessageId => latestMailViewModel?.MessageId ?? string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the references from the latest email
|
||||
/// </summary>
|
||||
public string References => latestMailViewModel?.References ?? string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the in-reply-to from the latest email
|
||||
/// </summary>
|
||||
public string InReplyTo => latestMailViewModel?.InReplyTo ?? string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file ID from the latest email
|
||||
/// </summary>
|
||||
public Guid FileId => latestMailViewModel?.FileId ?? Guid.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the folder ID from the latest email
|
||||
/// </summary>
|
||||
public Guid FolderId => latestMailViewModel?.FolderId ?? Guid.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the unique ID from the latest email
|
||||
/// </summary>
|
||||
public Guid UniqueId => latestMailViewModel?.UniqueId ?? Guid.Empty;
|
||||
|
||||
public Guid? ContactPictureFileId => latestMailViewModel?.MailCopy?.SenderContact?.ContactPictureFileId;
|
||||
|
||||
public bool ThumbnailUpdatedEvent => latestMailViewModel?.ThumbnailUpdatedEvent ?? false;
|
||||
|
||||
public AccountContact SenderContact => latestMailViewModel?.MailCopy?.SenderContact;
|
||||
|
||||
/// <summary>
|
||||
/// Gets all emails in this thread (observable)
|
||||
/// </summary>
|
||||
///
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(EmailCount))]
|
||||
[NotifyPropertyChangedFor(nameof(Subject))]
|
||||
[NotifyPropertyChangedFor(nameof(FromName))]
|
||||
[NotifyPropertyChangedFor(nameof(CreationDate))]
|
||||
[NotifyPropertyChangedFor(nameof(FromAddress))]
|
||||
[NotifyPropertyChangedFor(nameof(PreviewText))]
|
||||
[NotifyPropertyChangedFor(nameof(HasAttachments))]
|
||||
[NotifyPropertyChangedFor(nameof(IsCalendarEvent))]
|
||||
[NotifyPropertyChangedFor(nameof(IsFlagged))]
|
||||
[NotifyPropertyChangedFor(nameof(IsFocused))]
|
||||
[NotifyPropertyChangedFor(nameof(IsRead))]
|
||||
[NotifyPropertyChangedFor(nameof(IsDraft))]
|
||||
[NotifyPropertyChangedFor(nameof(DraftId))]
|
||||
[NotifyPropertyChangedFor(nameof(Id))]
|
||||
[NotifyPropertyChangedFor(nameof(Importance))]
|
||||
[NotifyPropertyChangedFor(nameof(ThreadId))]
|
||||
[NotifyPropertyChangedFor(nameof(MessageId))]
|
||||
[NotifyPropertyChangedFor(nameof(References))]
|
||||
[NotifyPropertyChangedFor(nameof(InReplyTo))]
|
||||
[NotifyPropertyChangedFor(nameof(FileId))]
|
||||
[NotifyPropertyChangedFor(nameof(FolderId))]
|
||||
[NotifyPropertyChangedFor(nameof(UniqueId))]
|
||||
[NotifyPropertyChangedFor(nameof(ContactPictureFileId))]
|
||||
[NotifyPropertyChangedFor(nameof(SenderContact))]
|
||||
public partial ObservableCollection<MailItemViewModel> ThreadEmails { get; set; } = [];
|
||||
|
||||
private MailItemViewModel latestMailViewModel => _cachedLatestMailViewModel;
|
||||
|
||||
public DateTime SortingDate => CreationDate;
|
||||
|
||||
public string SortingName => FromName;
|
||||
|
||||
public ThreadMailItemViewModel(string threadId)
|
||||
{
|
||||
MailItem = new ThreadMailItem();
|
||||
_threadId = threadId;
|
||||
}
|
||||
|
||||
// Local copies
|
||||
foreach (var item in threadMailItem.ThreadItems)
|
||||
internal void SuspendChildPropertyNotifications() => _suspendChildPropertyNotificationsCount++;
|
||||
|
||||
internal void ResumeChildPropertyNotifications()
|
||||
{
|
||||
if (_suspendChildPropertyNotificationsCount > 0)
|
||||
{
|
||||
AddMailItemViewModel(item);
|
||||
_suspendChildPropertyNotificationsCount--;
|
||||
}
|
||||
}
|
||||
|
||||
public ThreadMailItem GetThreadMailItem() => MailItem;
|
||||
|
||||
public IEnumerable<MailCopy> GetMailCopies()
|
||||
=> ThreadItems.OfType<MailItemViewModel>().Select(a => a.MailCopy);
|
||||
|
||||
public void AddMailItemViewModel(IMailItem mailItem)
|
||||
private void RefreshLatestMailCache()
|
||||
{
|
||||
if (mailItem == null) return;
|
||||
_cachedLatestMailViewModel = ThreadEmails
|
||||
.OrderByDescending(static item => item.MailCopy.CreationDate)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
if (mailItem is MailCopy mailCopy)
|
||||
MailItem.AddThreadItem(new MailItemViewModel(mailCopy));
|
||||
else if (mailItem is MailItemViewModel mailItemViewModel)
|
||||
MailItem.AddThreadItem(mailItemViewModel);
|
||||
/// <summary>
|
||||
/// Adds an email to this thread
|
||||
/// </summary>
|
||||
public void AddEmail(MailItemViewModel email)
|
||||
{
|
||||
if (email.MailCopy.ThreadId != _threadId)
|
||||
throw new ArgumentException($"Email ThreadId '{email.MailCopy.ThreadId}' does not match expander ThreadId '{_threadId}'");
|
||||
|
||||
// Insert email in sorted order by CreationDate (newest first, oldest last)
|
||||
var insertIndex = 0;
|
||||
for (int i = 0; i < ThreadEmails.Count; i++)
|
||||
{
|
||||
if (ThreadEmails[i].MailCopy.CreationDate < email.MailCopy.CreationDate)
|
||||
{
|
||||
insertIndex = i;
|
||||
break;
|
||||
}
|
||||
insertIndex = i + 1;
|
||||
}
|
||||
|
||||
ThreadEmails.Insert(insertIndex, email);
|
||||
email.PropertyChanged += ThreadEmailPropertyChanged;
|
||||
_uniqueIdSet.Add(email.MailCopy.UniqueId);
|
||||
RefreshLatestMailCache();
|
||||
OnPropertyChanged(nameof(EmailCount));
|
||||
NotifyMailItemUpdated(email, MailCopyChangeFlags.All);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes an email from this thread
|
||||
/// </summary>
|
||||
public void RemoveEmail(MailItemViewModel email)
|
||||
{
|
||||
if (ThreadEmails.Remove(email))
|
||||
{
|
||||
email.PropertyChanged -= ThreadEmailPropertyChanged;
|
||||
_uniqueIdSet.Remove(email.MailCopy.UniqueId);
|
||||
RefreshLatestMailCache();
|
||||
OnPropertyChanged(nameof(EmailCount));
|
||||
NotifyMailItemUpdated(email, MailCopyChangeFlags.All);
|
||||
}
|
||||
}
|
||||
|
||||
public void UnregisterThreadEmailPropertyChangedHandlers()
|
||||
{
|
||||
foreach (var email in ThreadEmails)
|
||||
{
|
||||
email.PropertyChanged -= ThreadEmailPropertyChanged;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void ThreadEmailPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (_suspendChildPropertyNotificationsCount > 0)
|
||||
return;
|
||||
|
||||
if (sender is not MailItemViewModel updatedMailItem)
|
||||
return;
|
||||
|
||||
if (e.PropertyName == nameof(MailItemViewModel.IsSelected) ||
|
||||
e.PropertyName == nameof(MailItemViewModel.IsDisplayedInThread) ||
|
||||
e.PropertyName == nameof(MailItemViewModel.IsBusy))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.PropertyName == nameof(MailItemViewModel.ThumbnailUpdatedEvent))
|
||||
{
|
||||
if (ReferenceEquals(updatedMailItem, latestMailViewModel))
|
||||
{
|
||||
OnPropertyChanged(nameof(ThumbnailUpdatedEvent));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var changedFlags = string.IsNullOrEmpty(e.PropertyName)
|
||||
? MailCopyChangeFlags.All
|
||||
: MailItemViewModel.GetChangeFlagsForProperty(e.PropertyName);
|
||||
|
||||
if (changedFlags == MailCopyChangeFlags.None)
|
||||
{
|
||||
NotifyMailItemUpdated(updatedMailItem, MailCopyChangeFlags.All);
|
||||
return;
|
||||
}
|
||||
|
||||
NotifyMailItemUpdated(updatedMailItem, changedFlags);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notifies that a mail item within this thread has been updated.
|
||||
/// </summary>
|
||||
/// <param name="updatedMailItem">The mail item that was updated (can be null to refresh all).</param>
|
||||
/// <param name="changedFlags">Set of changed child fields.</param>
|
||||
public void NotifyMailItemUpdated(MailItemViewModel updatedMailItem, MailCopyChangeFlags changedFlags = MailCopyChangeFlags.All)
|
||||
{
|
||||
if (changedFlags == MailCopyChangeFlags.None)
|
||||
return;
|
||||
|
||||
var previousLatest = latestMailViewModel;
|
||||
|
||||
if (changedFlags == MailCopyChangeFlags.All ||
|
||||
(changedFlags & MailCopyChangeFlags.CreationDate) != 0 ||
|
||||
previousLatest == null ||
|
||||
!ThreadEmails.Contains(previousLatest))
|
||||
{
|
||||
RefreshLatestMailCache();
|
||||
}
|
||||
|
||||
var currentLatest = latestMailViewModel;
|
||||
var latestChanged = !ReferenceEquals(previousLatest, currentLatest);
|
||||
|
||||
var updatesDisplayedLatest = changedFlags == MailCopyChangeFlags.All ||
|
||||
updatedMailItem == null ||
|
||||
latestChanged ||
|
||||
ReferenceEquals(updatedMailItem, previousLatest) ||
|
||||
ReferenceEquals(updatedMailItem, currentLatest);
|
||||
|
||||
var changedProperties = new List<string>(10);
|
||||
|
||||
void Queue(string propertyName)
|
||||
{
|
||||
if (!changedProperties.Contains(propertyName))
|
||||
{
|
||||
changedProperties.Add(propertyName);
|
||||
}
|
||||
}
|
||||
|
||||
if (updatesDisplayedLatest)
|
||||
{
|
||||
if (changedFlags == MailCopyChangeFlags.All || latestChanged)
|
||||
{
|
||||
Queue(nameof(Subject));
|
||||
Queue(nameof(FromName));
|
||||
Queue(nameof(CreationDate));
|
||||
Queue(nameof(FromAddress));
|
||||
Queue(nameof(PreviewText));
|
||||
Queue(nameof(IsFocused));
|
||||
Queue(nameof(DraftId));
|
||||
Queue(nameof(Id));
|
||||
Queue(nameof(Importance));
|
||||
Queue(nameof(ThreadId));
|
||||
Queue(nameof(MessageId));
|
||||
Queue(nameof(References));
|
||||
Queue(nameof(InReplyTo));
|
||||
Queue(nameof(FileId));
|
||||
Queue(nameof(FolderId));
|
||||
Queue(nameof(UniqueId));
|
||||
Queue(nameof(ContactPictureFileId));
|
||||
Queue(nameof(SenderContact));
|
||||
Queue(nameof(ThumbnailUpdatedEvent));
|
||||
Queue(nameof(SortingDate));
|
||||
Queue(nameof(SortingName));
|
||||
}
|
||||
else
|
||||
{
|
||||
if ((changedFlags & MailCopyChangeFlags.Subject) != 0)
|
||||
Queue(nameof(Subject));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.FromName) != 0)
|
||||
{
|
||||
Queue(nameof(FromName));
|
||||
Queue(nameof(SortingName));
|
||||
}
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.CreationDate) != 0)
|
||||
{
|
||||
Queue(nameof(CreationDate));
|
||||
Queue(nameof(SortingDate));
|
||||
}
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.FromAddress) != 0)
|
||||
Queue(nameof(FromAddress));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.PreviewText) != 0)
|
||||
Queue(nameof(PreviewText));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.IsFocused) != 0)
|
||||
Queue(nameof(IsFocused));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.DraftId) != 0)
|
||||
Queue(nameof(DraftId));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.Id) != 0)
|
||||
Queue(nameof(Id));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.Importance) != 0)
|
||||
Queue(nameof(Importance));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.ThreadId) != 0)
|
||||
Queue(nameof(ThreadId));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.MessageId) != 0)
|
||||
Queue(nameof(MessageId));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.References) != 0)
|
||||
Queue(nameof(References));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.InReplyTo) != 0)
|
||||
Queue(nameof(InReplyTo));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.FileId) != 0)
|
||||
Queue(nameof(FileId));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.FolderId) != 0)
|
||||
Queue(nameof(FolderId));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.UniqueId) != 0)
|
||||
Queue(nameof(UniqueId));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.SenderContact) != 0)
|
||||
{
|
||||
Queue(nameof(ContactPictureFileId));
|
||||
Queue(nameof(SenderContact));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.HasAttachments) != 0 || changedFlags == MailCopyChangeFlags.All)
|
||||
Queue(nameof(HasAttachments));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.ItemType) != 0 || changedFlags == MailCopyChangeFlags.All)
|
||||
Queue(nameof(IsCalendarEvent));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.IsFlagged) != 0 || changedFlags == MailCopyChangeFlags.All)
|
||||
Queue(nameof(IsFlagged));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.IsRead) != 0 || changedFlags == MailCopyChangeFlags.All)
|
||||
Queue(nameof(IsRead));
|
||||
|
||||
if ((changedFlags & MailCopyChangeFlags.IsDraft) != 0 || changedFlags == MailCopyChangeFlags.All)
|
||||
Queue(nameof(IsDraft));
|
||||
|
||||
foreach (var changedProperty in changedProperties)
|
||||
{
|
||||
OnPropertyChanged(changedProperty);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if this thread contains an email with the specified unique ID
|
||||
/// </summary>
|
||||
public bool HasUniqueId(Guid uniqueId) => _uniqueIdSet.Contains(uniqueId);
|
||||
|
||||
public IEnumerable<Guid> GetContainingIds() => ThreadEmails.Select(a => a.MailCopy.UniqueId);
|
||||
|
||||
public IEnumerable<MailItemViewModel> GetSelectedMailItems()
|
||||
{
|
||||
if (IsSelected)
|
||||
{
|
||||
// If the thread itself is selected, return all emails in the thread
|
||||
return ThreadEmails;
|
||||
}
|
||||
else
|
||||
Debugger.Break();
|
||||
{
|
||||
// Otherwise, return only individually selected emails within the thread
|
||||
return ThreadEmails.Where(e => e.IsSelected);
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasUniqueId(Guid uniqueMailId)
|
||||
=> ThreadItems.Any(a => a.UniqueId == uniqueMailId);
|
||||
|
||||
public IMailItem GetItemById(Guid uniqueMailId)
|
||||
=> ThreadItems.FirstOrDefault(a => a.UniqueId == uniqueMailId);
|
||||
|
||||
public void RemoveCopyItem(IMailItem item)
|
||||
{
|
||||
MailCopy copyToRemove = null;
|
||||
|
||||
if (item is MailItemViewModel mailItemViewModel)
|
||||
copyToRemove = mailItemViewModel.MailCopy;
|
||||
else if (item is MailCopy copyItem)
|
||||
copyToRemove = copyItem;
|
||||
|
||||
var existedItem = ThreadItems.FirstOrDefault(a => a.Id == copyToRemove.Id);
|
||||
|
||||
if (existedItem == null) return;
|
||||
|
||||
ThreadItems.Remove(existedItem);
|
||||
|
||||
NotifyPropertyChanges();
|
||||
}
|
||||
|
||||
public void NotifyPropertyChanges()
|
||||
{
|
||||
// TODO
|
||||
// Stupid temporary fix for not updating UI.
|
||||
// This view model must be reworked with ThreadMailItem together.
|
||||
|
||||
var current = MailItem;
|
||||
|
||||
MailItem = null;
|
||||
MailItem = current;
|
||||
}
|
||||
|
||||
public IMailItem LatestMailItem => ((IMailItemThread)MailItem).LatestMailItem;
|
||||
public IMailItem FirstMailItem => ((IMailItemThread)MailItem).FirstMailItem;
|
||||
|
||||
public string Id => ((IMailItem)MailItem).Id;
|
||||
public string Subject => ((IMailItem)MailItem).Subject;
|
||||
public string ThreadId => ((IMailItem)MailItem).ThreadId;
|
||||
public string MessageId => ((IMailItem)MailItem).MessageId;
|
||||
public string References => ((IMailItem)MailItem).References;
|
||||
public string PreviewText => ((IMailItem)MailItem).PreviewText;
|
||||
public string FromName => ((IMailItem)MailItem).FromName;
|
||||
public DateTime CreationDate => ((IMailItem)MailItem).CreationDate;
|
||||
public string FromAddress => ((IMailItem)MailItem).FromAddress;
|
||||
public bool HasAttachments => ((IMailItem)MailItem).HasAttachments;
|
||||
public bool IsFlagged => ((IMailItem)MailItem).IsFlagged;
|
||||
public bool IsFocused => ((IMailItem)MailItem).IsFocused;
|
||||
public bool IsRead => ((IMailItem)MailItem).IsRead;
|
||||
public bool IsDraft => ((IMailItem)MailItem).IsDraft;
|
||||
public string DraftId => string.Empty;
|
||||
public string InReplyTo => ((IMailItem)MailItem).InReplyTo;
|
||||
|
||||
public MailItemFolder AssignedFolder => ((IMailItem)MailItem).AssignedFolder;
|
||||
|
||||
public MailAccount AssignedAccount => ((IMailItem)MailItem).AssignedAccount;
|
||||
|
||||
public Guid UniqueId => ((IMailItem)MailItem).UniqueId;
|
||||
|
||||
public Guid FileId => ((IMailItem)MailItem).FileId;
|
||||
|
||||
public int CompareTo(DateTime other) => CreationDate.CompareTo(other);
|
||||
public int CompareTo(string other) => FromName.CompareTo(other);
|
||||
|
||||
// Get single mail item view model out of the only item in thread items.
|
||||
public MailItemViewModel GetSingleItemViewModel() => ThreadItems.First() as MailItemViewModel;
|
||||
|
||||
public IEnumerable<Guid> GetContainingIds() => ((IMailItemThread)MailItem).GetContainingIds();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Accounts;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Data;
|
||||
|
||||
public partial class WelcomeWizardContext : ObservableObject
|
||||
{
|
||||
// Step 2 — Provider selection
|
||||
[ObservableProperty]
|
||||
public partial IProviderDetail SelectedProvider { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string AccountName { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string AccountColorHex { get; set; }
|
||||
|
||||
// Special IMAP fields (iCloud/Yahoo)
|
||||
[ObservableProperty]
|
||||
public partial string DisplayName { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string EmailAddress { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string AppSpecificPassword { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ImapCalendarSupportMode CalendarSupportMode { get; set; } = ImapCalendarSupportMode.Disabled;
|
||||
|
||||
// Generic IMAP — populated by ImapCalDavSettingsPage
|
||||
public ImapCalDavSetupResult ImapCalDavSetupResult { get; set; }
|
||||
|
||||
// Computed helpers
|
||||
public bool IsOAuthProvider => SelectedProvider?.Type is MailProviderType.Outlook or MailProviderType.Gmail;
|
||||
|
||||
public bool IsSpecialImapProvider =>
|
||||
SelectedProvider?.SpecialImapProvider is SpecialImapProvider.iCloud or SpecialImapProvider.Yahoo;
|
||||
|
||||
public bool IsGenericImap =>
|
||||
SelectedProvider?.Type == MailProviderType.IMAP4
|
||||
&& SelectedProvider?.SpecialImapProvider == SpecialImapProvider.None;
|
||||
|
||||
public SpecialImapProviderDetails BuildSpecialImapProviderDetails()
|
||||
{
|
||||
if (!IsSpecialImapProvider) return null;
|
||||
|
||||
return new SpecialImapProviderDetails(
|
||||
EmailAddress,
|
||||
AppSpecificPassword,
|
||||
DisplayName,
|
||||
SelectedProvider.SpecialImapProvider,
|
||||
CalendarSupportMode);
|
||||
}
|
||||
|
||||
public AccountCreationDialogResult BuildAccountCreationDialogResult()
|
||||
{
|
||||
return new AccountCreationDialogResult(
|
||||
SelectedProvider.Type,
|
||||
AccountName,
|
||||
BuildSpecialImapProviderDetails(),
|
||||
AccountColorHex);
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
SelectedProvider = null;
|
||||
AccountName = null;
|
||||
AccountColorHex = null;
|
||||
DisplayName = null;
|
||||
EmailAddress = null;
|
||||
AppSpecificPassword = null;
|
||||
CalendarSupportMode = ImapCalendarSupportMode.Disabled;
|
||||
ImapCalDavSetupResult = null;
|
||||
}
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Accounts;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.ViewModels.Data;
|
||||
using Wino.Messaging.Client.Navigation;
|
||||
|
||||
namespace Wino.Mail.ViewModels;
|
||||
|
||||
public partial class EditAccountDetailsPageViewModel : MailBaseViewModel
|
||||
{
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly IThemeService _themeService;
|
||||
private readonly IImapTestService _imapTestService;
|
||||
private readonly IMailDialogService _mailDialogService;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial MailAccount Account { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string AccountName { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string SenderName { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial AppColorViewModel SelectedColor { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsImapServer))]
|
||||
public partial CustomServerInformation ServerInformation { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial List<AppColorViewModel> AvailableColors { get; set; }
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int SelectedIncomingServerConnectionSecurityIndex { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int SelectedIncomingServerAuthenticationMethodIndex { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int SelectedOutgoingServerConnectionSecurityIndex { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int SelectedOutgoingServerAuthenticationMethodIndex { get; set; }
|
||||
|
||||
public List<ImapAuthenticationMethodModel> AvailableAuthenticationMethods { get; } =
|
||||
[
|
||||
new ImapAuthenticationMethodModel(Core.Domain.Enums.ImapAuthenticationMethod.Auto, Translator.ImapAuthenticationMethod_Auto),
|
||||
new ImapAuthenticationMethodModel(Core.Domain.Enums.ImapAuthenticationMethod.None, Translator.ImapAuthenticationMethod_None),
|
||||
new ImapAuthenticationMethodModel(Core.Domain.Enums.ImapAuthenticationMethod.NormalPassword, Translator.ImapAuthenticationMethod_Plain),
|
||||
new ImapAuthenticationMethodModel(Core.Domain.Enums.ImapAuthenticationMethod.EncryptedPassword, Translator.ImapAuthenticationMethod_EncryptedPassword),
|
||||
new ImapAuthenticationMethodModel(Core.Domain.Enums.ImapAuthenticationMethod.Ntlm, Translator.ImapAuthenticationMethod_Ntlm),
|
||||
new ImapAuthenticationMethodModel(Core.Domain.Enums.ImapAuthenticationMethod.CramMd5, Translator.ImapAuthenticationMethod_CramMD5),
|
||||
new ImapAuthenticationMethodModel(Core.Domain.Enums.ImapAuthenticationMethod.DigestMd5, Translator.ImapAuthenticationMethod_DigestMD5)
|
||||
];
|
||||
|
||||
public List<ImapConnectionSecurityModel> AvailableConnectionSecurities { get; set; } =
|
||||
[
|
||||
new ImapConnectionSecurityModel(Core.Domain.Enums.ImapConnectionSecurity.Auto, Translator.ImapConnectionSecurity_Auto),
|
||||
new ImapConnectionSecurityModel(Core.Domain.Enums.ImapConnectionSecurity.SslTls, Translator.ImapConnectionSecurity_SslTls),
|
||||
new ImapConnectionSecurityModel(Core.Domain.Enums.ImapConnectionSecurity.StartTls, Translator.ImapConnectionSecurity_StartTls),
|
||||
new ImapConnectionSecurityModel(Core.Domain.Enums.ImapConnectionSecurity.None, Translator.ImapConnectionSecurity_None)
|
||||
];
|
||||
|
||||
public bool IsImapServer => ServerInformation != null;
|
||||
|
||||
public EditAccountDetailsPageViewModel(IAccountService accountService,
|
||||
IThemeService themeService,
|
||||
IImapTestService imapTestService,
|
||||
IMailDialogService mailDialogService)
|
||||
{
|
||||
_accountService = accountService;
|
||||
_themeService = themeService;
|
||||
_imapTestService = imapTestService;
|
||||
_mailDialogService = mailDialogService;
|
||||
|
||||
var colorHexList = _themeService.GetAvailableAccountColors();
|
||||
|
||||
AvailableColors = colorHexList.Select(a => new AppColorViewModel(a)).ToList();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SaveChangesAsync()
|
||||
{
|
||||
await UpdateAccountAsync();
|
||||
|
||||
Messenger.Send(new BackBreadcrumNavigationRequested());
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ValidateImapSettingsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _imapTestService.TestImapConnectionAsync(ServerInformation, true);
|
||||
_mailDialogService.InfoBarMessage(Translator.IMAPSetupDialog_ValidationSuccess_Title, Translator.IMAPSetupDialog_ValidationSuccess_Message, Core.Domain.Enums.InfoBarMessageType.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_mailDialogService.InfoBarMessage(Translator.IMAPSetupDialog_ValidationFailed_Title, ex.Message, Core.Domain.Enums.InfoBarMessageType.Error); ;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task UpdateCustomServerInformationAsync()
|
||||
{
|
||||
if (ServerInformation != null)
|
||||
{
|
||||
ServerInformation.IncomingAuthenticationMethod = AvailableAuthenticationMethods[SelectedIncomingServerAuthenticationMethodIndex].ImapAuthenticationMethod;
|
||||
ServerInformation.IncomingServerSocketOption = AvailableConnectionSecurities[SelectedIncomingServerConnectionSecurityIndex].ImapConnectionSecurity;
|
||||
|
||||
ServerInformation.OutgoingAuthenticationMethod = AvailableAuthenticationMethods[SelectedOutgoingServerAuthenticationMethodIndex].ImapAuthenticationMethod;
|
||||
ServerInformation.OutgoingServerSocketOption = AvailableConnectionSecurities[SelectedOutgoingServerConnectionSecurityIndex].ImapConnectionSecurity;
|
||||
|
||||
Account.ServerInformation = ServerInformation;
|
||||
}
|
||||
|
||||
await _accountService.UpdateAccountCustomServerInformationAsync(Account.ServerInformation);
|
||||
|
||||
_mailDialogService.InfoBarMessage(Translator.IMAPSetupDialog_SaveImapSuccess_Title, Translator.IMAPSetupDialog_SaveImapSuccess_Message, Core.Domain.Enums.InfoBarMessageType.Success);
|
||||
}
|
||||
|
||||
private Task UpdateAccountAsync()
|
||||
{
|
||||
Account.Name = AccountName;
|
||||
Account.SenderName = SenderName;
|
||||
Account.AccountColorHex = SelectedColor == null ? string.Empty : SelectedColor.Hex;
|
||||
|
||||
return _accountService.UpdateAccountAsync(Account);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ResetColor()
|
||||
=> SelectedColor = null;
|
||||
|
||||
partial void OnSelectedColorChanged(AppColorViewModel oldValue, AppColorViewModel newValue)
|
||||
{
|
||||
_ = UpdateAccountAsync();
|
||||
}
|
||||
|
||||
public override void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedTo(mode, parameters);
|
||||
|
||||
if (parameters is MailAccount account)
|
||||
{
|
||||
Account = account;
|
||||
AccountName = account.Name;
|
||||
SenderName = account.SenderName;
|
||||
ServerInformation = Account.ServerInformation;
|
||||
|
||||
if (!string.IsNullOrEmpty(account.AccountColorHex))
|
||||
{
|
||||
SelectedColor = AvailableColors.FirstOrDefault(a => a.Hex == account.AccountColorHex);
|
||||
}
|
||||
|
||||
if (ServerInformation != null)
|
||||
{
|
||||
SelectedIncomingServerAuthenticationMethodIndex = AvailableAuthenticationMethods.FindIndex(a => a.ImapAuthenticationMethod == ServerInformation.IncomingAuthenticationMethod);
|
||||
SelectedIncomingServerConnectionSecurityIndex = AvailableConnectionSecurities.FindIndex(a => a.ImapConnectionSecurity == ServerInformation.IncomingServerSocketOption);
|
||||
|
||||
SelectedOutgoingServerAuthenticationMethodIndex = AvailableAuthenticationMethods.FindIndex(a => a.ImapAuthenticationMethod == ServerInformation.OutgoingAuthenticationMethod);
|
||||
SelectedOutgoingServerConnectionSecurityIndex = AvailableConnectionSecurities.FindIndex(a => a.ImapConnectionSecurity == ServerInformation.OutgoingServerSocketOption);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Messaging.Client.Navigation;
|
||||
|
||||
namespace Wino.Mail.ViewModels;
|
||||
|
||||
public partial class EmailTemplatesPageViewModel(IEmailTemplateService emailTemplateService) : MailBaseViewModel
|
||||
{
|
||||
private readonly IEmailTemplateService _emailTemplateService = emailTemplateService;
|
||||
|
||||
public ObservableCollection<EmailTemplate> EmailTemplates { get; } = [];
|
||||
|
||||
public async Task LoadAsync()
|
||||
{
|
||||
var templates = await _emailTemplateService.GetEmailTemplatesAsync().ConfigureAwait(false);
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
EmailTemplates.Clear();
|
||||
|
||||
foreach (var template in templates)
|
||||
{
|
||||
EmailTemplates.Add(template);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void CreateTemplate()
|
||||
{
|
||||
Messenger.Send(new BreadcrumbNavigationRequested(
|
||||
Translator.SettingsEmailTemplates_CreatePageTitle,
|
||||
WinoPage.CreateEmailTemplatePage));
|
||||
}
|
||||
|
||||
public void OpenTemplate(EmailTemplate template)
|
||||
{
|
||||
if (template == null)
|
||||
return;
|
||||
|
||||
var title = string.IsNullOrWhiteSpace(template.Name)
|
||||
? Translator.SettingsEmailTemplates_EditPageTitle
|
||||
: template.Name;
|
||||
|
||||
Messenger.Send(new BreadcrumbNavigationRequested(
|
||||
title,
|
||||
WinoPage.CreateEmailTemplatePage,
|
||||
template.Id));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,46 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.Domain.Models.Translations;
|
||||
|
||||
namespace Wino.Mail.ViewModels;
|
||||
|
||||
public partial class LanguageTimePageViewModel(IPreferencesService preferencesService, ITranslationService translationService) : MailBaseViewModel
|
||||
{
|
||||
public IPreferencesService PreferencesService { get; } = preferencesService;
|
||||
private readonly ITranslationService _translationService = translationService;
|
||||
|
||||
[ObservableProperty]
|
||||
private List<AppLanguageModel> _availableLanguages;
|
||||
|
||||
[ObservableProperty]
|
||||
private AppLanguageModel _selectedLanguage;
|
||||
|
||||
private bool isInitialized = false;
|
||||
|
||||
public override void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedTo(mode, parameters);
|
||||
|
||||
AvailableLanguages = _translationService.GetAvailableLanguages();
|
||||
|
||||
SelectedLanguage = AvailableLanguages.FirstOrDefault(a => a.Language == PreferencesService.CurrentLanguage);
|
||||
|
||||
isInitialized = true;
|
||||
}
|
||||
|
||||
protected override async void OnPropertyChanged(PropertyChangedEventArgs e)
|
||||
{
|
||||
base.OnPropertyChanged(e);
|
||||
|
||||
if (!isInitialized) return;
|
||||
|
||||
if (e.PropertyName == nameof(SelectedLanguage))
|
||||
{
|
||||
await _translationService.InitializeLanguageAsync(SelectedLanguage.Language);
|
||||
}
|
||||
}
|
||||
}
|
||||
+430
-153
@@ -1,11 +1,10 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using MoreLinq;
|
||||
using MoreLinq.Extensions;
|
||||
@@ -17,9 +16,12 @@ using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.MenuItems;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.Domain.Models;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
using Wino.Core.Services;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
using Wino.Messaging.Client.Accounts;
|
||||
using Wino.Messaging.Client.Navigation;
|
||||
using Wino.Messaging.Client.Shell;
|
||||
@@ -28,17 +30,19 @@ using Wino.Messaging.UI;
|
||||
|
||||
namespace Wino.Mail.ViewModels;
|
||||
|
||||
public partial class AppShellViewModel : MailBaseViewModel,
|
||||
IRecipient<NavigateManageAccountsRequested>,
|
||||
public partial class MailAppShellViewModel : MailBaseViewModel,
|
||||
IMailShellClient,
|
||||
IRecipient<MailtoProtocolMessageRequested>,
|
||||
IRecipient<RefreshUnreadCountsMessage>,
|
||||
IRecipient<AccountsMenuRefreshRequested>,
|
||||
IRecipient<MergedInboxRenamed>,
|
||||
IRecipient<LanguageChanged>,
|
||||
IRecipient<AccountMenuItemsReordered>,
|
||||
IRecipient<AccountSynchronizationProgressUpdatedMessage>,
|
||||
IRecipient<AccountSynchronizerStateChanged>,
|
||||
IRecipient<NavigateAppPreferencesRequested>,
|
||||
IRecipient<AccountFolderConfigurationUpdated>
|
||||
IRecipient<AccountFolderConfigurationUpdated>,
|
||||
IRecipient<AccountRemovedMessage>,
|
||||
IRecipient<AccountUpdatedMessage>
|
||||
{
|
||||
#region Menu Items
|
||||
|
||||
@@ -51,7 +55,8 @@ public partial class AppShellViewModel : MailBaseViewModel,
|
||||
public MenuItemCollection MenuItems { get; set; }
|
||||
|
||||
private readonly SettingsItem SettingsItem = new SettingsItem();
|
||||
private readonly ManageAccountsMenuItem ManageAccountsMenuItem = new ManageAccountsMenuItem();
|
||||
private readonly ContactsMenuItem ContactsMenuItem = new ContactsMenuItem();
|
||||
private readonly StoreUpdateMenuItem StoreUpdateMenuItem = new StoreUpdateMenuItem();
|
||||
|
||||
public IMenuItem CreateMailMenuItem = new NewMailMenuItem();
|
||||
|
||||
@@ -60,9 +65,11 @@ public partial class AppShellViewModel : MailBaseViewModel,
|
||||
private const string IsActivateStartupLaunchAskedKey = nameof(IsActivateStartupLaunchAskedKey);
|
||||
|
||||
public IStatePersistanceService StatePersistenceService { get; }
|
||||
public IWinoServerConnectionManager ServerConnectionManager { get; }
|
||||
public IPreferencesService PreferencesService { get; }
|
||||
public INavigationService NavigationService { get; }
|
||||
public WinoApplicationMode Mode => WinoApplicationMode.Mail;
|
||||
public bool HandlesNavigationSelection => true;
|
||||
public IMenuItem CreatePrimaryMenuItem => CreateMailMenuItem;
|
||||
|
||||
private readonly IFolderService _folderService;
|
||||
private readonly IConfigurationService _configurationService;
|
||||
@@ -74,20 +81,19 @@ public partial class AppShellViewModel : MailBaseViewModel,
|
||||
private readonly INotificationBuilder _notificationBuilder;
|
||||
private readonly IWinoRequestDelegator _winoRequestDelegator;
|
||||
private readonly IMailDialogService _dialogService;
|
||||
private readonly IBackgroundTaskService _backgroundTaskService;
|
||||
private readonly IMimeFileService _mimeFileService;
|
||||
private readonly IWebView2RuntimeValidatorService _webView2RuntimeValidatorService;
|
||||
private readonly IStoreUpdateService _storeUpdateService;
|
||||
|
||||
private readonly INativeAppService _nativeAppService;
|
||||
private readonly IMailService _mailService;
|
||||
private bool _hasRegisteredPersistentRecipients;
|
||||
private readonly SemaphoreSlim _menuRefreshSemaphore = new(1, 1);
|
||||
|
||||
private readonly SemaphoreSlim accountInitFolderUpdateSlim = new SemaphoreSlim(1);
|
||||
|
||||
[ObservableProperty]
|
||||
private WinoServerConnectionStatus activeConnectionStatus;
|
||||
|
||||
public AppShellViewModel(IMailDialogService dialogService,
|
||||
public MailAppShellViewModel(IMailDialogService dialogService,
|
||||
INavigationService navigationService,
|
||||
IBackgroundTaskService backgroundTaskService,
|
||||
IMimeFileService mimeFileService,
|
||||
INativeAppService nativeAppService,
|
||||
IMailService mailService,
|
||||
@@ -100,21 +106,12 @@ public partial class AppShellViewModel : MailBaseViewModel,
|
||||
IWinoRequestDelegator winoRequestDelegator,
|
||||
IFolderService folderService,
|
||||
IStatePersistanceService statePersistanceService,
|
||||
IWinoServerConnectionManager serverConnectionManager,
|
||||
IConfigurationService configurationService,
|
||||
IStartupBehaviorService startupBehaviorService)
|
||||
IStartupBehaviorService startupBehaviorService,
|
||||
IWebView2RuntimeValidatorService webView2RuntimeValidatorService,
|
||||
IStoreUpdateService storeUpdateService)
|
||||
{
|
||||
StatePersistenceService = statePersistanceService;
|
||||
ServerConnectionManager = serverConnectionManager;
|
||||
|
||||
ActiveConnectionStatus = serverConnectionManager.Status;
|
||||
ServerConnectionManager.StatusChanged += async (sender, status) =>
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
ActiveConnectionStatus = status;
|
||||
});
|
||||
};
|
||||
|
||||
PreferencesService = preferencesService;
|
||||
_dialogService = dialogService;
|
||||
@@ -122,7 +119,6 @@ public partial class AppShellViewModel : MailBaseViewModel,
|
||||
|
||||
_configurationService = configurationService;
|
||||
_startupBehaviorService = startupBehaviorService;
|
||||
_backgroundTaskService = backgroundTaskService;
|
||||
_mimeFileService = mimeFileService;
|
||||
_nativeAppService = nativeAppService;
|
||||
_mailService = mailService;
|
||||
@@ -133,11 +129,10 @@ public partial class AppShellViewModel : MailBaseViewModel,
|
||||
_launchProtocolService = launchProtocolService;
|
||||
_notificationBuilder = notificationBuilder;
|
||||
_winoRequestDelegator = winoRequestDelegator;
|
||||
_webView2RuntimeValidatorService = webView2RuntimeValidatorService;
|
||||
_storeUpdateService = storeUpdateService;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private Task ReconnectServerAsync() => ServerConnectionManager.ConnectAsync();
|
||||
|
||||
protected override void OnDispatcherAssigned()
|
||||
{
|
||||
base.OnDispatcherAssigned();
|
||||
@@ -154,23 +149,11 @@ public partial class AppShellViewModel : MailBaseViewModel,
|
||||
return _contextMenuItemService.GetFolderContextMenuActions(folder);
|
||||
}
|
||||
|
||||
private async Task CreateFooterItemsAsync()
|
||||
private async Task CreateFooterItemsAsync(bool showNotification = false)
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
// TODO: Selected footer item container still remains selected after re-creation.
|
||||
// To reproduce, go settings and change the language.
|
||||
|
||||
foreach (var item in FooterItems)
|
||||
{
|
||||
item.IsExpanded = false;
|
||||
item.IsSelected = false;
|
||||
}
|
||||
|
||||
FooterItems.Clear();
|
||||
|
||||
FooterItems.Add(ManageAccountsMenuItem);
|
||||
FooterItems.Add(SettingsItem);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -234,21 +217,82 @@ public partial class AppShellViewModel : MailBaseViewModel,
|
||||
}
|
||||
}
|
||||
|
||||
private async void PreferencesServiceChanged(object sender, string e)
|
||||
{
|
||||
if (e == nameof(IPreferencesService.IsStoreUpdateNotificationsEnabled))
|
||||
{
|
||||
await CreateFooterItemsAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedTo(mode, parameters);
|
||||
await CreateFooterItemsAsync();
|
||||
if (!_hasRegisteredPersistentRecipients)
|
||||
{
|
||||
RegisterRecipients();
|
||||
_hasRegisteredPersistentRecipients = true;
|
||||
}
|
||||
|
||||
var activationContext = parameters as ShellModeActivationContext;
|
||||
var shouldRunStartupFlows = (activationContext?.IsInitialActivation ?? true) &&
|
||||
activationContext?.SuppressStartupFlows != true;
|
||||
var hasExistingAccountMenuItems = MenuItems?.OfType<IAccountMenuItem>().Any() == true;
|
||||
|
||||
PreferencesService.PreferenceChanged -= PreferencesServiceChanged;
|
||||
PreferencesService.PreferenceChanged += PreferencesServiceChanged;
|
||||
|
||||
await CreateFooterItemsAsync(true);
|
||||
|
||||
if (!hasExistingAccountMenuItems)
|
||||
{
|
||||
await RecreateMenuItemsAsync();
|
||||
}
|
||||
|
||||
await RecreateMenuItemsAsync();
|
||||
await ProcessLaunchOptionsAsync();
|
||||
await ValidateWebView2RuntimeAsync();
|
||||
|
||||
if (!Debugger.IsAttached)
|
||||
if (shouldRunStartupFlows && !Debugger.IsAttached)
|
||||
{
|
||||
await ForceAllAccountSynchronizationsAsync();
|
||||
}
|
||||
|
||||
await MakeSureEnableStartupLaunchAsync();
|
||||
await ConfigureBackgroundTasksAsync();
|
||||
if (shouldRunStartupFlows)
|
||||
{
|
||||
await MakeSureEnableStartupLaunchAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ValidateWebView2RuntimeAsync()
|
||||
{
|
||||
var isRuntimeAvailable = await _webView2RuntimeValidatorService.IsRuntimeAvailableAsync();
|
||||
|
||||
if (!isRuntimeAvailable)
|
||||
{
|
||||
await ExecuteUIThread(() => _notificationBuilder.CreateWebView2RuntimeMissingNotification());
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnNavigatedFrom(NavigationMode mode, object parameters)
|
||||
{
|
||||
PreferencesService.PreferenceChanged -= PreferencesServiceChanged;
|
||||
}
|
||||
|
||||
public void PrepareForShellShutdown()
|
||||
{
|
||||
PreferencesService.PreferenceChanged -= PreferencesServiceChanged;
|
||||
|
||||
if (_hasRegisteredPersistentRecipients)
|
||||
{
|
||||
UnregisterRecipients();
|
||||
_hasRegisteredPersistentRecipients = false;
|
||||
}
|
||||
|
||||
latestSelectedAccountMenuItem = null;
|
||||
SelectedMenuItem = null;
|
||||
|
||||
MenuItems?.Clear();
|
||||
MenuItems?.Add(CreateMailMenuItem);
|
||||
FooterItems?.Clear();
|
||||
}
|
||||
|
||||
private async Task MakeSureEnableStartupLaunchAsync()
|
||||
@@ -291,22 +335,6 @@ public partial class AppShellViewModel : MailBaseViewModel,
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ConfigureBackgroundTasksAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// This will only unregister once. Safe to execute multiple times.
|
||||
_backgroundTaskService.UnregisterAllBackgroundTask();
|
||||
|
||||
await _backgroundTaskService.RegisterBackgroundTasksAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to configure background tasks.");
|
||||
|
||||
_dialogService.InfoBarMessage(Translator.Info_BackgroundExecutionUnknownErrorTitle, Translator.Info_BackgroundExecutionUnknownErrorMessage, InfoBarMessageType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ForceAllAccountSynchronizationsAsync()
|
||||
{
|
||||
@@ -321,7 +349,7 @@ public partial class AppShellViewModel : MailBaseViewModel,
|
||||
Type = MailSynchronizationType.FullFolders
|
||||
};
|
||||
|
||||
Messenger.Send(new NewMailSynchronizationRequested(options, SynchronizationSource.Client));
|
||||
Messenger.Send(new NewMailSynchronizationRequested(options));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,7 +410,7 @@ public partial class AppShellViewModel : MailBaseViewModel,
|
||||
{
|
||||
if (PreferencesService.StartupEntityId == null)
|
||||
{
|
||||
NavigationService.Navigate(WinoPage.WelcomePage);
|
||||
NavigateToWelcomeWizard();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -406,8 +434,8 @@ public partial class AppShellViewModel : MailBaseViewModel,
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback to welcome page if startup entity is not found.
|
||||
NavigationService.Navigate(WinoPage.WelcomePage);
|
||||
// Fallback to the welcome wizard if startup entity is not found.
|
||||
NavigateToWelcomeWizard();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -426,7 +454,7 @@ public partial class AppShellViewModel : MailBaseViewModel,
|
||||
|
||||
var args = new NavigateMailFolderEventArgs(baseFolderMenuItem, folderInitAwaitTask);
|
||||
|
||||
NavigationService.Navigate(WinoPage.MailListPage, args, NavigationReferenceFrame.ShellFrame);
|
||||
NavigationService.Navigate(WinoPage.MailListPage, args, NavigationReferenceFrame.InnerShellFrame);
|
||||
|
||||
UpdateWindowTitleForFolder(baseFolderMenuItem);
|
||||
});
|
||||
@@ -440,6 +468,11 @@ public partial class AppShellViewModel : MailBaseViewModel,
|
||||
StatePersistenceService.CoreWindowTitle = $"{folder.AssignedAccountName} - {folder.FolderName}";
|
||||
}
|
||||
|
||||
private void UpdateWindowTitle(string title)
|
||||
{
|
||||
StatePersistenceService.CoreWindowTitle = title;
|
||||
}
|
||||
|
||||
private async Task NavigateSpecialFolderAsync(MailAccount account, SpecialFolderType specialFolderType, bool extendAccountMenu)
|
||||
{
|
||||
try
|
||||
@@ -558,21 +591,75 @@ public partial class AppShellViewModel : MailBaseViewModel,
|
||||
}
|
||||
}
|
||||
|
||||
public Task HandleAccountAttentionAsync(MailAccount account)
|
||||
=> FixAccountIssuesAsync(account);
|
||||
|
||||
private void TriggerFullSynchronization(MailAccount account)
|
||||
{
|
||||
Messenger.Send(new NewMailSynchronizationRequested(new MailSynchronizationOptions
|
||||
{
|
||||
AccountId = account.Id,
|
||||
Type = MailSynchronizationType.FullFolders
|
||||
}));
|
||||
|
||||
if (account.IsCalendarAccessGranted)
|
||||
{
|
||||
Messenger.Send(new NewCalendarSynchronizationRequested(new CalendarSynchronizationOptions
|
||||
{
|
||||
AccountId = account.Id,
|
||||
Type = CalendarSynchronizationType.CalendarEvents
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task FixAccountIssuesAsync(MailAccount account)
|
||||
{
|
||||
// TODO: This area is very unclear. Needs to be rewritten with care.
|
||||
// Fix account issues are expected to not work, but may work for some cases.
|
||||
|
||||
try
|
||||
{
|
||||
if (account.AttentionReason == AccountAttentionReason.InvalidCredentials)
|
||||
await _accountService.FixTokenIssuesAsync(account.Id);
|
||||
{
|
||||
if (account.ProviderType is MailProviderType.Gmail or MailProviderType.Outlook)
|
||||
{
|
||||
await SynchronizationManager.Instance.HandleAuthorizationAsync(
|
||||
account.ProviderType,
|
||||
account,
|
||||
account.ProviderType == MailProviderType.Gmail);
|
||||
|
||||
await _accountService.ClearAccountAttentionAsync(account.Id);
|
||||
|
||||
_dialogService.InfoBarMessage(
|
||||
Translator.Info_AccountIssueFixSuccessTitle,
|
||||
Translator.Info_AccountIssueFixSuccessMessage,
|
||||
InfoBarMessageType.Success);
|
||||
|
||||
TriggerFullSynchronization(account);
|
||||
return;
|
||||
}
|
||||
|
||||
NavigationService.Navigate(WinoPage.SettingsPage, WinoPage.ManageAccountsPage);
|
||||
Messenger.Send(new BreadcrumbNavigationRequested(
|
||||
Translator.ImapCalDavSettingsPage_TitleEdit,
|
||||
WinoPage.ImapCalDavSettingsPage,
|
||||
ImapCalDavSettingsNavigationContext.CreateForEditMode(account.Id)));
|
||||
|
||||
_dialogService.InfoBarMessage(
|
||||
Translator.Info_AccountIssueFixSuccessTitle,
|
||||
Translator.Info_AccountIssueFixImapMessage,
|
||||
InfoBarMessageType.Information);
|
||||
return;
|
||||
}
|
||||
else if (account.AttentionReason == AccountAttentionReason.MissingSystemFolderConfiguration)
|
||||
{
|
||||
await _dialogService.HandleSystemFolderConfigurationDialogAsync(account.Id, _folderService);
|
||||
await _accountService.ClearAccountAttentionAsync(account.Id);
|
||||
|
||||
await _accountService.ClearAccountAttentionAsync(account.Id);
|
||||
_dialogService.InfoBarMessage(
|
||||
Translator.Info_AccountIssueFixSuccessTitle,
|
||||
Translator.Info_AccountIssueFixSuccessMessage,
|
||||
InfoBarMessageType.Success);
|
||||
|
||||
_dialogService.InfoBarMessage(Translator.Info_AccountIssueFixFailedTitle, Translator.Info_AccountIssueFixSuccessMessage, InfoBarMessageType.Success);
|
||||
TriggerFullSynchronization(account);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -619,14 +706,6 @@ public partial class AppShellViewModel : MailBaseViewModel,
|
||||
// Don't navigate to merged account if it's already selected. Preserve user's already selected folder.
|
||||
await ChangeLoadedAccountAsync(clickedMergedAccountMenuItem, true);
|
||||
}
|
||||
else if (clickedMenuItem is SettingsItem)
|
||||
{
|
||||
NavigationService.Navigate(WinoPage.SettingsPage, parameter, NavigationReferenceFrame.ShellFrame, NavigationTransitionType.None);
|
||||
}
|
||||
else if (clickedMenuItem is ManageAccountsMenuItem)
|
||||
{
|
||||
NavigationService.Navigate(WinoPage.ManageAccountsPage, parameter, NavigationReferenceFrame.ShellFrame, NavigationTransitionType.None);
|
||||
}
|
||||
else if (clickedMenuItem is IAccountMenuItem clickedAccountMenuItem)
|
||||
{
|
||||
// Changing loaded account.
|
||||
@@ -801,7 +880,7 @@ public partial class AppShellViewModel : MailBaseViewModel,
|
||||
|
||||
if (isManageAccountClicked)
|
||||
{
|
||||
SelectedMenuItem = ManageAccountsMenuItem;
|
||||
NavigationService.Navigate(WinoPage.SettingsPage, WinoPage.ManageAccountsPage);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -874,49 +953,20 @@ public partial class AppShellViewModel : MailBaseViewModel,
|
||||
await _winoRequestDelegator.ExecuteAsync(draftPreparationRequest);
|
||||
}
|
||||
|
||||
protected override async void OnAccountUpdated(MailAccount updatedAccount)
|
||||
public override async Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args)
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
if (args.Handled || args.Mode != WinoApplicationMode.Mail)
|
||||
return;
|
||||
|
||||
if (args.Action == KeyboardShortcutAction.NewMail)
|
||||
{
|
||||
if (MenuItems.TryGetAccountMenuItem(updatedAccount.Id, out IAccountMenuItem foundAccountMenuItem))
|
||||
{
|
||||
foundAccountMenuItem.UpdateAccount(updatedAccount);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override void OnAccountRemoved(MailAccount removedAccount)
|
||||
=> Messenger.Send(new AccountsMenuRefreshRequested(false));
|
||||
|
||||
protected override async void OnAccountCreated(MailAccount createdAccount)
|
||||
{
|
||||
latestSelectedAccountMenuItem = null;
|
||||
|
||||
await RecreateMenuItemsAsync();
|
||||
|
||||
if (!MenuItems.TryGetAccountMenuItem(createdAccount.Id, out IAccountMenuItem createdMenuItem)) return;
|
||||
|
||||
await ChangeLoadedAccountAsync(createdMenuItem);
|
||||
|
||||
// Each created account should start a new synchronization automatically.
|
||||
var options = new MailSynchronizationOptions()
|
||||
{
|
||||
AccountId = createdAccount.Id,
|
||||
Type = MailSynchronizationType.FullFolders,
|
||||
};
|
||||
|
||||
Messenger.Send(new NewMailSynchronizationRequested(options, SynchronizationSource.Client));
|
||||
|
||||
try
|
||||
{
|
||||
await _nativeAppService.PinAppToTaskbarAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to pin Wino to taskbar.");
|
||||
await HandleCreateNewMailAsync();
|
||||
args.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// TODO: Handle by messaging.
|
||||
private async Task SetAccountAttentionAsync(Guid accountId, AccountAttentionReason reason)
|
||||
{
|
||||
@@ -931,8 +981,6 @@ public partial class AppShellViewModel : MailBaseViewModel,
|
||||
accountMenuItem.UpdateAccount(accountModel);
|
||||
}
|
||||
|
||||
public void Receive(NavigateManageAccountsRequested message) => SelectedMenuItem = ManageAccountsMenuItem;
|
||||
|
||||
public async void Receive(MailtoProtocolMessageRequested message)
|
||||
{
|
||||
var accounts = await _accountService.GetAccountsAsync();
|
||||
@@ -963,13 +1011,94 @@ public partial class AppShellViewModel : MailBaseViewModel,
|
||||
|
||||
private async Task RecreateMenuItemsAsync()
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
await _menuRefreshSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
MenuItems.Clear();
|
||||
MenuItems.Add(CreateMailMenuItem);
|
||||
});
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
MenuItems.Clear();
|
||||
MenuItems.Add(CreateMailMenuItem);
|
||||
});
|
||||
|
||||
await LoadAccountsAsync();
|
||||
await LoadAccountsAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_menuRefreshSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RestoreSelectedAccountAfterMenuRefreshAsync(bool automaticallyNavigateFirstItem)
|
||||
{
|
||||
IAccountMenuItem validSelectedMenuItem = null;
|
||||
bool hasPreviousSelection = latestSelectedAccountMenuItem != null;
|
||||
|
||||
if (hasPreviousSelection)
|
||||
{
|
||||
var selectedEntityId = latestSelectedAccountMenuItem.EntityId.GetValueOrDefault();
|
||||
|
||||
if (selectedEntityId != Guid.Empty &&
|
||||
MenuItems.TryGetAccountMenuItem(selectedEntityId, out IAccountMenuItem foundSelectedMenuItem))
|
||||
{
|
||||
validSelectedMenuItem = foundSelectedMenuItem;
|
||||
}
|
||||
else
|
||||
{
|
||||
latestSelectedAccountMenuItem = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (validSelectedMenuItem == null)
|
||||
{
|
||||
validSelectedMenuItem = MenuItems.FirstOrDefault(a => a is IAccountMenuItem) as IAccountMenuItem;
|
||||
hasPreviousSelection = false;
|
||||
}
|
||||
|
||||
if (validSelectedMenuItem != null)
|
||||
{
|
||||
await ChangeLoadedAccountAsync(validSelectedMenuItem, hasPreviousSelection || automaticallyNavigateFirstItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ExecuteUIThread(() => SelectedMenuItem = null);
|
||||
NavigateToWelcomeWizard();
|
||||
}
|
||||
}
|
||||
|
||||
private void NavigateToWelcomeWizard()
|
||||
=> NavigationService.Navigate(
|
||||
WinoPage.WelcomeHostPage,
|
||||
null,
|
||||
NavigationReferenceFrame.ShellFrame,
|
||||
NavigationTransitionType.None);
|
||||
|
||||
private bool IsAccountCurrentlyLoaded(Guid accountId)
|
||||
{
|
||||
return latestSelectedAccountMenuItem?.HoldingAccounts?.Any(a => a.Id == accountId) == true;
|
||||
}
|
||||
|
||||
private async Task RefreshLoadedAccountFolderStructureAsync(Guid accountId)
|
||||
{
|
||||
if (!IsAccountCurrentlyLoaded(accountId) || latestSelectedAccountMenuItem == null)
|
||||
return;
|
||||
|
||||
var selectedFolderId = (SelectedMenuItem as IBaseFolderMenuItem)?.HandlingFolders
|
||||
?.FirstOrDefault(a => a.MailAccountId == accountId)?.Id;
|
||||
|
||||
var folders = await _folderService.GetAccountFoldersForDisplayAsync(latestSelectedAccountMenuItem);
|
||||
|
||||
await MenuItems.ReplaceFoldersAsync(folders);
|
||||
await UpdateUnreadItemCountAsync();
|
||||
|
||||
if (selectedFolderId.HasValue &&
|
||||
MenuItems.TryGetFolderMenuItem(selectedFolderId.Value, out IBaseFolderMenuItem selectedFolderMenuItem))
|
||||
{
|
||||
await NavigateFolderAsync(selectedFolderMenuItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
await NavigateInboxAsync(latestSelectedAccountMenuItem);
|
||||
}
|
||||
}
|
||||
|
||||
public async void Receive(RefreshUnreadCountsMessage message)
|
||||
@@ -978,27 +1107,12 @@ public partial class AppShellViewModel : MailBaseViewModel,
|
||||
public async void Receive(AccountsMenuRefreshRequested message)
|
||||
{
|
||||
await RecreateMenuItemsAsync();
|
||||
|
||||
// Try to restore latest selected account.
|
||||
if (latestSelectedAccountMenuItem != null)
|
||||
{
|
||||
await ChangeLoadedAccountAsync(latestSelectedAccountMenuItem, navigateInbox: true);
|
||||
}
|
||||
else if (MenuItems.FirstOrDefault(a => a is IAccountMenuItem) is IAccountMenuItem firstAccount)
|
||||
{
|
||||
await ChangeLoadedAccountAsync(firstAccount, message.AutomaticallyNavigateFirstItem);
|
||||
}
|
||||
await RestoreSelectedAccountAfterMenuRefreshAsync(message.AutomaticallyNavigateFirstItem);
|
||||
}
|
||||
|
||||
public async void Receive(AccountFolderConfigurationUpdated message)
|
||||
{
|
||||
// Reloading of folders is needed to re-create folder tree if the account is loaded.
|
||||
|
||||
if (MenuItems.TryGetAccountMenuItem(message.AccountId, out IAccountMenuItem accountMenuItem) &&
|
||||
latestSelectedAccountMenuItem == accountMenuItem)
|
||||
{
|
||||
await ChangeLoadedAccountAsync(accountMenuItem, true);
|
||||
}
|
||||
await RefreshLoadedAccountFolderStructureAsync(message.AccountId);
|
||||
}
|
||||
|
||||
public async void Receive(MergedInboxRenamed message)
|
||||
@@ -1015,10 +1129,9 @@ public partial class AppShellViewModel : MailBaseViewModel,
|
||||
|
||||
public async void Receive(LanguageChanged message)
|
||||
{
|
||||
await CreateFooterItemsAsync();
|
||||
await CreateFooterItemsAsync(true);
|
||||
await RecreateMenuItemsAsync();
|
||||
|
||||
await ChangeLoadedAccountAsync(latestSelectedAccountMenuItem, navigateInbox: false);
|
||||
await RestoreSelectedAccountAfterMenuRefreshAsync(false);
|
||||
}
|
||||
|
||||
private void ReorderAccountMenuItems(Dictionary<Guid, int> newAccountOrder)
|
||||
@@ -1056,6 +1169,24 @@ public partial class AppShellViewModel : MailBaseViewModel,
|
||||
UpdateFolderCollection(mailItemFolder);
|
||||
}
|
||||
|
||||
protected override async void OnFolderDeleted(MailItemFolder folder)
|
||||
{
|
||||
base.OnFolderDeleted(folder);
|
||||
|
||||
bool wasSelected = SelectedMenuItem is IBaseFolderMenuItem selectedFolder &&
|
||||
selectedFolder.HandlingFolders.Any(a => a.Id == folder.Id);
|
||||
|
||||
await ExecuteUIThread(() => MenuItems.RemoveFolderMenuItem(folder.Id));
|
||||
|
||||
if (wasSelected && latestSelectedAccountMenuItem != null)
|
||||
{
|
||||
await NavigateInboxAsync(latestSelectedAccountMenuItem);
|
||||
return;
|
||||
}
|
||||
|
||||
await RefreshLoadedAccountFolderStructureAsync(folder.MailAccountId);
|
||||
}
|
||||
|
||||
protected override void OnFolderSynchronizationEnabled(IMailItemFolder mailItemFolder)
|
||||
{
|
||||
base.OnFolderSynchronizationEnabled(mailItemFolder);
|
||||
@@ -1063,17 +1194,163 @@ public partial class AppShellViewModel : MailBaseViewModel,
|
||||
UpdateFolderCollection(mailItemFolder);
|
||||
}
|
||||
|
||||
public async void Receive(AccountSynchronizationProgressUpdatedMessage message)
|
||||
public async void Receive(AccountSynchronizerStateChanged message)
|
||||
{
|
||||
var accountMenuItem = MenuItems.GetSpecificAccountMenuItem(message.AccountId);
|
||||
|
||||
if (accountMenuItem == null) return;
|
||||
|
||||
await ExecuteUIThread(() => { accountMenuItem.SynchronizationProgress = message.Progress; });
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
accountMenuItem.TotalItemsToSync = message.TotalItemsToSync;
|
||||
accountMenuItem.RemainingItemsToSync = message.RemainingItemsToSync;
|
||||
accountMenuItem.SynchronizationStatus = message.SynchronizationStatus;
|
||||
|
||||
// If this account is part of a merged inbox, update the merged inbox progress as well
|
||||
if (accountMenuItem.ParentMenuItem is MergedAccountMenuItem mergedAccountMenuItem)
|
||||
{
|
||||
mergedAccountMenuItem.RefreshSynchronizationProgress();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async void Receive(NavigateAppPreferencesRequested message)
|
||||
{
|
||||
await MenuItemInvokedOrSelectedAsync(SettingsItem, WinoPage.AppPreferencesPage);
|
||||
NavigationService.Navigate(WinoPage.SettingsPage, WinoPage.AppPreferencesPage);
|
||||
}
|
||||
|
||||
protected override void RegisterRecipients()
|
||||
{
|
||||
base.RegisterRecipients();
|
||||
|
||||
Messenger.Register<AccountRemovedMessage>(this);
|
||||
Messenger.Register<AccountUpdatedMessage>(this);
|
||||
Messenger.Register<MailtoProtocolMessageRequested>(this);
|
||||
Messenger.Register<RefreshUnreadCountsMessage>(this);
|
||||
Messenger.Register<AccountsMenuRefreshRequested>(this);
|
||||
Messenger.Register<MergedInboxRenamed>(this);
|
||||
Messenger.Register<LanguageChanged>(this);
|
||||
Messenger.Register<AccountMenuItemsReordered>(this);
|
||||
Messenger.Register<AccountSynchronizerStateChanged>(this);
|
||||
Messenger.Register<NavigateAppPreferencesRequested>(this);
|
||||
Messenger.Register<AccountFolderConfigurationUpdated>(this);
|
||||
}
|
||||
|
||||
protected override void UnregisterRecipients()
|
||||
{
|
||||
base.UnregisterRecipients();
|
||||
|
||||
Messenger.Unregister<AccountRemovedMessage>(this);
|
||||
Messenger.Unregister<AccountUpdatedMessage>(this);
|
||||
Messenger.Unregister<MailtoProtocolMessageRequested>(this);
|
||||
Messenger.Unregister<RefreshUnreadCountsMessage>(this);
|
||||
Messenger.Unregister<AccountsMenuRefreshRequested>(this);
|
||||
Messenger.Unregister<MergedInboxRenamed>(this);
|
||||
Messenger.Unregister<LanguageChanged>(this);
|
||||
Messenger.Unregister<AccountMenuItemsReordered>(this);
|
||||
Messenger.Unregister<AccountSynchronizerStateChanged>(this);
|
||||
Messenger.Unregister<NavigateAppPreferencesRequested>(this);
|
||||
Messenger.Unregister<AccountFolderConfigurationUpdated>(this);
|
||||
}
|
||||
|
||||
public async void Receive(AccountRemovedMessage message)
|
||||
{
|
||||
var remainingAccounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
|
||||
if (!remainingAccounts.Any())
|
||||
{
|
||||
latestSelectedAccountMenuItem = null;
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
SelectedMenuItem = null;
|
||||
MenuItems?.Clear();
|
||||
MenuItems?.Add(CreateMailMenuItem);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (latestSelectedAccountMenuItem?.HoldingAccounts?.Any(a => a.Id == message.Account.Id) == true)
|
||||
{
|
||||
latestSelectedAccountMenuItem = null;
|
||||
await ExecuteUIThread(() => SelectedMenuItem = null);
|
||||
}
|
||||
|
||||
await RecreateMenuItemsAsync();
|
||||
await RestoreSelectedAccountAfterMenuRefreshAsync(false);
|
||||
}
|
||||
|
||||
public async Task HandleAccountCreatedAsync(MailAccount createdAccount)
|
||||
{
|
||||
latestSelectedAccountMenuItem = null;
|
||||
|
||||
await RecreateMenuItemsAsync();
|
||||
|
||||
if (!MenuItems.TryGetAccountMenuItem(createdAccount.Id, out IAccountMenuItem createdMenuItem))
|
||||
{
|
||||
Log.Warning("Created account {AccountId} could not be found in menu items after refresh.", createdAccount.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
await ChangeLoadedAccountAsync(createdMenuItem);
|
||||
|
||||
// Each created account should start a new synchronization automatically.
|
||||
var options = new MailSynchronizationOptions()
|
||||
{
|
||||
AccountId = createdAccount.Id,
|
||||
Type = MailSynchronizationType.FullFolders,
|
||||
};
|
||||
|
||||
Messenger.Send(new NewMailSynchronizationRequested(options));
|
||||
|
||||
if (createdAccount.IsCalendarAccessGranted)
|
||||
{
|
||||
var calendarOptions = new CalendarSynchronizationOptions()
|
||||
{
|
||||
AccountId = createdAccount.Id,
|
||||
Type = CalendarSynchronizationType.CalendarEvents
|
||||
};
|
||||
|
||||
Messenger.Send(new NewCalendarSynchronizationRequested(calendarOptions));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _nativeAppService.PinAppToTaskbarAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to pin Wino to taskbar.");
|
||||
}
|
||||
}
|
||||
|
||||
public async void Receive(AccountUpdatedMessage message)
|
||||
{
|
||||
var updatedAccount = message.Account;
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
if (MenuItems.TryGetAccountMenuItem(updatedAccount.Id, out IAccountMenuItem foundAccountMenuItem))
|
||||
{
|
||||
foundAccountMenuItem.UpdateAccount(updatedAccount);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void IShellClient.Activate(ShellModeActivationContext activationContext)
|
||||
=> OnNavigatedTo(NavigationMode.New, activationContext);
|
||||
|
||||
void IShellClient.Deactivate()
|
||||
=> OnNavigatedFrom(NavigationMode.New, null!);
|
||||
|
||||
Task IShellClient.HandleNavigationItemInvokedAsync(IMenuItem menuItem)
|
||||
=> MenuItemInvokedOrSelectedAsync(menuItem);
|
||||
|
||||
Task IShellClient.HandleNavigationSelectionChangedAsync(IMenuItem menuItem)
|
||||
=> menuItem == null ? Task.CompletedTask : MenuItemInvokedOrSelectedAsync(menuItem);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.ViewModels;
|
||||
using Wino.Messaging.UI;
|
||||
@@ -12,39 +12,28 @@ public class MailBaseViewModel : CoreBaseViewModel,
|
||||
IRecipient<MailAddedMessage>,
|
||||
IRecipient<MailRemovedMessage>,
|
||||
IRecipient<MailUpdatedMessage>,
|
||||
IRecipient<BulkMailUpdatedMessage>,
|
||||
IRecipient<MailDownloadedMessage>,
|
||||
IRecipient<DraftCreated>,
|
||||
IRecipient<DraftFailed>,
|
||||
IRecipient<DraftMapped>,
|
||||
IRecipient<FolderRenamed>,
|
||||
IRecipient<FolderDeleted>,
|
||||
IRecipient<FolderSynchronizationEnabled>
|
||||
{
|
||||
protected virtual void OnMailAdded(MailCopy addedMail) { }
|
||||
protected virtual void OnMailRemoved(MailCopy removedMail) { }
|
||||
protected virtual void OnMailUpdated(MailCopy updatedMail) { }
|
||||
|
||||
protected virtual void OnMailUpdated(IReadOnlyList<MailCopy> updatedMails)
|
||||
{
|
||||
if (updatedMails == null) return;
|
||||
|
||||
foreach (var mail in updatedMails)
|
||||
{
|
||||
OnMailUpdated(mail);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void OnMailUpdated(MailCopy updatedMail, MailUpdateSource source, MailCopyChangeFlags changedProperties) { }
|
||||
protected virtual void OnMailDownloaded(MailCopy downloadedMail) { }
|
||||
protected virtual void OnDraftCreated(MailCopy draftMail, MailAccount account) { }
|
||||
protected virtual void OnDraftFailed(MailCopy draftMail, MailAccount account) { }
|
||||
protected virtual void OnDraftMapped(string localDraftCopyId, string remoteDraftCopyId) { }
|
||||
protected virtual void OnFolderRenamed(IMailItemFolder mailItemFolder) { }
|
||||
protected virtual void OnFolderDeleted(MailItemFolder folder) { }
|
||||
protected virtual void OnFolderSynchronizationEnabled(IMailItemFolder mailItemFolder) { }
|
||||
|
||||
void IRecipient<MailAddedMessage>.Receive(MailAddedMessage message) => OnMailAdded(message.AddedMail);
|
||||
void IRecipient<MailRemovedMessage>.Receive(MailRemovedMessage message) => OnMailRemoved(message.RemovedMail);
|
||||
void IRecipient<MailUpdatedMessage>.Receive(MailUpdatedMessage message) => OnMailUpdated(message.UpdatedMail);
|
||||
void IRecipient<BulkMailUpdatedMessage>.Receive(BulkMailUpdatedMessage message) => OnMailUpdated(message.UpdatedMails);
|
||||
void IRecipient<MailUpdatedMessage>.Receive(MailUpdatedMessage message) => OnMailUpdated(message.UpdatedMail, message.Source, message.ChangedProperties);
|
||||
void IRecipient<MailDownloadedMessage>.Receive(MailDownloadedMessage message) => OnMailDownloaded(message.DownloadedMail);
|
||||
|
||||
void IRecipient<DraftMapped>.Receive(DraftMapped message) => OnDraftMapped(message.LocalDraftCopyId, message.RemoteDraftCopyId);
|
||||
@@ -52,5 +41,40 @@ public class MailBaseViewModel : CoreBaseViewModel,
|
||||
void IRecipient<DraftCreated>.Receive(DraftCreated message) => OnDraftCreated(message.DraftMail, message.Account);
|
||||
|
||||
void IRecipient<FolderRenamed>.Receive(FolderRenamed message) => OnFolderRenamed(message.MailItemFolder);
|
||||
void IRecipient<FolderDeleted>.Receive(FolderDeleted message) => OnFolderDeleted(message.MailItemFolder);
|
||||
void IRecipient<FolderSynchronizationEnabled>.Receive(FolderSynchronizationEnabled message) => OnFolderSynchronizationEnabled(message.MailItemFolder);
|
||||
|
||||
protected override void RegisterRecipients()
|
||||
{
|
||||
base.RegisterRecipients();
|
||||
|
||||
UnregisterRecipients();
|
||||
|
||||
Messenger.Register<MailAddedMessage>(this);
|
||||
Messenger.Register<MailRemovedMessage>(this);
|
||||
Messenger.Register<MailUpdatedMessage>(this);
|
||||
Messenger.Register<MailDownloadedMessage>(this);
|
||||
Messenger.Register<DraftCreated>(this);
|
||||
Messenger.Register<DraftFailed>(this);
|
||||
Messenger.Register<DraftMapped>(this);
|
||||
Messenger.Register<FolderRenamed>(this);
|
||||
Messenger.Register<FolderDeleted>(this);
|
||||
Messenger.Register<FolderSynchronizationEnabled>(this);
|
||||
}
|
||||
|
||||
protected override void UnregisterRecipients()
|
||||
{
|
||||
base.UnregisterRecipients();
|
||||
|
||||
Messenger.Unregister<MailAddedMessage>(this);
|
||||
Messenger.Unregister<MailRemovedMessage>(this);
|
||||
Messenger.Unregister<MailUpdatedMessage>(this);
|
||||
Messenger.Unregister<MailDownloadedMessage>(this);
|
||||
Messenger.Unregister<DraftCreated>(this);
|
||||
Messenger.Unregister<DraftFailed>(this);
|
||||
Messenger.Unregister<DraftMapped>(this);
|
||||
Messenger.Unregister<FolderRenamed>(this);
|
||||
Messenger.Unregister<FolderDeleted>(this);
|
||||
Messenger.Unregister<FolderSynchronizationEnabled>(this);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,27 +11,30 @@ using CommunityToolkit.Mvvm.Messaging;
|
||||
using MailKit;
|
||||
|
||||
using MimeKit;
|
||||
using MimeKit.Cryptography;
|
||||
using Serilog;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Exceptions;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models.Menus;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.Domain.Models.Printing;
|
||||
using Wino.Core.Domain.Models.Reader;
|
||||
using Wino.Core.Services;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
using Wino.Mail.ViewModels.Messages;
|
||||
using Wino.Messaging.Client.Mails;
|
||||
using Wino.Messaging.Server;
|
||||
using Wino.Messaging.UI;
|
||||
using IMailService = Wino.Core.Domain.Interfaces.IMailService;
|
||||
|
||||
namespace Wino.Mail.ViewModels;
|
||||
|
||||
public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
||||
IRecipient<NewMailItemRenderingRequestedEvent>,
|
||||
IRecipient<ReaderItemRefreshRequestedEvent>,
|
||||
IRecipient<ThumbnailAdded>,
|
||||
ITransferProgress // For listening IMAP message download progress.
|
||||
{
|
||||
@@ -40,13 +43,13 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
||||
|
||||
private readonly IMimeFileService _mimeFileService;
|
||||
private readonly Core.Domain.Interfaces.IMailService _mailService;
|
||||
private readonly IFolderService _folderService;
|
||||
private readonly IFileService _fileService;
|
||||
private readonly IWinoRequestDelegator _requestDelegator;
|
||||
private readonly IContactService _contactService;
|
||||
private readonly IClipboardService _clipboardService;
|
||||
private readonly IUnsubscriptionService _unsubscriptionService;
|
||||
private readonly IApplicationConfiguration _applicationConfiguration;
|
||||
private readonly IWinoServerConnectionManager _winoServerConnectionManager;
|
||||
private bool forceImageLoading = false;
|
||||
|
||||
private MailItemViewModel initializedMailItemViewModel = null;
|
||||
@@ -56,11 +59,17 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
||||
// Used in 'Save as' and 'Print' functionality.
|
||||
public Func<string, Task<bool>> SaveHTMLasPDFFunc { get; set; }
|
||||
|
||||
public Func<WebView2PrintSettingsModel, Task<PrintingResult>> DirectPrintFuncAsync { get; set; }
|
||||
|
||||
#region Properties
|
||||
|
||||
public bool ShouldDisplayDownloadProgress => IsIndetermineProgress || (CurrentDownloadPercentage > 0 && CurrentDownloadPercentage <= 100);
|
||||
public bool CanUnsubscribe => CurrentRenderModel?.UnsubscribeInfo?.CanUnsubscribe ?? false;
|
||||
public bool IsJunkMail => initializedMailItemViewModel?.AssignedFolder != null && initializedMailItemViewModel.AssignedFolder.SpecialFolderType == SpecialFolderType.Junk;
|
||||
public bool IsSmimeSigned => (CurrentRenderModel?.Signatures?.Count ?? 0) > 0;
|
||||
public bool IsSmimeEncrypted => CurrentRenderModel?.IsSmimeEncrypted ?? false;
|
||||
public bool IsJunkMail => initializedMailItemViewModel?.MailCopy.AssignedFolder != null && initializedMailItemViewModel.MailCopy.AssignedFolder.SpecialFolderType == SpecialFolderType.Junk;
|
||||
public bool SmimeSignaturesValid => CurrentRenderModel?.Signatures?.Any(x => x.Value) ?? false;
|
||||
public bool SmimeSignaturesInvalid => !SmimeSignaturesValid;
|
||||
|
||||
public bool IsImageRenderingDisabled
|
||||
{
|
||||
@@ -100,6 +109,10 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(CanUnsubscribe))]
|
||||
[NotifyPropertyChangedFor(nameof(IsSmimeSigned))]
|
||||
[NotifyPropertyChangedFor(nameof(IsSmimeEncrypted))]
|
||||
[NotifyPropertyChangedFor(nameof(SmimeSignaturesValid))]
|
||||
[NotifyPropertyChangedFor(nameof(SmimeSignaturesInvalid))]
|
||||
public partial MailRenderModel CurrentRenderModel { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
@@ -112,7 +125,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
||||
public partial string FromName { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string ContactPicture { get; set; }
|
||||
public partial IMailItemDisplayInformation CurrentMailItemDisplayInformation { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial DateTime CreationDate { get; set; }
|
||||
@@ -120,7 +133,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
||||
public ObservableCollection<AccountContactViewModel> CcItems { get; set; } = [];
|
||||
public ObservableCollection<AccountContactViewModel> BccItems { get; set; } = [];
|
||||
public ObservableCollection<MailAttachmentViewModel> Attachments { get; set; } = [];
|
||||
public ObservableCollection<MailOperationMenuItem> MenuItems { get; set; } = [];
|
||||
public ObservableCollection<IMenuOperation> MenuItems { get; set; } = [];
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -128,22 +141,24 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
||||
public IStatePersistanceService StatePersistenceService { get; }
|
||||
public IPreferencesService PreferencesService { get; }
|
||||
public IPrintService PrintService { get; }
|
||||
public Guid? CurrentMailAccountId => initializedMailItemViewModel?.MailCopy.AssignedAccount?.Id;
|
||||
public Guid? CurrentMailFileId => initializedMailItemViewModel?.MailCopy.FileId;
|
||||
|
||||
public MailRenderingPageViewModel(IMailDialogService dialogService,
|
||||
INativeAppService nativeAppService,
|
||||
IUnderlyingThemeService underlyingThemeService,
|
||||
IMimeFileService mimeFileService,
|
||||
IMailService mailService,
|
||||
IFileService fileService,
|
||||
IWinoRequestDelegator requestDelegator,
|
||||
IStatePersistanceService statePersistenceService,
|
||||
IContactService contactService,
|
||||
IClipboardService clipboardService,
|
||||
IUnsubscriptionService unsubscriptionService,
|
||||
IPreferencesService preferencesService,
|
||||
IPrintService printService,
|
||||
IApplicationConfiguration applicationConfiguration,
|
||||
IWinoServerConnectionManager winoServerConnectionManager)
|
||||
INativeAppService nativeAppService,
|
||||
IUnderlyingThemeService underlyingThemeService,
|
||||
IMimeFileService mimeFileService,
|
||||
IMailService mailService,
|
||||
IFolderService folderService,
|
||||
IFileService fileService,
|
||||
IWinoRequestDelegator requestDelegator,
|
||||
IStatePersistanceService statePersistenceService,
|
||||
IContactService contactService,
|
||||
IClipboardService clipboardService,
|
||||
IUnsubscriptionService unsubscriptionService,
|
||||
IPreferencesService preferencesService,
|
||||
IPrintService printService,
|
||||
IApplicationConfiguration applicationConfiguration)
|
||||
{
|
||||
_dialogService = dialogService;
|
||||
NativeAppService = nativeAppService;
|
||||
@@ -152,12 +167,12 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
||||
PreferencesService = preferencesService;
|
||||
PrintService = printService;
|
||||
_applicationConfiguration = applicationConfiguration;
|
||||
_winoServerConnectionManager = winoServerConnectionManager;
|
||||
_clipboardService = clipboardService;
|
||||
_unsubscriptionService = unsubscriptionService;
|
||||
_underlyingThemeService = underlyingThemeService;
|
||||
_mimeFileService = mimeFileService;
|
||||
_mailService = mailService;
|
||||
_folderService = folderService;
|
||||
_fileService = fileService;
|
||||
_requestDelegator = requestDelegator;
|
||||
}
|
||||
@@ -240,63 +255,98 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task OperationClicked(MailOperationMenuItem menuItem)
|
||||
private async Task OperationClicked(IMenuOperation menuItem)
|
||||
{
|
||||
if (menuItem == null) return;
|
||||
if (menuItem is not MailOperationMenuItem mailOperationMenuItem) return;
|
||||
|
||||
await HandleMailOperationAsync(menuItem.Operation);
|
||||
await HandleMailOperationAsync(mailOperationMenuItem.Operation);
|
||||
}
|
||||
|
||||
private async Task HandleMailOperationAsync(MailOperation operation)
|
||||
{
|
||||
// Toggle theme
|
||||
if (operation == MailOperation.DarkEditor || operation == MailOperation.LightEditor)
|
||||
IsDarkWebviewRenderer = !IsDarkWebviewRenderer;
|
||||
else if (operation == MailOperation.SaveAs)
|
||||
try
|
||||
{
|
||||
await SaveAsAsync();
|
||||
}
|
||||
else if (operation == MailOperation.Print)
|
||||
{
|
||||
await PrintAsync();
|
||||
}
|
||||
else if (operation == MailOperation.ViewMessageSource)
|
||||
{
|
||||
await _dialogService.ShowMessageSourceDialogAsync(initializedMimeMessageInformation.MimeMessage.ToString());
|
||||
}
|
||||
else if (operation == MailOperation.Reply || operation == MailOperation.ReplyAll || operation == MailOperation.Forward)
|
||||
{
|
||||
if (initializedMailItemViewModel == null) return;
|
||||
|
||||
// Create new draft.
|
||||
var draftOptions = new DraftCreationOptions()
|
||||
if (operation == MailOperation.SaveAs)
|
||||
{
|
||||
Reason = operation switch
|
||||
await SaveAsAsync();
|
||||
}
|
||||
else if (operation == MailOperation.Print)
|
||||
{
|
||||
var settings = await _dialogService.ShowPrintDialogAsync();
|
||||
|
||||
if (settings == null) return;
|
||||
|
||||
var printingResult = await DirectPrintFuncAsync.Invoke(settings);
|
||||
|
||||
// TODO: More detailed printing result handling.
|
||||
if (printingResult == PrintingResult.Submitted)
|
||||
{
|
||||
MailOperation.Reply => DraftCreationReason.Reply,
|
||||
MailOperation.ReplyAll => DraftCreationReason.ReplyAll,
|
||||
MailOperation.Forward => DraftCreationReason.Forward,
|
||||
_ => DraftCreationReason.Empty
|
||||
},
|
||||
ReferencedMessage = new ReferencedMessage()
|
||||
{
|
||||
MimeMessage = initializedMimeMessageInformation.MimeMessage,
|
||||
MailCopy = initializedMailItemViewModel.MailCopy
|
||||
_dialogService.InfoBarMessage(Translator.DialogMessage_PrintingSuccessTitle, Translator.DialogMessage_PrintingSuccessMessage, InfoBarMessageType.Success);
|
||||
}
|
||||
else if (printingResult == PrintingResult.Failed)
|
||||
{
|
||||
_dialogService.InfoBarMessage(Translator.DialogMessage_PrintingFailedTitle, Translator.DialogMessage_PrintingFailedMessage, InfoBarMessageType.Error);
|
||||
}
|
||||
};
|
||||
|
||||
var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(initializedMailItemViewModel.AssignedAccount.Id, draftOptions).ConfigureAwait(false);
|
||||
}
|
||||
else if (operation == MailOperation.ViewMessageSource)
|
||||
{
|
||||
await _dialogService.ShowMessageSourceDialogAsync(initializedMimeMessageInformation.MimeMessage.ToString());
|
||||
}
|
||||
else if (operation == MailOperation.Reply || operation == MailOperation.ReplyAll || operation == MailOperation.Forward)
|
||||
{
|
||||
if (initializedMailItemViewModel == null) return;
|
||||
|
||||
var draftPreparationRequest = new DraftPreparationRequest(initializedMailItemViewModel.AssignedAccount, draftMailCopy, draftBase64MimeMessage, draftOptions.Reason, initializedMailItemViewModel.MailCopy);
|
||||
// Create new draft.
|
||||
var draftOptions = new DraftCreationOptions()
|
||||
{
|
||||
Reason = operation switch
|
||||
{
|
||||
MailOperation.Reply => DraftCreationReason.Reply,
|
||||
MailOperation.ReplyAll => DraftCreationReason.ReplyAll,
|
||||
MailOperation.Forward => DraftCreationReason.Forward,
|
||||
_ => DraftCreationReason.Empty
|
||||
},
|
||||
ReferencedMessage = new ReferencedMessage()
|
||||
{
|
||||
MimeMessage = initializedMimeMessageInformation.MimeMessage,
|
||||
MailCopy = initializedMailItemViewModel.MailCopy
|
||||
}
|
||||
};
|
||||
|
||||
await _requestDelegator.ExecuteAsync(draftPreparationRequest);
|
||||
var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(initializedMailItemViewModel.MailCopy.AssignedAccount.Id, draftOptions).ConfigureAwait(false);
|
||||
|
||||
var draftPreparationRequest = new DraftPreparationRequest(initializedMailItemViewModel.MailCopy.AssignedAccount, draftMailCopy, draftBase64MimeMessage, draftOptions.Reason, initializedMailItemViewModel.MailCopy);
|
||||
|
||||
await _requestDelegator.ExecuteAsync(draftPreparationRequest);
|
||||
|
||||
}
|
||||
else if (initializedMailItemViewModel != null)
|
||||
{
|
||||
// All other operations require a mail item.
|
||||
var prepRequest = new MailOperationPreperationRequest(operation, initializedMailItemViewModel.MailCopy);
|
||||
await _requestDelegator.ExecuteAsync(prepRequest);
|
||||
}
|
||||
}
|
||||
else if (initializedMailItemViewModel != null)
|
||||
catch (UnavailableSpecialFolderException unavailableSpecialFolderException)
|
||||
{
|
||||
// All other operations require a mail item.
|
||||
var prepRequest = new MailOperationPreperationRequest(operation, initializedMailItemViewModel.MailCopy);
|
||||
await _requestDelegator.ExecuteAsync(prepRequest);
|
||||
_dialogService.InfoBarMessage(Translator.Info_MissingFolderTitle,
|
||||
string.Format(Translator.Info_MissingFolderMessage, unavailableSpecialFolderException.SpecialFolderType),
|
||||
InfoBarMessageType.Warning,
|
||||
Translator.SettingConfigureSpecialFolders_Button,
|
||||
() =>
|
||||
{
|
||||
_dialogService.HandleSystemFolderConfigurationDialogAsync(unavailableSpecialFolderException.AccountId, _folderService);
|
||||
});
|
||||
}
|
||||
catch (NotImplementedException)
|
||||
{
|
||||
_dialogService.ShowNotSupportedMessage();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Mail operation execution failed. Operation: {Operation}", operation);
|
||||
_dialogService.InfoBarMessage(Translator.Info_RequestCreationFailedTitle, ex.Message, InfoBarMessageType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,6 +360,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
||||
|
||||
initializedMailItemViewModel = null;
|
||||
initializedMimeMessageInformation = null;
|
||||
CurrentMailItemDisplayInformation = null;
|
||||
|
||||
// Dispose existing content first.
|
||||
Messenger.Send(new CancelRenderingContentRequested());
|
||||
@@ -355,8 +406,10 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
||||
// To show the progress on the UI.
|
||||
CurrentDownloadPercentage = 1;
|
||||
|
||||
var package = new DownloadMissingMessageRequested(mailItemViewModel.AssignedAccount.Id, mailItemViewModel.MailCopy);
|
||||
await _winoServerConnectionManager.GetResponseAsync<bool, DownloadMissingMessageRequested>(package);
|
||||
// Download missing MIME message using SynchronizationManager
|
||||
await SynchronizationManager.Instance.DownloadMimeMessageAsync(
|
||||
mailItemViewModel.MailCopy,
|
||||
mailItemViewModel.MailCopy.AssignedAccount.Id);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -375,7 +428,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
||||
private async Task RenderAsync(MailItemViewModel mailItemViewModel, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ResetProgress();
|
||||
var isMimeExists = await _mimeFileService.IsMimeExistAsync(mailItemViewModel.AssignedAccount.Id, mailItemViewModel.MailCopy.FileId);
|
||||
var isMimeExists = await _mimeFileService.IsMimeExistAsync(mailItemViewModel.MailCopy.AssignedAccount.Id, mailItemViewModel.MailCopy.FileId);
|
||||
|
||||
if (!isMimeExists)
|
||||
{
|
||||
@@ -384,7 +437,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
||||
|
||||
// Find the MIME for this item and render it.
|
||||
var mimeMessageInformation = await _mimeFileService.GetMimeMessageInformationAsync(mailItemViewModel.MailCopy.FileId,
|
||||
mailItemViewModel.AssignedAccount.Id,
|
||||
mailItemViewModel.MailCopy.AssignedAccount.Id,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (mimeMessageInformation == null)
|
||||
@@ -394,6 +447,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
||||
}
|
||||
|
||||
initializedMailItemViewModel = mailItemViewModel;
|
||||
await ExecuteUIThread(() => { CurrentMailItemDisplayInformation = mailItemViewModel; });
|
||||
await RenderAsync(mimeMessageInformation);
|
||||
}
|
||||
|
||||
@@ -435,13 +489,14 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
||||
// TODO: FromName and FromAddress is probably not correct here for mail lists.
|
||||
FromAddress = message.From.Mailboxes.FirstOrDefault()?.Address ?? Translator.UnknownAddress;
|
||||
FromName = message.From.Mailboxes.FirstOrDefault()?.Name ?? Translator.UnknownSender;
|
||||
CreationDate = message.Date.DateTime;
|
||||
ContactPicture = initializedMailItemViewModel?.SenderContact?.Base64ContactPicture;
|
||||
|
||||
// Use the received date from MailCopy if available, otherwise fall back to the sent date from MIME message
|
||||
CreationDate = initializedMailItemViewModel?.MailCopy.CreationDate ?? message.Date.DateTime;
|
||||
|
||||
// Automatically disable images for Junk folder to prevent pixel tracking.
|
||||
// This can only work for selected mail item rendering, not for EML file rendering.
|
||||
if (initializedMailItemViewModel != null &&
|
||||
initializedMailItemViewModel.AssignedFolder.SpecialFolderType == SpecialFolderType.Junk)
|
||||
initializedMailItemViewModel.MailCopy.AssignedFolder.SpecialFolderType == SpecialFolderType.Junk)
|
||||
{
|
||||
renderingOptions.LoadImages = false;
|
||||
}
|
||||
@@ -480,7 +535,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
||||
var contactViewModel = new AccountContactViewModel(foundContact);
|
||||
|
||||
// Make sure that user account first in the list.
|
||||
if (string.Equals(contactViewModel.Address, initializedMailItemViewModel?.AssignedAccount?.Address, StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(contactViewModel.Address, initializedMailItemViewModel?.MailCopy.AssignedAccount?.Address, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
contactViewModel.IsMe = true;
|
||||
accounts.Insert(0, contactViewModel);
|
||||
@@ -506,11 +561,15 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
||||
{
|
||||
base.OnNavigatedFrom(mode, parameters);
|
||||
|
||||
renderCancellationTokenSource.Cancel();
|
||||
renderCancellationTokenSource?.Cancel();
|
||||
renderCancellationTokenSource?.Dispose();
|
||||
renderCancellationTokenSource = null;
|
||||
|
||||
CurrentDownloadPercentage = 0d;
|
||||
|
||||
initializedMailItemViewModel = null;
|
||||
initializedMimeMessageInformation = null;
|
||||
CurrentMailItemDisplayInformation = null;
|
||||
|
||||
forceImageLoading = false;
|
||||
|
||||
@@ -527,12 +586,6 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
||||
{
|
||||
MenuItems.Clear();
|
||||
|
||||
// Add light/dark editor theme switch.
|
||||
if (IsDarkWebviewRenderer)
|
||||
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.LightEditor));
|
||||
else
|
||||
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.DarkEditor));
|
||||
|
||||
// Save As PDF
|
||||
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.SaveAs, true, true));
|
||||
|
||||
@@ -563,7 +616,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
||||
}
|
||||
|
||||
// Archive - Unarchive
|
||||
if (initializedMailItemViewModel.AssignedFolder.SpecialFolderType == SpecialFolderType.Archive)
|
||||
if (initializedMailItemViewModel.MailCopy.AssignedFolder.SpecialFolderType == SpecialFolderType.Archive)
|
||||
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.UnArchive));
|
||||
else
|
||||
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.Archive));
|
||||
@@ -586,15 +639,15 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
||||
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.MarkAsRead, true, false));
|
||||
}
|
||||
|
||||
protected override async void OnMailUpdated(MailCopy updatedMail)
|
||||
protected override async void OnMailUpdated(MailCopy updatedMail, MailUpdateSource source, MailCopyChangeFlags changedProperties)
|
||||
{
|
||||
base.OnMailUpdated(updatedMail);
|
||||
base.OnMailUpdated(updatedMail, source, changedProperties);
|
||||
|
||||
if (initializedMailItemViewModel == null) return;
|
||||
|
||||
// Check if the updated mail is the same mail item we are rendering.
|
||||
// This is done with UniqueId to include FolderId into calculations.
|
||||
if (initializedMailItemViewModel.UniqueId != updatedMail.UniqueId) return;
|
||||
if (initializedMailItemViewModel.MailCopy.UniqueId != updatedMail.UniqueId) return;
|
||||
|
||||
await ExecuteUIThread(() => { InitializeCommandBarItems(); });
|
||||
}
|
||||
@@ -690,40 +743,6 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PrintAsync()
|
||||
{
|
||||
// Printing:
|
||||
// 1. Let WebView2 save the current HTML as PDF to temporary location.
|
||||
// 2. Saving as PDF will divide pages correctly for Win2D CanvasBitmap.
|
||||
// 3. Use Win2D CanvasBitmap as IPrintDocumentSource and WinRT APIs to print the PDF.
|
||||
|
||||
try
|
||||
{
|
||||
var printFilePath = Path.Combine(_applicationConfiguration.ApplicationTempFolderPath, "print.pdf");
|
||||
|
||||
if (File.Exists(printFilePath)) File.Delete(printFilePath);
|
||||
|
||||
await SaveHTMLasPDFFunc(printFilePath);
|
||||
|
||||
var result = await PrintService.PrintPdfFileAsync(printFilePath, Subject);
|
||||
|
||||
if (result == PrintingResult.Submitted)
|
||||
{
|
||||
_dialogService.InfoBarMessage(Translator.DialogMessage_PrintingSuccessTitle, Translator.DialogMessage_PrintingSuccessMessage, InfoBarMessageType.Success);
|
||||
}
|
||||
else if (result != PrintingResult.Canceled)
|
||||
{
|
||||
var message = string.Format(Translator.DialogMessage_PrintingFailedMessage, result);
|
||||
_dialogService.InfoBarMessage(Translator.DialogMessage_PrintingFailedTitle, message, InfoBarMessageType.Warning);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to print mail.");
|
||||
_dialogService.InfoBarMessage(string.Empty, ex.Message, InfoBarMessageType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveAsAsync()
|
||||
{
|
||||
try
|
||||
@@ -739,8 +758,8 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
||||
if (isSaved)
|
||||
{
|
||||
_dialogService.InfoBarMessage(Translator.Info_PDFSaveSuccessTitle,
|
||||
string.Format(Translator.Info_PDFSaveSuccessMessage, pdfFilePath),
|
||||
InfoBarMessageType.Success);
|
||||
string.Format(Translator.Info_PDFSaveSuccessMessage, pdfFilePath),
|
||||
InfoBarMessageType.Success);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -782,8 +801,10 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
||||
// For upload.
|
||||
void ITransferProgress.Report(long bytesTransferred) { }
|
||||
|
||||
public async void Receive(NewMailItemRenderingRequestedEvent message)
|
||||
public async void Receive(ReaderItemRefreshRequestedEvent message)
|
||||
{
|
||||
if (message.MailItemViewModel == null || message.MailItemViewModel.IsDraft) return;
|
||||
|
||||
try
|
||||
{
|
||||
await RenderAsync(message.MailItemViewModel, renderCancellationTokenSource.Token);
|
||||
@@ -822,4 +843,86 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ShowSmimeSigningCertificateInfoAsync()
|
||||
{
|
||||
if (IsSmimeSigned)
|
||||
{
|
||||
MimePart signaturePart;
|
||||
if (initializedMimeMessageInformation?.MimeMessage?.Body is MultipartSigned signed && signed[1] is MimePart signaturePart1)
|
||||
{
|
||||
signaturePart = signaturePart1;
|
||||
}
|
||||
else if (initializedMimeMessageInformation?.MimeMessage?.Body is ApplicationPkcs7Mime pkcs7)
|
||||
{
|
||||
signaturePart = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
//_dialogService.InfoBarMessage(Translator.Info_SmimeSignatureNotFoundTitle, Translator.Info_SmimeSignatureNotFoundMessage, InfoBarMessageType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
string info = $"{Translator.SmimeSignaturesInMessage}:\n";
|
||||
foreach (var (signature, valid) in CurrentRenderModel.Signatures)
|
||||
{
|
||||
info += string.Format(Translator.SmimeSignatureEntry, valid ? "✅" : "❌", signature.SignerCertificate.Name, signature.SignerCertificate.Fingerprint, signature.SignerCertificate.CreationDate, signature.SignerCertificate.ExpirationDate);
|
||||
}
|
||||
await ShowSmimeCertificateInfoAsync(signaturePart, info, Translator.SmimeSigningCertificateInfoTitle);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private async Task ShowSmimeCertificateInfoAsync(MimePart certificateAttachment, string additionalInfo = "", string title = null)
|
||||
{
|
||||
{
|
||||
if (certificateAttachment == null)
|
||||
{
|
||||
await _dialogService.ShowConfirmationDialogAsync(
|
||||
$"{additionalInfo}\n{Translator.SmimeNoCertificateFileFound}", title ?? Translator.SmimeCertificateInfoTitle, Translator.Buttons_OK);
|
||||
return;
|
||||
}
|
||||
var fileName = certificateAttachment.FileName ?? "smime.p7s";
|
||||
var contentType = certificateAttachment.ContentType?.MimeType ?? "application/pkcs7-signature";
|
||||
var size = certificateAttachment.Content?.Stream?.Length ?? 0;
|
||||
var info = string.Format(Translator.SmimeCertificateFileInfo, fileName, contentType, size);
|
||||
|
||||
var result = await _dialogService.ShowConfirmationDialogAsync(
|
||||
$"{additionalInfo}\n{info}", title ?? Translator.SmimeCertificateInfoTitle,
|
||||
Translator.SmimeSaveCertificate);
|
||||
if (result)
|
||||
{
|
||||
var pickedPath = await _dialogService.PickFilePathAsync(fileName);
|
||||
if (!string.IsNullOrEmpty(pickedPath))
|
||||
{
|
||||
var pickedDirectory = Path.GetDirectoryName(pickedPath);
|
||||
var pickedFileName = Path.GetFileName(pickedPath);
|
||||
await using (var stream = await _fileService.GetFileStreamAsync(pickedDirectory, pickedFileName))
|
||||
{
|
||||
await certificateAttachment.Content!.DecodeToAsync(stream);
|
||||
}
|
||||
|
||||
_dialogService.InfoBarMessage(Translator.SmimeCertificate, string.Format(Translator.SmimeCertificateSavedTo, pickedPath),
|
||||
InfoBarMessageType.Success);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void RegisterRecipients()
|
||||
{
|
||||
base.RegisterRecipients();
|
||||
|
||||
Messenger.Register<ReaderItemRefreshRequestedEvent>(this);
|
||||
Messenger.Register<ThumbnailAdded>(this);
|
||||
}
|
||||
|
||||
protected override void UnregisterRecipients()
|
||||
{
|
||||
base.UnregisterRecipients();
|
||||
|
||||
Messenger.Unregister<ReaderItemRefreshRequestedEvent>(this);
|
||||
Messenger.Unregister<ThumbnailAdded>(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Wino.Core;
|
||||
|
||||
namespace Wino.Mail.ViewModels;
|
||||
|
||||
public static class MailViewModelsContainerSetup
|
||||
{
|
||||
public static void RegisterViewModelService(this IServiceCollection services)
|
||||
{
|
||||
// View models use core services.
|
||||
services.RegisterCoreServices();
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
@@ -12,22 +14,8 @@ public partial class MessageListPageViewModel : MailBaseViewModel
|
||||
{
|
||||
public IPreferencesService PreferencesService { get; }
|
||||
private readonly IThumbnailService _thumbnailService;
|
||||
|
||||
private int selectedMarkAsOptionIndex;
|
||||
public int SelectedMarkAsOptionIndex
|
||||
{
|
||||
get => selectedMarkAsOptionIndex;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref selectedMarkAsOptionIndex, value))
|
||||
{
|
||||
if (value >= 0)
|
||||
{
|
||||
PreferencesService.MarkAsPreference = (MailMarkAsOption)Enum.GetValues<MailMarkAsOption>().GetValue(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private readonly IStatePersistanceService _statePersistenceService;
|
||||
private readonly IDialogServiceBase _dialogService;
|
||||
|
||||
private readonly List<MailOperation> availableHoverActions =
|
||||
[
|
||||
@@ -38,6 +26,13 @@ public partial class MessageListPageViewModel : MailBaseViewModel
|
||||
MailOperation.MoveToJunk
|
||||
];
|
||||
|
||||
private readonly List<MailListDisplayMode> availableMailSpacingOptions =
|
||||
[
|
||||
MailListDisplayMode.Compact,
|
||||
MailListDisplayMode.Medium,
|
||||
MailListDisplayMode.Spacious
|
||||
];
|
||||
|
||||
public List<string> AvailableHoverActionsTranslations { get; set; } =
|
||||
[
|
||||
Translator.HoverActionOption_Archive,
|
||||
@@ -47,6 +42,37 @@ public partial class MessageListPageViewModel : MailBaseViewModel
|
||||
Translator.HoverActionOption_MoveJunk
|
||||
];
|
||||
|
||||
public IMailItemDisplayInformation DemoPreviewMailItemInformation { get; } = new DemoMailItemDisplayInformation();
|
||||
|
||||
public MailListDisplayMode SelectedMailSpacingMode => availableMailSpacingOptions[selectedMailSpacingIndex];
|
||||
|
||||
private int selectedMarkAsOptionIndex;
|
||||
public int SelectedMarkAsOptionIndex
|
||||
{
|
||||
get => selectedMarkAsOptionIndex;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref selectedMarkAsOptionIndex, value) && value >= 0)
|
||||
{
|
||||
PreferencesService.MarkAsPreference = (MailMarkAsOption)Enum.GetValues<MailMarkAsOption>().GetValue(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int selectedMailSpacingIndex;
|
||||
public int SelectedMailSpacingIndex
|
||||
{
|
||||
get => selectedMailSpacingIndex;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref selectedMailSpacingIndex, value) && value >= 0 && value < availableMailSpacingOptions.Count)
|
||||
{
|
||||
PreferencesService.MailItemDisplayMode = availableMailSpacingOptions[value];
|
||||
OnPropertyChanged(nameof(SelectedMailSpacingMode));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region Properties
|
||||
private int leftHoverActionIndex;
|
||||
public int LeftHoverActionIndex
|
||||
@@ -88,13 +114,19 @@ public partial class MessageListPageViewModel : MailBaseViewModel
|
||||
}
|
||||
#endregion
|
||||
|
||||
public MessageListPageViewModel(IPreferencesService preferencesService, IThumbnailService thumbnailService)
|
||||
public MessageListPageViewModel(IPreferencesService preferencesService,
|
||||
IThumbnailService thumbnailService,
|
||||
IStatePersistanceService statePersistenceService,
|
||||
IDialogServiceBase dialogService)
|
||||
{
|
||||
PreferencesService = preferencesService;
|
||||
_thumbnailService = thumbnailService;
|
||||
_statePersistenceService = statePersistenceService;
|
||||
_dialogService = dialogService;
|
||||
leftHoverActionIndex = availableHoverActions.IndexOf(PreferencesService.LeftHoverAction);
|
||||
centerHoverActionIndex = availableHoverActions.IndexOf(PreferencesService.CenterHoverAction);
|
||||
rightHoverActionIndex = availableHoverActions.IndexOf(PreferencesService.RightHoverAction);
|
||||
selectedMailSpacingIndex = availableMailSpacingOptions.IndexOf(PreferencesService.MailItemDisplayMode);
|
||||
SelectedMarkAsOptionIndex = Array.IndexOf(Enum.GetValues<MailMarkAsOption>(), PreferencesService.MarkAsPreference);
|
||||
}
|
||||
|
||||
@@ -103,4 +135,39 @@ public partial class MessageListPageViewModel : MailBaseViewModel
|
||||
{
|
||||
await _thumbnailService.ClearCache();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ResetMailListPaneLength()
|
||||
{
|
||||
_statePersistenceService.MailListPaneLength = 420;
|
||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Info, Translator.Info_MailListSizeResetSuccessMessage, InfoBarMessageType.Success);
|
||||
}
|
||||
|
||||
private sealed class DemoMailItemDisplayInformation : IMailItemDisplayInformation
|
||||
{
|
||||
public event PropertyChangedEventHandler PropertyChanged
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
public string Subject => "Quarterly planning notes";
|
||||
public string FromName => "Ava Brooks";
|
||||
public string FromAddress => "ava@contoso.com";
|
||||
public string PreviewText => "Agenda draft, attendee updates, and a few follow-up items for this week.";
|
||||
public bool IsRead => false;
|
||||
public bool IsDraft => false;
|
||||
public bool HasAttachments => true;
|
||||
public bool IsCalendarEvent => false;
|
||||
public bool IsFlagged => true;
|
||||
public DateTime CreationDate => DateTime.Now.AddMinutes(-12);
|
||||
public Guid? ContactPictureFileId => null;
|
||||
public bool ThumbnailUpdatedEvent => false;
|
||||
public bool IsThreadExpanded => false;
|
||||
public AccountContact SenderContact => new()
|
||||
{
|
||||
Address = "ava@contoso.com",
|
||||
Name = "Ava Brooks"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Wino has complex selected item detection mechanism with nested ListViews that
|
||||
/// supports multi selection with threads. Each list view will raise this for mail list page
|
||||
/// to react.
|
||||
/// </summary>
|
||||
public class MailItemSelectedEvent
|
||||
{
|
||||
public MailItemSelectedEvent(MailItemViewModel selectedMailItem)
|
||||
{
|
||||
SelectedMailItem = selectedMailItem;
|
||||
}
|
||||
|
||||
public MailItemViewModel SelectedMailItem { get; set; }
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Selected item removed event.
|
||||
/// </summary>
|
||||
public class MailItemSelectionRemovedEvent
|
||||
{
|
||||
public MailItemSelectionRemovedEvent(MailItemViewModel removedMailItem)
|
||||
{
|
||||
RemovedMailItem = removedMailItem;
|
||||
}
|
||||
|
||||
public MailItemViewModel RemovedMailItem { get; set; }
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// When the rendering page is active, but new item is requested to be rendered.
|
||||
/// To not trigger navigation again and re-use existing Chromium.
|
||||
/// </summary>
|
||||
/// <param name="MailItemViewModel"></param>
|
||||
public record NewMailItemRenderingRequestedEvent(MailItemViewModel MailItemViewModel);
|
||||
@@ -0,0 +1,10 @@
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Requests refreshing the currently active reader page (mail rendering or compose)
|
||||
/// with a different selected mail item without re-navigation.
|
||||
/// </summary>
|
||||
/// <param name="MailItemViewModel">The selected mail item to refresh with.</param>
|
||||
public record ReaderItemRefreshRequestedEvent(MailItemViewModel MailItemViewModel);
|
||||
@@ -1,8 +1,8 @@
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
using System;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// When listing view model manipulated the selected mail container in the UI.
|
||||
/// </summary>
|
||||
public record SelectMailItemContainerEvent(MailItemViewModel SelectedMailViewModel, bool ScrollToItem = false);
|
||||
public record SelectMailItemContainerEvent(Guid MailUniqueId, bool ScrollToItem = false);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
|
||||
namespace Wino.Mail.ViewModels.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// When a swipe action is performed on a mail item container.
|
||||
/// </summary>
|
||||
public record SwipeActionRequested(MailOperation Operation, IMailListItem MailItem);
|
||||
@@ -0,0 +1,122 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.ViewModels.Data;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
using Wino.Messaging.Client.Navigation;
|
||||
|
||||
namespace Wino.Mail.ViewModels;
|
||||
|
||||
public partial class ProviderSelectionPageViewModel : MailBaseViewModel
|
||||
{
|
||||
private readonly IProviderService _providerService;
|
||||
private readonly INewThemeService _themeService;
|
||||
|
||||
public WelcomeWizardContext WizardContext { get; }
|
||||
|
||||
public List<IProviderDetail> Providers { get; private set; } = [];
|
||||
public List<AppColorViewModel> AvailableColors { get; private set; } = [];
|
||||
|
||||
[ObservableProperty]
|
||||
public partial IProviderDetail SelectedProvider { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial AppColorViewModel SelectedColor { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string AccountName { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool CanProceed { get; set; }
|
||||
|
||||
public bool IsColorSelected => SelectedColor != null;
|
||||
|
||||
public ProviderSelectionPageViewModel(
|
||||
IProviderService providerService,
|
||||
INewThemeService themeService,
|
||||
WelcomeWizardContext wizardContext)
|
||||
{
|
||||
_providerService = providerService;
|
||||
_themeService = themeService;
|
||||
WizardContext = wizardContext;
|
||||
}
|
||||
|
||||
public override void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedTo(mode, parameters);
|
||||
|
||||
Providers = _providerService.GetAvailableProviders();
|
||||
AvailableColors = _themeService.GetAvailableAccountColors()
|
||||
.Select(hex => new AppColorViewModel(hex))
|
||||
.ToList();
|
||||
|
||||
// Restore from wizard context if navigating back
|
||||
if (WizardContext.SelectedProvider != null)
|
||||
{
|
||||
SelectedProvider = Providers.FirstOrDefault(p =>
|
||||
p.Type == WizardContext.SelectedProvider.Type &&
|
||||
p.SpecialImapProvider == WizardContext.SelectedProvider.SpecialImapProvider);
|
||||
AccountName = WizardContext.AccountName;
|
||||
|
||||
if (WizardContext.AccountColorHex != null)
|
||||
SelectedColor = AvailableColors.FirstOrDefault(c => c.Hex == WizardContext.AccountColorHex);
|
||||
}
|
||||
|
||||
Validate();
|
||||
}
|
||||
|
||||
partial void OnSelectedProviderChanged(IProviderDetail value) => Validate();
|
||||
partial void OnAccountNameChanged(string value) => Validate();
|
||||
partial void OnSelectedColorChanged(AppColorViewModel value) => OnPropertyChanged(nameof(IsColorSelected));
|
||||
|
||||
[RelayCommand]
|
||||
private void ClearColor() => SelectedColor = null;
|
||||
|
||||
private void Validate()
|
||||
{
|
||||
CanProceed = SelectedProvider != null && !string.IsNullOrWhiteSpace(AccountName);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Proceed()
|
||||
{
|
||||
if (!CanProceed) return;
|
||||
|
||||
// Persist to wizard context
|
||||
WizardContext.SelectedProvider = SelectedProvider;
|
||||
WizardContext.AccountName = AccountName?.Trim();
|
||||
WizardContext.AccountColorHex = SelectedColor?.Hex ?? string.Empty;
|
||||
|
||||
if (WizardContext.IsGenericImap)
|
||||
{
|
||||
// Navigate to ImapCalDavSettingsPage in wizard mode
|
||||
var context = ImapCalDavSettingsNavigationContext.CreateForWizardMode(
|
||||
WizardContext.BuildAccountCreationDialogResult());
|
||||
|
||||
Messenger.Send(new BreadcrumbNavigationRequested(
|
||||
Translator.ImapCalDavSettingsPage_TitleCreate,
|
||||
WinoPage.ImapCalDavSettingsPage,
|
||||
context));
|
||||
}
|
||||
else if (SelectedProvider.SpecialImapProvider is SpecialImapProvider.iCloud or SpecialImapProvider.Yahoo)
|
||||
{
|
||||
// Navigate to credentials page for special IMAP providers
|
||||
Messenger.Send(new BreadcrumbNavigationRequested(
|
||||
SelectedProvider.Name,
|
||||
WinoPage.SpecialImapCredentialsPage));
|
||||
}
|
||||
else
|
||||
{
|
||||
// OAuth — go directly to progress page
|
||||
Messenger.Send(new BreadcrumbNavigationRequested(
|
||||
Translator.WelcomeWizard_Step3Title,
|
||||
WinoPage.AccountSetupProgressPage));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Common;
|
||||
|
||||
namespace Wino.Mail.ViewModels;
|
||||
|
||||
public partial class SignatureAndEncryptionPageViewModel : MailBaseViewModel
|
||||
{
|
||||
private readonly ISmimeCertificateService _smimeCertificateService;
|
||||
private readonly IDialogServiceBase _dialogService;
|
||||
private readonly IFileService _fileService;
|
||||
|
||||
public ObservableCollection<X509Certificate2> PersonalCertificates { get; } = [];
|
||||
public ObservableCollection<X509Certificate2> RecipientCertificates { get; } = [];
|
||||
public List<X509Certificate2> SelectedPersonalCertificates { get; } = [];
|
||||
public List<X509Certificate2> SelectedRecipientCertificates { get; } = [];
|
||||
|
||||
public bool PersonalCertificatesEmpty => PersonalCertificates.Count == 0;
|
||||
|
||||
public SignatureAndEncryptionPageViewModel(
|
||||
IDialogServiceBase dialogService,
|
||||
ISmimeCertificateService smimeCertificateService,
|
||||
IFileService fileService
|
||||
)
|
||||
{
|
||||
_dialogService = dialogService;
|
||||
_fileService = fileService;
|
||||
_smimeCertificateService = smimeCertificateService;
|
||||
|
||||
PersonalCertificates.CollectionChanged += (s, e) => { OnPropertyChanged(nameof(PersonalCertificatesEmpty)); };
|
||||
LoadAllCertificates();
|
||||
}
|
||||
|
||||
private void LoadAllCertificates()
|
||||
{
|
||||
PersonalCertificates.Clear();
|
||||
var personalCerts = _smimeCertificateService.GetCertificates();
|
||||
foreach (var cert in personalCerts)
|
||||
{
|
||||
PersonalCertificates.Add(cert);
|
||||
}
|
||||
|
||||
// Recipient certificates
|
||||
RecipientCertificates.Clear();
|
||||
var recipientCerts = _smimeCertificateService.GetCertificates(storeName: StoreName.AddressBook);
|
||||
foreach (var cert in recipientCerts)
|
||||
{
|
||||
RecipientCertificates.Add(cert);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task ImportPersonalCertificatesAsync()
|
||||
{
|
||||
await ImportCertificates(StoreName.My);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task ImportRecipientCertificatesAsync()
|
||||
{
|
||||
await ImportCertificates(StoreName.AddressBook);
|
||||
}
|
||||
|
||||
private async Task ImportCertificates(StoreName storeName)
|
||||
{
|
||||
var files = await PickCertificateFilesAsync();
|
||||
var failedImports = new List<string>();
|
||||
var successCount = 0;
|
||||
foreach (var file in files)
|
||||
{
|
||||
string password = null;
|
||||
if (file.FileExtension.Equals(".pfx") || file.FileExtension.Equals(".p12"))
|
||||
{
|
||||
password = await PromptForPasswordAsync(file.FileName);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_smimeCertificateService.ImportCertificate(file.FileExtension, file.Data, password,
|
||||
storeName: storeName);
|
||||
successCount++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failedImports.Add($"{file.FileName}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
LoadAllCertificates();
|
||||
if (successCount > 0)
|
||||
{
|
||||
_dialogService.InfoBarMessage(
|
||||
string.Format(Translator.Smime_ImportCertificates_Success),
|
||||
Translator.GeneralTitle_Info,
|
||||
InfoBarMessageType.Success);
|
||||
}
|
||||
if (failedImports.Count > 0)
|
||||
{
|
||||
await _dialogService.ShowMessageAsync(
|
||||
$"{Translator.Smime_ImportCertificates_Error}\n\n{string.Join("\n", failedImports)}",
|
||||
Translator.GeneralTitle_Warning,
|
||||
Core.Domain.Enums.WinoCustomMessageDialogIcon.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task RemovePersonalCertificatesAsync()
|
||||
{
|
||||
await RemoveCertificatesAsync(SelectedPersonalCertificates, StoreName.My);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task RemoveRecipientCertificatesAsync()
|
||||
{
|
||||
await RemoveCertificatesAsync(SelectedRecipientCertificates, StoreName.AddressBook);
|
||||
}
|
||||
|
||||
private async Task RemoveCertificatesAsync(List<X509Certificate2> certificates, StoreName storeName)
|
||||
{
|
||||
if (certificates.Any())
|
||||
{
|
||||
var confirm = await ConfirmAsync(string.Format(Translator.Smime_RemoveCertificates_Confirm,
|
||||
string.Join(", ", certificates.Select(cert => cert.Subject))));
|
||||
if (confirm)
|
||||
{
|
||||
foreach (var cert in certificates)
|
||||
{
|
||||
_smimeCertificateService.RemoveCertificate(cert.Thumbprint, storeName: storeName);
|
||||
}
|
||||
|
||||
LoadAllCertificates();
|
||||
_dialogService.InfoBarMessage(
|
||||
Translator.Smime_RemoveCertificates_Success,
|
||||
Translator.GeneralTitle_Info,
|
||||
InfoBarMessageType.Success
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task ExportPersonalCertificatesAsync()
|
||||
{
|
||||
await ExportCertificatesAsync(SelectedPersonalCertificates);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task ExportRecipientCertificatesAsync()
|
||||
{
|
||||
await ExportCertificatesAsync(SelectedRecipientCertificates);
|
||||
}
|
||||
|
||||
// Export logic for .cer or .pem
|
||||
private async Task ExportCertificatesAsync(IEnumerable<X509Certificate2> cert)
|
||||
{
|
||||
var failedExports = new List<string>();
|
||||
var successCount = 0;
|
||||
foreach (var certificate in cert)
|
||||
{
|
||||
var fileName = $"{certificate.Subject.Replace("CN=", "")}.cer";
|
||||
var path = await _dialogService.PickFilePathAsync(fileName);
|
||||
if (path != null)
|
||||
{
|
||||
var folderPath = System.IO.Path.GetDirectoryName(path);
|
||||
await using var stream = await _fileService.GetFileStreamAsync(folderPath, fileName);
|
||||
if (stream != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var certificateData = certificate.Export(X509ContentType.Cert);
|
||||
await stream.WriteAsync(certificateData, 0, certificateData.Length);
|
||||
await stream.FlushAsync();
|
||||
successCount++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failedExports.Add($"{certificate.Subject}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
failedExports.Add($"{certificate.Subject}: File stream error");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (successCount > 0)
|
||||
{
|
||||
_dialogService.InfoBarMessage(
|
||||
Translator.Smime_ExportCertificates_Success,
|
||||
Translator.GeneralTitle_Info,
|
||||
InfoBarMessageType.Success
|
||||
);
|
||||
}
|
||||
if (failedExports.Count > 0)
|
||||
{
|
||||
await _dialogService.ShowMessageAsync(
|
||||
$"{Translator.Smime_ExportCertificates_Error}\n\n{string.Join("\n", failedExports)}",
|
||||
Translator.GeneralTitle_Warning,
|
||||
Core.Domain.Enums.WinoCustomMessageDialogIcon.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowCertificateDetailsAsync(X509Certificate2 cert)
|
||||
{
|
||||
var details = string.Format(Translator.Smime_CertificateDetails, cert.Subject, cert.Issuer, cert.NotBefore,
|
||||
cert.NotAfter, cert.Thumbprint);
|
||||
await _dialogService.ShowMessageAsync(details, Translator.GeneralTitle_Info,
|
||||
Core.Domain.Enums.WinoCustomMessageDialogIcon.Information);
|
||||
}
|
||||
|
||||
// Confirmation dialog
|
||||
private async Task<bool> ConfirmAsync(string message)
|
||||
{
|
||||
return await _dialogService.ShowConfirmationDialogAsync(message, Translator.Smime_Confirm_Title,
|
||||
Translator.Buttons_Yes);
|
||||
}
|
||||
|
||||
// File picker for importing certificates
|
||||
private async Task<List<SharedFile>> PickCertificateFilesAsync()
|
||||
{
|
||||
return await _dialogService.PickFilesAsync(".pfx", ".p12", ".cer", ".crt");
|
||||
}
|
||||
|
||||
// Ask for password for .pfx/.p12
|
||||
private async Task<string> PromptForPasswordAsync(string fileName)
|
||||
{
|
||||
return await _dialogService.ShowTextInputDialogAsync("",
|
||||
Translator.Smime_CertificatePassword_Title,
|
||||
string.Format(Translator.Smime_CertificatePassword_Placeholder, fileName), Translator.Buttons_OK);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
using Wino.Messaging.Client.Navigation;
|
||||
|
||||
namespace Wino.Mail.ViewModels;
|
||||
|
||||
public partial class SpecialImapCredentialsPageViewModel : MailBaseViewModel
|
||||
{
|
||||
private static readonly Dictionary<SpecialImapProvider, string> AppPasswordHelpLinks = new()
|
||||
{
|
||||
{ SpecialImapProvider.iCloud, "https://support.apple.com/en-us/102654" },
|
||||
{ SpecialImapProvider.Yahoo, "http://help.yahoo.com/kb/SLN15241.html" },
|
||||
};
|
||||
|
||||
private readonly INativeAppService _nativeAppService;
|
||||
|
||||
public WelcomeWizardContext WizardContext { get; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string DisplayName { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string EmailAddress { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string AppSpecificPassword { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int SelectedCalendarModeIndex { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool CanProceed { get; set; }
|
||||
|
||||
public string AppPasswordHelpUrl
|
||||
{
|
||||
get
|
||||
{
|
||||
if (WizardContext.SelectedProvider == null) return null;
|
||||
AppPasswordHelpLinks.TryGetValue(WizardContext.SelectedProvider.SpecialImapProvider, out var url);
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
public string CalendarModeCalDavDescription
|
||||
=> WizardContext.SelectedProvider?.SpecialImapProvider == SpecialImapProvider.iCloud
|
||||
? Translator.ProviderSelection_CalendarMode_CalDavDescription_Apple
|
||||
: Translator.ProviderSelection_CalendarMode_CalDavDescription_Yahoo;
|
||||
|
||||
public SpecialImapCredentialsPageViewModel(
|
||||
INativeAppService nativeAppService,
|
||||
WelcomeWizardContext wizardContext)
|
||||
{
|
||||
_nativeAppService = nativeAppService;
|
||||
WizardContext = wizardContext;
|
||||
}
|
||||
|
||||
public override void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedTo(mode, parameters);
|
||||
|
||||
// Restore from context when navigating back
|
||||
DisplayName = WizardContext.DisplayName;
|
||||
EmailAddress = WizardContext.EmailAddress;
|
||||
AppSpecificPassword = WizardContext.AppSpecificPassword;
|
||||
|
||||
SelectedCalendarModeIndex = WizardContext.CalendarSupportMode switch
|
||||
{
|
||||
ImapCalendarSupportMode.CalDav => 1,
|
||||
ImapCalendarSupportMode.LocalOnly => 2,
|
||||
_ => 0
|
||||
};
|
||||
|
||||
OnPropertyChanged(nameof(AppPasswordHelpUrl));
|
||||
OnPropertyChanged(nameof(CalendarModeCalDavDescription));
|
||||
|
||||
Validate();
|
||||
}
|
||||
|
||||
partial void OnDisplayNameChanged(string value) => Validate();
|
||||
partial void OnEmailAddressChanged(string value) => Validate();
|
||||
partial void OnAppSpecificPasswordChanged(string value) => Validate();
|
||||
|
||||
private void Validate()
|
||||
{
|
||||
CanProceed = !string.IsNullOrWhiteSpace(DisplayName)
|
||||
&& !string.IsNullOrWhiteSpace(EmailAddress)
|
||||
&& EmailValidation.EmailValidator.Validate(EmailAddress ?? string.Empty)
|
||||
&& !string.IsNullOrWhiteSpace(AppSpecificPassword);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Proceed()
|
||||
{
|
||||
if (!CanProceed) return;
|
||||
|
||||
WizardContext.DisplayName = DisplayName?.Trim();
|
||||
WizardContext.EmailAddress = EmailAddress?.Trim();
|
||||
WizardContext.AppSpecificPassword = AppSpecificPassword?.Trim();
|
||||
WizardContext.CalendarSupportMode = SelectedCalendarModeIndex switch
|
||||
{
|
||||
1 => ImapCalendarSupportMode.CalDav,
|
||||
2 => ImapCalendarSupportMode.LocalOnly,
|
||||
_ => ImapCalendarSupportMode.Disabled
|
||||
};
|
||||
|
||||
Messenger.Send(new BreadcrumbNavigationRequested(
|
||||
Translator.WelcomeWizard_Step3Title,
|
||||
WinoPage.AccountSetupProgressPage));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task OpenAppPasswordHelp()
|
||||
{
|
||||
var url = AppPasswordHelpUrl;
|
||||
if (url != null)
|
||||
await _nativeAppService.LaunchUriAsync(new Uri(url));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Serilog;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.Extensions;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
|
||||
namespace Wino.Mail.ViewModels;
|
||||
|
||||
public partial class StoragePageViewModel(
|
||||
IAccountService accountService,
|
||||
IMimeStorageService mimeStorageService,
|
||||
IMailDialogService dialogService) : MailBaseViewModel
|
||||
{
|
||||
private readonly ILogger _logger = Log.ForContext<StoragePageViewModel>();
|
||||
private readonly IAccountService _accountService = accountService;
|
||||
private readonly IMimeStorageService _mimeStorageService = mimeStorageService;
|
||||
private readonly IMailDialogService _dialogService = dialogService;
|
||||
|
||||
public ObservableCollection<AccountStorageItemViewModel> AccountStorageItems { get; } = [];
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsBusy))]
|
||||
public partial bool IsLoading { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsBusy))]
|
||||
public partial bool IsCleaning { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string MimeRootPath { get; set; } = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string SummaryText { get; set; } = "";
|
||||
|
||||
public bool IsBusy => IsLoading || IsCleaning;
|
||||
|
||||
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedTo(mode, parameters);
|
||||
await ExecuteUIThread(() => { SummaryText = Translator.SettingsStorage_NoLocalMimeDataFound; });
|
||||
|
||||
await RefreshStorageAsync();
|
||||
}
|
||||
|
||||
partial void OnIsLoadingChanged(bool value)
|
||||
{
|
||||
UpdateAccountBusyState();
|
||||
}
|
||||
|
||||
partial void OnIsCleaningChanged(bool value)
|
||||
{
|
||||
UpdateAccountBusyState();
|
||||
}
|
||||
|
||||
private void UpdateAccountBusyState()
|
||||
{
|
||||
Dispatcher.ExecuteOnUIThread(() =>
|
||||
{
|
||||
foreach (var item in AccountStorageItems)
|
||||
{
|
||||
item.IsBusy = IsBusy;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task RefreshStorageAsync()
|
||||
{
|
||||
if (IsBusy) return;
|
||||
|
||||
await ExecuteUIThread(() => { IsLoading = true; });
|
||||
|
||||
try
|
||||
{
|
||||
var mimeRootPath = await _mimeStorageService.GetMimeRootPathAsync().ConfigureAwait(false);
|
||||
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
|
||||
var sizeMap = await _mimeStorageService.GetAccountsMimeStorageSizesAsync(accounts.Select(a => a.Id)).ConfigureAwait(false);
|
||||
|
||||
var storageItems = accounts
|
||||
.Select(account =>
|
||||
{
|
||||
sizeMap.TryGetValue(account.Id, out var accountSize);
|
||||
var viewModel = new AccountStorageItemViewModel(account, accountSize, DeleteAllCommand, DeleteOlderThanOneMonthCommand, DeleteOlderThanThreeMonthsCommand, DeleteOlderThanSixMonthsCommand, DeleteOlderThanOneYearCommand);
|
||||
viewModel.SizeDescription = string.Format(Translator.SettingsStorage_AccountUsageDescription, viewModel.SizeText);
|
||||
return viewModel;
|
||||
})
|
||||
.OrderByDescending(a => a.SizeBytes)
|
||||
.ToList();
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
MimeRootPath = mimeRootPath;
|
||||
AccountStorageItems.Clear();
|
||||
|
||||
foreach (var item in storageItems)
|
||||
{
|
||||
AccountStorageItems.Add(item);
|
||||
}
|
||||
|
||||
var total = storageItems.Sum(a => a.SizeBytes);
|
||||
SummaryText = storageItems.Count == 0
|
||||
? Translator.SettingsStorage_NoAccountsFound
|
||||
: string.Format(Translator.SettingsStorage_TotalUsage, total.GetBytesReadable());
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Failed to refresh storage data.");
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error, ex.Message, Core.Domain.Enums.InfoBarMessageType.Error);
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
await ExecuteUIThread(() => { IsLoading = false; });
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task DeleteAllAsync(AccountStorageItemViewModel accountItem)
|
||||
{
|
||||
if (accountItem == null || IsBusy) return;
|
||||
|
||||
bool approved = await _dialogService.ShowConfirmationDialogAsync(
|
||||
string.Format(Translator.SettingsStorage_DeleteAll_Confirm_Message, accountItem.AccountName),
|
||||
Translator.SettingsStorage_DeleteAll_Confirm_Title,
|
||||
Translator.Buttons_Delete);
|
||||
|
||||
if (!approved) return;
|
||||
|
||||
await ExecuteUIThread(() => { IsCleaning = true; });
|
||||
|
||||
try
|
||||
{
|
||||
await _mimeStorageService.DeleteAccountMimeStorageAsync(accountItem.Account.Id).ConfigureAwait(false);
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Info, Translator.SettingsStorage_DeleteAll_Success, Core.Domain.Enums.InfoBarMessageType.Success);
|
||||
});
|
||||
await RefreshStorageAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Failed to delete all MIME content for account {AccountId}", accountItem.Account.Id);
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error, ex.Message, Core.Domain.Enums.InfoBarMessageType.Error);
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
await ExecuteUIThread(() => { IsCleaning = false; });
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private Task DeleteOlderThanOneMonthAsync(AccountStorageItemViewModel accountItem)
|
||||
=> DeleteOlderThanAsync(accountItem, 1);
|
||||
|
||||
[RelayCommand]
|
||||
private Task DeleteOlderThanThreeMonthsAsync(AccountStorageItemViewModel accountItem)
|
||||
=> DeleteOlderThanAsync(accountItem, 3);
|
||||
|
||||
[RelayCommand]
|
||||
private Task DeleteOlderThanSixMonthsAsync(AccountStorageItemViewModel accountItem)
|
||||
=> DeleteOlderThanAsync(accountItem, 6);
|
||||
|
||||
[RelayCommand]
|
||||
private Task DeleteOlderThanOneYearAsync(AccountStorageItemViewModel accountItem)
|
||||
=> DeleteOlderThanAsync(accountItem, 12);
|
||||
|
||||
private async Task DeleteOlderThanAsync(AccountStorageItemViewModel accountItem, int months)
|
||||
{
|
||||
if (accountItem == null || IsBusy) return;
|
||||
|
||||
string rangeText = GetRangeText(months);
|
||||
|
||||
bool approved = await _dialogService.ShowConfirmationDialogAsync(
|
||||
string.Format(Translator.SettingsStorage_DeleteOld_Confirm_Message, rangeText, accountItem.AccountName),
|
||||
Translator.SettingsStorage_DeleteOld_Confirm_Title,
|
||||
Translator.Buttons_Delete);
|
||||
|
||||
if (!approved) return;
|
||||
|
||||
await ExecuteUIThread(() => { IsCleaning = true; });
|
||||
|
||||
try
|
||||
{
|
||||
var cutoffDateUtc = DateTime.UtcNow.AddMonths(-months);
|
||||
var deletedDirectoryCount = await _mimeStorageService
|
||||
.DeleteAccountMimeStorageOlderThanAsync(accountItem.Account.Id, cutoffDateUtc)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
_dialogService.InfoBarMessage(
|
||||
Translator.GeneralTitle_Info,
|
||||
string.Format(Translator.SettingsStorage_DeleteOld_Success, deletedDirectoryCount, rangeText),
|
||||
Core.Domain.Enums.InfoBarMessageType.Success);
|
||||
});
|
||||
|
||||
await RefreshStorageAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Failed to delete MIME content by cutoff for account {AccountId}", accountItem.Account.Id);
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error, ex.Message, Core.Domain.Enums.InfoBarMessageType.Error);
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
await ExecuteUIThread(() => { IsCleaning = false; });
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetRangeText(int months)
|
||||
{
|
||||
return months switch
|
||||
{
|
||||
1 => Translator.SettingsStorage_1Month,
|
||||
3 => Translator.SettingsStorage_3Months,
|
||||
6 => Translator.SettingsStorage_6Months,
|
||||
12 => Translator.SettingsStorage_1Year,
|
||||
_ => string.Format(Translator.SettingsStorage_Months, months)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Accounts;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.Domain.Models.Updates;
|
||||
using Wino.Messaging.Client.Navigation;
|
||||
using Wino.Messaging.UI;
|
||||
|
||||
namespace Wino.Mail.ViewModels;
|
||||
|
||||
public partial class WelcomePageV2ViewModel : MailBaseViewModel
|
||||
{
|
||||
private readonly IUpdateManager _updateManager;
|
||||
private readonly IMailDialogService _dialogService;
|
||||
private readonly IWinoAccountDataSyncService _syncService;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial List<UpdateNoteSection> UpdateSections { get; set; } = [];
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(GetStartedCommand))]
|
||||
[NotifyCanExecuteChangedFor(nameof(ImportFromWinoAccountCommand))]
|
||||
public partial bool IsImportInProgress { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(HasImportStatus))]
|
||||
public partial string ImportStatusMessage { get; set; } = string.Empty;
|
||||
|
||||
public bool HasImportStatus => !string.IsNullOrWhiteSpace(ImportStatusMessage);
|
||||
|
||||
public WelcomePageV2ViewModel(IUpdateManager updateManager,
|
||||
IMailDialogService dialogService,
|
||||
IWinoAccountDataSyncService syncService)
|
||||
{
|
||||
_updateManager = updateManager;
|
||||
_dialogService = dialogService;
|
||||
_syncService = syncService;
|
||||
}
|
||||
|
||||
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedTo(mode, parameters);
|
||||
|
||||
try
|
||||
{
|
||||
var updateNotes = await _updateManager.GetLatestUpdateNotesAsync();
|
||||
UpdateSections = updateNotes.Sections;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
UpdateSections = [];
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanOpenWelcomeActions))]
|
||||
private void GetStarted()
|
||||
{
|
||||
Messenger.Send(new BreadcrumbNavigationRequested(
|
||||
Translator.WelcomeWizard_Step2Title,
|
||||
WinoPage.ProviderSelectionPage));
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanOpenWelcomeActions))]
|
||||
private async Task ImportFromWinoAccountAsync()
|
||||
{
|
||||
await ExecuteUIThread(() => ImportStatusMessage = string.Empty);
|
||||
|
||||
try
|
||||
{
|
||||
var account = await _dialogService.ShowWinoAccountLoginDialogAsync().ConfigureAwait(false);
|
||||
if (account == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await ExecuteUIThread(() => IsImportInProgress = true);
|
||||
|
||||
var result = await _syncService.ImportAsync(new WinoAccountSyncSelection()).ConfigureAwait(false);
|
||||
if (result.ImportedMailboxCount > 0)
|
||||
{
|
||||
ReportUIChange(new WelcomeImportCompletedMessage(result.ImportedMailboxCount));
|
||||
return;
|
||||
}
|
||||
|
||||
await ExecuteUIThread(() => ImportStatusMessage = BuildInlineImportMessage(result));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await _dialogService.ShowMessageAsync(ex.Message, Translator.GeneralTitle_Error, WinoCustomMessageDialogIcon.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await ExecuteUIThread(() => IsImportInProgress = false);
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanOpenWelcomeActions() => !IsImportInProgress;
|
||||
|
||||
private static string BuildInlineImportMessage(WinoAccountSyncImportResult result)
|
||||
{
|
||||
var preferencesMessage = result.FailedPreferenceCount > 0
|
||||
? string.Format(Translator.WinoAccount_Management_ImportPartial, result.AppliedPreferenceCount, result.FailedPreferenceCount)
|
||||
: result.HadRemotePreferences
|
||||
? string.Format(Translator.WinoAccount_Management_ImportPreferencesSucceeded, result.AppliedPreferenceCount)
|
||||
: string.Empty;
|
||||
|
||||
if (result.RemoteMailboxCount == 0)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(preferencesMessage)
|
||||
? Translator.WelcomeWindow_ImportNoAccountsFound
|
||||
: $"{preferencesMessage} {Translator.WelcomeWindow_ImportNoAccountsFound}";
|
||||
}
|
||||
|
||||
if (result.SkippedDuplicateMailboxCount > 0 && result.ImportedMailboxCount == 0)
|
||||
{
|
||||
var duplicateMessage = string.Format(Translator.WelcomeWindow_ImportDuplicateAccountsSkipped, result.SkippedDuplicateMailboxCount);
|
||||
return string.IsNullOrWhiteSpace(preferencesMessage)
|
||||
? duplicateMessage
|
||||
: $"{preferencesMessage} {duplicateMessage}";
|
||||
}
|
||||
|
||||
return string.IsNullOrWhiteSpace(preferencesMessage)
|
||||
? Translator.WinoAccount_Management_ImportEmpty
|
||||
: preferencesMessage;
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
using System;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
|
||||
namespace Wino.Mail.ViewModels;
|
||||
|
||||
public partial class WelcomePageViewModel : MailBaseViewModel
|
||||
{
|
||||
public const string VersionFile = "1102.md";
|
||||
private readonly IMailDialogService _dialogService;
|
||||
private readonly IFileService _fileService;
|
||||
|
||||
[ObservableProperty]
|
||||
private string currentVersionNotes;
|
||||
|
||||
public WelcomePageViewModel(IMailDialogService dialogService, IFileService fileService)
|
||||
{
|
||||
_dialogService = dialogService;
|
||||
_fileService = fileService;
|
||||
}
|
||||
|
||||
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedTo(mode, parameters);
|
||||
|
||||
try
|
||||
{
|
||||
CurrentVersionNotes = await _fileService.GetFileContentByApplicationUriAsync($"ms-appx:///Assets/ReleaseNotes/{VersionFile}");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error, "Can't find the patch notes.", Core.Domain.Enums.InfoBarMessageType.Information);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Platforms>x86;x64;arm64</Platforms>
|
||||
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
|
||||
<AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>
|
||||
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
|
||||
<IsTrimmable>true</IsTrimmable>
|
||||
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="EmailValidation" />
|
||||
<PackageReference Include="Microsoft.Identity.Client" />
|
||||
<PackageReference Include="Sentry.Serilog" />
|
||||
<PackageReference Include="MimeKit" />
|
||||
<PackageReference Include="System.Reactive" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
@@ -19,4 +23,7 @@
|
||||
<ProjectReference Include="..\Wino.Messages\Wino.Messaging.csproj" />
|
||||
<ProjectReference Include="..\Wino.Services\Wino.Services.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Helpers\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user